├── .github ├── actions │ └── setup-env │ │ └── action.yaml └── workflows │ ├── deploy-website.yaml │ └── template-catalogue.yaml ├── .gitignore ├── .vscode ├── cspell.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── .gitignore ├── movie-personal-library │ ├── README.md │ ├── api │ │ ├── .gitignore │ │ ├── data │ │ │ └── movie-db.json │ │ ├── httpc.json │ │ ├── package.json │ │ ├── src │ │ │ ├── calls │ │ │ │ └── index.ts │ │ │ ├── db.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── console │ │ ├── package.json │ │ ├── src │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── package.json │ ├── pnpm-lock.yaml │ └── pnpm-workspace.yaml ├── netlify-functions │ ├── README.md │ ├── api.http │ ├── netlify.toml │ ├── package.json │ ├── pnpm-lock.yaml │ ├── pnpm-workspace.yaml │ ├── public │ │ └── _redirects │ ├── src │ │ ├── calls │ │ │ └── index.ts │ │ ├── functions │ │ │ └── api.ts │ │ └── page.ts │ └── tsconfig.json └── vercel-next │ ├── .gitignore │ ├── README.md │ ├── components │ ├── InvokeAddFunction.tsx │ ├── InvokeGreetFunction.tsx │ └── client.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── [[...slug]].ts │ │ └── _calls.ts │ └── index.tsx │ ├── pnpm-lock.yaml │ ├── pnpm-workspace.yaml │ ├── public │ ├── favicon.ico │ ├── httpc-brand.svg │ ├── next.svg │ ├── thirteen.svg │ └── vercel.svg │ ├── styles │ ├── Home.module.css │ ├── font.ts │ └── globals.css │ └── tsconfig.json ├── package.json ├── packages ├── adapter-cloudflare │ ├── README.md │ ├── env.d.ts │ ├── jest.config.json │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── adapter-netlify │ ├── README.md │ ├── jest.config.json │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tests │ │ └── index.test.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── adapter-vercel │ ├── README.md │ ├── jest.config.json │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tests │ │ └── index.test.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── cli │ ├── README.md │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── call.ts │ │ │ ├── client.ts │ │ │ ├── create.ts │ │ │ └── index.ts │ │ ├── env.d.ts │ │ ├── index.ts │ │ ├── utils │ │ │ ├── fsUtils.ts │ │ │ ├── generateMetadata.ts │ │ │ ├── index.ts │ │ │ ├── log.ts │ │ │ ├── packageUtils.ts │ │ │ ├── process.ts │ │ │ └── template.ts │ │ └── version.ts │ ├── tasks │ │ └── export-version.js │ ├── tsconfig.build.json │ └── tsconfig.json ├── client │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── factory.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── middlewares.ts │ │ ├── typed.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ ├── tsconfig.dev.cjs.json │ ├── tsconfig.dev.esm.json │ └── tsconfig.json ├── create │ ├── README.md │ ├── index.mjs │ └── package.json ├── kit │ ├── README.md │ ├── caching-lru.ts │ ├── caching-redis.ts │ ├── env.d.ts │ ├── geo-maxmind.ts │ ├── jest.config.json │ ├── logging-winston.ts │ ├── node.ts │ ├── package.json │ ├── src │ │ ├── Application.ts │ │ ├── auth │ │ │ ├── AuthenticationApiKeyMiddleware.ts │ │ │ ├── AuthenticationBearerMiddleware.ts │ │ │ ├── AuthorizationMiddleware.ts │ │ │ ├── BasicAuthenticationMiddleware.ts │ │ │ ├── BearerAuthenticationService.ts │ │ │ ├── JwtService.ts │ │ │ ├── PermissionsAuthorizationService.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── pipeline.ts │ │ │ └── types.ts │ │ ├── caching │ │ │ ├── CacheMiddleware.ts │ │ │ ├── CachingService.ts │ │ │ ├── InMemoryCache.ts │ │ │ ├── LogCacheDecorator.ts │ │ │ ├── PrefixCacheDecorator.ts │ │ │ ├── WaterfallCache.ts │ │ │ ├── context.ts │ │ │ ├── decorators.ts │ │ │ ├── index.ts │ │ │ ├── lru │ │ │ │ └── index.ts │ │ │ ├── redis │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── controller │ │ │ ├── decorators.ts │ │ │ ├── httpController.ts │ │ │ └── index.ts │ │ ├── di │ │ │ ├── container.ts │ │ │ ├── context.ts │ │ │ ├── decorators.ts │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ └── tsyringe-patch.ts │ │ ├── email │ │ │ ├── MailersendEmailSender.ts │ │ │ ├── NoopEmailSender.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── env.ts │ │ ├── events │ │ │ ├── EventBus.ts │ │ │ ├── factory.ts │ │ │ ├── handling.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── fetch.ts │ │ ├── geo │ │ │ └── maxmind │ │ │ │ ├── MaxMindGeoService.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── internal │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── logging │ │ │ ├── CallLoggerMiddleware.ts │ │ │ ├── NullLoggerService.ts │ │ │ ├── console │ │ │ │ ├── LogService.ts │ │ │ │ ├── Logger.ts │ │ │ │ └── index.ts │ │ │ ├── context.ts │ │ │ ├── decorators.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── winston │ │ │ │ ├── WinstonLogService.ts │ │ │ │ ├── WinstonLogger.ts │ │ │ │ └── index.ts │ │ ├── node │ │ │ ├── Application.ts │ │ │ ├── fs.ts │ │ │ ├── index.internal.ts │ │ │ └── index.ts │ │ ├── permissions │ │ │ ├── Parser.ts │ │ │ ├── PermissionChecker.ts │ │ │ ├── PermissionModel.ts │ │ │ ├── PermissionSerializer.ts │ │ │ ├── Token.ts │ │ │ ├── builder.ts │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── services │ │ │ ├── BaseService.ts │ │ │ ├── aspects.ts │ │ │ ├── catches.ts │ │ │ ├── context.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── testing │ │ │ └── index.ts │ │ ├── types.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── objects.ts │ │ │ └── time.ts │ │ └── validation │ │ │ ├── NativeTypeValidator.ts │ │ │ ├── PredicateValidator.ts │ │ │ ├── ValidationService.ts │ │ │ ├── class │ │ │ └── index.ts │ │ │ ├── decorators.ts │ │ │ ├── index.ts │ │ │ ├── pipeline.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ ├── valibot │ │ │ └── index.ts │ │ │ └── zod │ │ │ └── index.ts │ ├── tests │ │ ├── auth │ │ │ ├── AuthenticationApiKeyMiddleware.test.ts │ │ │ ├── AuthenticationBearerMiddleware.test.ts │ │ │ └── BasicAuthenticationMiddleware.test.ts │ │ ├── caching │ │ │ ├── CacheMiddleware.test.ts │ │ │ └── PrefixCacheDecorator.test.ts │ │ ├── di │ │ │ └── decorators.test.ts │ │ ├── permissions.test.ts │ │ ├── utils.ts │ │ └── validations │ │ │ ├── ClassValidator.test.ts │ │ │ ├── NativeTypeValidator.test.ts │ │ │ ├── ValibotValidator.test.ts │ │ │ └── ValidationService.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.dev.json │ ├── tsconfig.json │ ├── validation-class.ts │ ├── validation-valibot.ts │ └── validation-zod.ts ├── server │ ├── README.md │ ├── env.d.ts │ ├── jest.config.json │ ├── node.d.ts │ ├── package.json │ ├── src │ │ ├── context.ts │ │ ├── errors.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── index.web.ts │ │ ├── internal │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── logger.ts │ │ ├── middlewares │ │ │ ├── LogCallMiddleware.ts │ │ │ ├── PassthroughMiddleware.ts │ │ │ └── index.ts │ │ ├── node │ │ │ ├── index.ts │ │ │ ├── mocks │ │ │ │ ├── IncomingMessageMock.ts │ │ │ │ ├── ServerResponseMock.ts │ │ │ │ └── index.ts │ │ │ ├── server.ts │ │ │ └── testing.ts │ │ ├── parsers │ │ │ ├── FormUrlEncodedParser.ts │ │ │ ├── HttpCCallParser.ts │ │ │ ├── Parser.ts │ │ │ ├── PathMatcher.ts │ │ │ ├── RawBodyParser.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── pipeline.ts │ │ ├── presets │ │ │ ├── index.ts │ │ │ └── static.ts │ │ ├── renderers │ │ │ ├── BinaryRenderer.ts │ │ │ ├── ErrorRenderer.ts │ │ │ ├── JsonRenderer.ts │ │ │ └── index.ts │ │ ├── requests │ │ │ ├── coors.ts │ │ │ ├── httpc.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── responses │ │ │ ├── BinaryResponse.ts │ │ │ ├── EmptyResponse.ts │ │ │ ├── ErrorResponse.ts │ │ │ ├── HttpCServerResponse.ts │ │ │ ├── JsonResponse.ts │ │ │ ├── RedirectResponse.ts │ │ │ └── index.ts │ │ ├── server.ts │ │ ├── testing │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── pipeline.ts │ │ │ └── tester.ts │ │ └── utils.ts │ ├── tests │ │ ├── PathMatcher.test.ts │ │ ├── error-handler.test.ts │ │ ├── httpCall.test.ts │ │ ├── httpGroup.test.ts │ │ ├── httpc-call-convention.test.ts │ │ ├── middlewares │ │ │ └── PassthroughMiddleware.test.ts │ │ ├── node │ │ │ └── node-error-handler.test.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ ├── tsconfig.dev.cjs.json │ ├── tsconfig.dev.esm.json │ └── tsconfig.json └── www │ ├── .prettierrc.json │ ├── README.md │ ├── assets │ ├── code-snippets │ │ ├── hero-basic-client.ts │ │ ├── hero-basic-server.ts │ │ ├── hero-context-client.ts │ │ ├── hero-context-server.ts │ │ ├── hero-context-session.ts │ │ ├── hero-middlewares-client.ts │ │ ├── hero-middlewares-server.ts │ │ ├── hero-services-client.ts │ │ ├── hero-services-server.ts │ │ ├── hero-transaction-client.ts │ │ ├── hero-transaction-server.ts │ │ ├── hero-vercel-api.ts │ │ ├── hero-vercel-client.tsx │ │ ├── hero-vercel-server.ts │ │ └── posts.ts │ └── svg │ │ ├── httpc-brand.svg │ │ ├── httpc-functions.svg │ │ ├── httpc-pipeline-middlewares.svg │ │ ├── httpc-pipeline-middlewares_.svg │ │ └── httpc-server-architecture.svg │ ├── astro.config.ts │ ├── package.json │ ├── public │ ├── _headers │ ├── _redirects │ ├── animations │ │ ├── anim-pipeline.json │ │ └── anim-publish.json │ ├── assets │ │ ├── external-link-line.svg │ │ ├── file-download.svg │ │ ├── httpc-no-boilerplate.png │ │ ├── httpc-typesafe.png │ │ ├── iosevka-custom-bold.woff2 │ │ └── iosevka-custom.woff2 │ ├── favicon.ico │ ├── favicon.svg │ ├── robots.txt │ └── rss-style.xsl │ ├── src │ ├── code-theme.ts │ ├── components │ │ ├── AdapterList.astro │ │ ├── ArticleNavigation.astro │ │ ├── Aside.astro │ │ ├── BrandIcon.astro │ │ ├── CodeBlock.astro │ │ ├── HomeSamples.astro │ │ ├── IconMenu.astro │ │ ├── Navbar.astro │ │ ├── NavbarIcons.astro │ │ ├── NavbarLinks.astro │ │ ├── PackageButton.astro │ │ ├── PackageCard.astro │ │ ├── PostList.astro │ │ ├── Rightbar.astro │ │ ├── Sidebar.astro │ │ ├── Svg.astro │ │ ├── TableOfContents.astro │ │ ├── Tag.astro │ │ ├── TagList.astro │ │ ├── TutorialList.astro │ │ └── tabs │ │ │ ├── CodeTabs.tsx │ │ │ ├── InstallPackage.astro │ │ │ ├── PackageManagerTabs.astro │ │ │ ├── Tabs.tsx │ │ │ └── store.ts │ ├── constants.ts │ ├── content │ │ ├── blog │ │ │ ├── function-nullable-on-demand-with-typescript-overloads.mdx │ │ │ └── release-v0.1.0-announcement.mdx │ │ ├── config.ts │ │ ├── docs │ │ │ ├── adapters │ │ │ │ ├── index.mdx │ │ │ │ ├── netlify.mdx │ │ │ │ ├── next.mdx │ │ │ │ └── vercel.mdx │ │ │ ├── client-generation.mdx │ │ │ ├── client-publishing.mdx │ │ │ ├── client-usage.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── httpc-call-convention.mdx │ │ │ ├── httpc-family.mdx │ │ │ ├── introduction.mdx │ │ │ ├── kit-authentication.mdx │ │ │ ├── kit-authorization.mdx │ │ │ ├── kit-caching.mdx │ │ │ ├── kit-dependency-injection.mdx │ │ │ ├── kit-extending.mdx │ │ │ ├── kit-introduction.mdx │ │ │ ├── kit-logging.mdx │ │ │ ├── kit-validation.mdx │ │ │ ├── package-httpc-cli.mdx │ │ │ ├── package-httpc-client.mdx │ │ │ ├── package-httpc-kit.mdx │ │ │ ├── package-httpc-server.mdx │ │ │ ├── server-architecture.mdx │ │ │ ├── server-extending.mdx │ │ │ ├── server-request-context.mdx │ │ │ ├── server-testing.mdx │ │ │ └── tutorials │ │ │ │ ├── guide-project-organization.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── tutorial-movie-library.mdx │ │ └── index.ts │ ├── env.d.ts │ ├── icons │ │ ├── discord.svg │ │ └── github.svg │ ├── layouts │ │ ├── Blog.astro │ │ ├── Docs.astro │ │ ├── Layout.astro │ │ ├── Page.astro │ │ └── Redirect.astro │ ├── pages │ │ ├── 404.mdx │ │ ├── blog │ │ │ ├── [...slug].astro │ │ │ ├── index.astro │ │ │ └── tags │ │ │ │ └── [tag].astro │ │ ├── changelog.mdx │ │ ├── discord.mdx │ │ ├── docs │ │ │ ├── [...slug].astro │ │ │ └── index.astro │ │ ├── index.astro │ │ └── rss.xml.ts │ ├── plugins │ │ └── codeBlockDecorator.ts │ ├── sidebar.ts │ ├── style │ │ ├── _mixin.scss │ │ └── style.scss │ └── utils.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── generateTemplateCatalogue.ts ├── prerelease.ts └── tsconfig.json ├── templates ├── client │ ├── index.cjs │ ├── index.d.ts │ ├── index.mjs │ ├── package.json │ └── types │ │ └── index.d.ts ├── kit-blank │ ├── package.json │ ├── src │ │ ├── calls │ │ │ └── index.ts │ │ ├── env.d.ts │ │ └── index.ts │ └── tsconfig.json ├── server-blank │ ├── package.json │ ├── src │ │ ├── calls │ │ │ └── index.ts │ │ ├── env.d.ts │ │ └── index.ts │ └── tsconfig.json └── templates.json ├── tsconfig.base.json └── tsconfig.dev.json /.github/actions/setup-env/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Setup env" 2 | description: "Complete setup of node, pnpm and install dependencies" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/setup-node@v2 7 | with: 8 | node-version: "16" 9 | 10 | - uses: pnpm/action-setup@v2.2.4 11 | with: 12 | version: 8.6.12 13 | run_install: false 14 | 15 | - name: Cache pnpm modules 16 | uses: actions/cache@v3 17 | with: 18 | path: ~/.pnpm-store 19 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 20 | restore-keys: | 21 | ${{ runner.os }}-pnpm 22 | 23 | - name: Install dependencies 24 | run: |- 25 | pnpm config set store-dir ~/.pnpm-store 26 | pnpm install -r --frozen-lockfile 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yaml: -------------------------------------------------------------------------------- 1 | name: deploy-website 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - "packages/www/**" 8 | 9 | jobs: 10 | deploy: 11 | if: github.repository == 'giuseppelt/httpc' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup node & pnpm 17 | uses: ./.github/actions/setup-env 18 | 19 | - name: Build website 20 | run: pnpm run --filter @httpc/www build 21 | 22 | - name: Publish to Cloudflare Pages 23 | uses: cloudflare/pages-action@v1 24 | with: 25 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 26 | accountId: 78edf5165d1814cb77465c26ca14b1b2 27 | projectName: httpc-www 28 | directory: packages/www/dist 29 | branch: main 30 | gitHubToken: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/template-catalogue.yaml: -------------------------------------------------------------------------------- 1 | name: template-catalogue 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: ["templates/**/package.json"] 7 | 8 | jobs: 9 | update: 10 | if: github.repository == 'giuseppelt/httpc' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup node & pnpm 16 | uses: ./.github/actions/setup-env 17 | 18 | - name: Generate template catalogue 19 | run: pnpm run generate:template 20 | 21 | - name: Commit if changed 22 | run: |- 23 | git config user.name 'github-actions[bot]' 24 | git config user.email 'github-actions[bot]@users.noreply.github.com' 25 | git add templates/templates.json 26 | git commit -m "chore: updated template-catalogue" || exit 0 27 | git push 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #node 3 | node_modules/ 4 | .env 5 | .env.* 6 | .pnpm-debug.log 7 | .tsbuildinfo 8 | .build 9 | 10 | #local 11 | .expo/ 12 | .netlify/ 13 | .astro/ 14 | 15 | *.local 16 | 17 | # testing 18 | coverage/ 19 | 20 | # Gitignore 21 | *.swp 22 | *.*~ 23 | .DS_Store 24 | *.pyc 25 | nupkg/ 26 | 27 | # Rider 28 | .idea 29 | 30 | # User-specific files 31 | *.suo 32 | *.user 33 | *.userosscache 34 | *.sln.docstates 35 | 36 | # Build results 37 | dist/ 38 | dist-ssr/ 39 | [Dd]ebug/ 40 | [Dd]ebugPublic/ 41 | [Rr]elease/ 42 | [Rr]eleases/ 43 | x64/ 44 | x86/ 45 | build/ 46 | bld/ 47 | [Bb]in/ 48 | [Oo]bj/ 49 | [Oo]ut/ 50 | msbuild.log 51 | msbuild.err 52 | msbuild.wrn 53 | 54 | # Windows thumbnail cache files 55 | Thumbs.db 56 | Thumbs.db:encryptable 57 | 58 | # Dump file 59 | *.stackdump 60 | 61 | # Folder config file 62 | [Dd]esktop.ini 63 | 64 | # Recycle Bin used on file shares 65 | $RECYCLE.BIN/ 66 | 67 | # Windows shortcuts 68 | *.lnk 69 | 70 | # General 71 | .DS_Store 72 | .AppleDouble 73 | .LSOverride 74 | ._* 75 | .DocumentRevisions-V100 76 | .fseventsd 77 | .Spotlight-V100 78 | .TemporaryItems 79 | .Trashes 80 | .VolumeIcon.icns 81 | .com.apple.timemachine.donotpresent 82 | 83 | # Linux 84 | *~ 85 | .fuse_hidden* 86 | .directory 87 | .Trash-* 88 | .nfs* 89 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.DS_Store": true, 5 | "**/.vs": true, 6 | "**/dist": true, 7 | "**/build": true, 8 | "**/.build": true, 9 | "**/.docusaurus": true, 10 | "**/.next": true, 11 | "**/.vercel": true, 12 | "**/.expo": true, 13 | "**/.expo-shared": true, 14 | "**/.astro": true, 15 | }, 16 | "[typescriptreact]": { 17 | "editor.tabSize": 2 18 | }, 19 | "[html]": { 20 | "editor.tabSize": 2 21 | }, 22 | "[astro]": { 23 | "editor.tabSize": 2 24 | }, 25 | "typescript.tsdk": "node_modules\\typescript\\lib", 26 | "jest.jestCommandLine": "pnpm test --", 27 | "task.allowAutomaticTasks": "on" 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "tsc watch", 6 | "type": "npm", 7 | "script": "watch:ts", 8 | "group": "build", 9 | "isBackground": true, 10 | "problemMatcher": "$tsc-watch", 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "runOptions": { 15 | "runOn": "folderOpen" 16 | } 17 | }, 18 | { 19 | "label": "www dev", 20 | "type": "npm", 21 | "script": "dev", 22 | "group": "build", 23 | "path": "${workspaceFolder}/packages/www", 24 | "options": { 25 | "cwd": "${workspaceFolder}/packages/www" 26 | }, 27 | "isBackground": true, 28 | "presentation": { 29 | "reveal": "never" 30 | }, 31 | }, 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Upcoming 4 | - feat(kit): caching hooks 5 | - feat(kit): caching redis provider 6 | - feat(server): updated request processor with pre/post processors 7 | - feat(server): global error handler configurable 8 | - feat(server): updated server logging with errors trace points 9 | 10 | 11 | ## v0.1.0 First release 12 | This is the first release of the httpc framework. This release includes: 13 | - v0.1.0 **`@httpc/server`** -- The core httpc component, the server handles httpc calls 14 | - v0.1.0 **`@httpc/kit`** -- Toolbox with many builtin components addressing authentication, validation, caching... 15 | - v0.1.0 **`@httpc/client`** -- The client tailored to make httpc function calls with natural javascript 16 | - v0.1.0 **`@httpc/cli`** -- CLI to automate common tasks like client generation or testing 17 | - v0.1.0 **`create-httpc`** -- Create a new httpc project from a template 18 | 19 | ### Pre-release 20 | Adapters are released with a prerelease tag. 21 | - v0.0.1-pre **`@httpc/adapter-vercel`** -- Host on Vercel serverless function with or without Next integration 22 | - v0.0.1-pre **`@httpc/adapter-netlify`** -- Host on Netlify functions 23 | 24 | ### Documentation 25 | The home site with documentation is live at [httpc.dev](https://httpc.dev). 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Giuseppe La Torre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | **/api/client/ 2 | **/api/clients/ 3 | -------------------------------------------------------------------------------- /examples/movie-personal-library/api/.gitignore: -------------------------------------------------------------------------------- 1 | client/types/ 2 | -------------------------------------------------------------------------------- /examples/movie-personal-library/api/httpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-personal-library-api-client", 3 | "entry": "src/calls/index.ts", 4 | "dest": "client" 5 | } 6 | -------------------------------------------------------------------------------- /examples/movie-personal-library/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-personal-libary-api", 3 | "description": "Black template based on @httpc/server", 4 | "title": "Server blank", 5 | "templateType": "server", 6 | "version": "0.0.1", 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node dist/index.js", 11 | "dev": "ts-node-dev --transpile-only src/index.ts", 12 | "build": "tsc", 13 | "generate:client": "httpc client generate" 14 | }, 15 | "engines": { 16 | "node": ">=16.13" 17 | }, 18 | "dependencies": { 19 | "@httpc/server": "^0.0.1" 20 | }, 21 | "devDependencies": { 22 | "@httpc/cli": "^0.0.1", 23 | "@types/node": "^16.13", 24 | "ts-node-dev": "^2.0.0", 25 | "typescript": "^4.9.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/movie-personal-library/api/src/db.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | 3 | 4 | export type Movie = Readonly<{ 5 | id: number 6 | original_language: string 7 | overview: string 8 | release_date: string 9 | runtime: number 10 | tagline: string 11 | title: string 12 | genres: readonly string[] 13 | directors: readonly Person[] 14 | cast: readonly Person[] 15 | }> 16 | 17 | export type Person = Readonly<{ 18 | id: number 19 | name: string 20 | }> 21 | 22 | 23 | 24 | const DATABASE_FILE = "data/movie-db.json"; 25 | 26 | let movies: Movie[] = []; 27 | 28 | async function load(): Promise { 29 | movies = JSON.parse(await fs.readFile(DATABASE_FILE, "utf8")); 30 | return movies; 31 | } 32 | 33 | async function save() { 34 | await fs.writeFile(DATABASE_FILE, JSON.stringify(movies), "utf8"); 35 | } 36 | 37 | 38 | export default { 39 | load, 40 | save, 41 | } 42 | -------------------------------------------------------------------------------- /examples/movie-personal-library/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createHttpCServer } from "@httpc/server"; 2 | import calls from "./calls"; 3 | 4 | 5 | const PORT = Number(process.env.PORT) || 3000; 6 | 7 | const server = createHttpCServer({ 8 | calls 9 | }); 10 | 11 | 12 | server.listen(PORT); 13 | console.log("Server started: http//localhost:%d", PORT); 14 | -------------------------------------------------------------------------------- /examples/movie-personal-library/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "target": "es2022", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/movie-personal-library/console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-personal-library-console", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "dev": "ts-node src/index.ts" 7 | }, 8 | "devDependencies": { 9 | "@types/node": "^16.13", 10 | "@types/prompts": "^2.4.1", 11 | "ts-node": "^10.9.1", 12 | "typescript": "^4.9.3" 13 | }, 14 | "dependencies": { 15 | "cross-fetch": "^3.1.5", 16 | "kleur": "^4.1.5", 17 | "movie-personal-library-api-client": "workspace:*", 18 | "prompts": "^2.4.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/movie-personal-library/console/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "target": "es2022", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/movie-personal-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-personal-library-monorepo", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "pnpm": { 7 | "overrides": { 8 | "@httpc/cli": "link:..\\..\\packages\\cli", 9 | "@httpc/client": "link:..\\..\\packages\\client", 10 | "@httpc/server": "link:..\\..\\packages\\server" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/movie-personal-library/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - api 3 | - api/client 4 | - console 5 | -------------------------------------------------------------------------------- /examples/netlify-functions/README.md: -------------------------------------------------------------------------------- 1 | # httpc example: Netlify functions 2 | Checkout documentation on the [httpc website](https://httpc.dev) 3 | -------------------------------------------------------------------------------- /examples/netlify-functions/api.http: -------------------------------------------------------------------------------- 1 | @host = http://localhost:8888 2 | 3 | ### 4 | GET {{host}}/api/add?$p=[1,3] 5 | 6 | ### 7 | POST {{host}}/api/greet 8 | content-type: application/json 9 | 10 | "walter" 11 | 12 | -------------------------------------------------------------------------------- /examples/netlify-functions/netlify.toml: -------------------------------------------------------------------------------- 1 | [dev] 2 | autoLaunch = false 3 | publish = "dist" 4 | 5 | [build] 6 | command = "npm run build" 7 | publish = "dist" 8 | 9 | [functions] 10 | directory = "dist/functions/" 11 | -------------------------------------------------------------------------------- /examples/netlify-functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "httpc-example-netlify-functions", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "scripts": { 7 | "dev": "netlify dev --dir ./dist", 8 | "build": "tsc" 9 | }, 10 | "devDependencies": { 11 | "@types/node": "~16", 12 | "netlify-cli": "^16.5.1", 13 | "typescript": "^4.9.4" 14 | }, 15 | "dependencies": { 16 | "@httpc/adapter-netlify": "^0.0.1", 17 | "@httpc/client": "^0.0.1", 18 | "@httpc/server": "^0.0.1" 19 | }, 20 | "pnpm": { 21 | "overrides": { 22 | "@httpc/cli": "link:..\\..\\packages\\cli", 23 | "@httpc/client": "link:..\\..\\packages\\client", 24 | "@httpc/server": "link:..\\..\\packages\\server", 25 | "@httpc/adapter-netlify": "link:..\\..\\packages\\adapter-netlify" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/netlify-functions/pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/examples/netlify-functions/pnpm-workspace.yaml -------------------------------------------------------------------------------- /examples/netlify-functions/public/_redirects: -------------------------------------------------------------------------------- 1 | /api/* /.netlify/functions/api?$c=:splat 200 2 | -------------------------------------------------------------------------------- /examples/netlify-functions/src/calls/index.ts: -------------------------------------------------------------------------------- 1 | 2 | function add(x: number, y: number) { 3 | return x + y; 4 | } 5 | 6 | function greet(name: string) { 7 | return `Hello ${name || "Anonymous"}`; 8 | } 9 | 10 | 11 | export default { 12 | add, 13 | greet 14 | } 15 | -------------------------------------------------------------------------------- /examples/netlify-functions/src/functions/api.ts: -------------------------------------------------------------------------------- 1 | import { createHttpCNetlifyHandler } from "@httpc/adapter-netlify"; 2 | import calls from "../calls"; 3 | 4 | 5 | export const handler = createHttpCNetlifyHandler({ 6 | path: "api", 7 | cors: true, 8 | calls 9 | }); 10 | -------------------------------------------------------------------------------- /examples/netlify-functions/src/page.ts: -------------------------------------------------------------------------------- 1 | import { createClient, ClientDef } from "@httpc/client"; 2 | import calls from "./calls"; 3 | 4 | 5 | async function page() { 6 | const client = createClient>({ endpoint: "/api" }); 7 | await client.greet("Mark"); 8 | } 9 | -------------------------------------------------------------------------------- /examples/netlify-functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "target": "es2022", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/vercel-next/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/vercel-next/README.md: -------------------------------------------------------------------------------- 1 | # httpc example: Next on Vercel 2 | Checkout documentation on the [httpc website](https://httpc.dev) 3 | -------------------------------------------------------------------------------- /examples/vercel-next/components/InvokeAddFunction.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from '../styles/Home.module.css' 3 | import { inter } from '../styles/font'; 4 | import client from './client'; 5 | 6 | 7 | export function InvokeAddFunction() { 8 | const [x, setX] = useState(0); 9 | const [y, setY] = useState(0); 10 | 11 | const [result, setResult] = useState(); 12 | 13 | const invokeAdd = async () => { 14 | const result = await client.add(x, y); 15 | setResult(result); 16 | }; 17 | 18 | 19 | return ( 20 | 25 |

26 | Call add function -> 27 |

28 |

29 | Sample addition made via API 30 |

31 | 32 |
33 |
34 | 35 | setX(Number(ev.target.value))} /> 36 |
37 | + 38 |
39 | 40 | setY(Number(ev.target.value))} /> 41 |
42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 | {result} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /examples/vercel-next/components/InvokeGreetFunction.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from '../styles/Home.module.css' 3 | import { inter } from '../styles/font'; 4 | import client from './client'; 5 | 6 | 7 | export function InvokeGreetFunction() { 8 | const [name, setName] = useState(""); 9 | const [result, setResult] = useState(""); 10 | 11 | const invokeGreet = async () => { 12 | const result = await client.greet(name); 13 | setResult(result); 14 | }; 15 | 16 | 17 | return ( 18 | 23 |

24 | Call greet function -> 25 |

26 |

27 | Greeting message from the API 28 |

29 | 30 |
31 |
32 | 33 | setName(ev.target.value)} /> 34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 | {result} 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/vercel-next/components/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient, ClientDef } from "@httpc/client"; 2 | import type calls from "../pages/api/_calls"; 3 | 4 | 5 | const client = createClient>({ 6 | endpoint: "/api" 7 | }); 8 | 9 | export default client; 10 | -------------------------------------------------------------------------------- /examples/vercel-next/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /examples/vercel-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "httpc-example-vercel-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@httpc/adapter-vercel": "^0.0.1", 13 | "@httpc/client": "^0.0.1", 14 | "@httpc/server": "^0.0.1", 15 | "@next/font": "13.1.1", 16 | "@types/node": "^16.18.10", 17 | "@types/react": "18.0.26", 18 | "@types/react-dom": "18.0.10", 19 | "next": "13.1.1", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "typescript": "^4.9.4" 23 | }, 24 | "pnpm": { 25 | "overrides": { 26 | "@httpc/cli": "link:..\\..\\packages\\cli", 27 | "@httpc/client": "link:..\\..\\packages\\client", 28 | "@httpc/server": "link:..\\..\\packages\\server", 29 | "@httpc/adapter-vercel": "link:..\\..\\packages\\adapter-vercel" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/vercel-next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /examples/vercel-next/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/vercel-next/pages/api/[[...slug]].ts: -------------------------------------------------------------------------------- 1 | import { createHttpCVercelAdapter } from "@httpc/adapter-vercel"; 2 | import calls from "./_calls"; 3 | 4 | 5 | export default createHttpCVercelAdapter({ 6 | calls, 7 | log: true, 8 | }); 9 | 10 | 11 | export const config = { 12 | api: { 13 | bodyParser: false, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/vercel-next/pages/api/_calls.ts: -------------------------------------------------------------------------------- 1 | 2 | function add(a: number, b: number) { 3 | return a + b; 4 | } 5 | 6 | function greet(name: string) { 7 | return `Hello ${name || "Anonymous"}`; 8 | } 9 | 10 | export default { 11 | add, 12 | greet, 13 | } 14 | -------------------------------------------------------------------------------- /examples/vercel-next/pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/examples/vercel-next/pnpm-workspace.yaml -------------------------------------------------------------------------------- /examples/vercel-next/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/examples/vercel-next/public/favicon.ico -------------------------------------------------------------------------------- /examples/vercel-next/public/httpc-brand.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /examples/vercel-next/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vercel-next/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vercel-next/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vercel-next/styles/font.ts: -------------------------------------------------------------------------------- 1 | import { Inter } from '@next/font/google' 2 | 3 | export const inter = Inter({ subsets: ['latin'] }); 4 | -------------------------------------------------------------------------------- /examples/vercel-next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@httpc/monorepo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "generate:template": "ts-node -T scripts/generateTemplateCatalogue", 8 | "prerelease": "ts-node -T scripts/prerelease", 9 | "build": "pnpm run -r --filter=./packages/* --filter=!@httpc/www build", 10 | "publish": "pnpm publish --filter=./packages/* --filter=!@httpc/www", 11 | "test": "pnpm run -r test", 12 | "watch:ts": "tsc --build tsconfig.dev.json --watch --incremental --force" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^16.18.23", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^5.4.5" 18 | }, 19 | "engines": { 20 | "node": ">=16.13.0", 21 | "pnpm": ">=7" 22 | }, 23 | "pnpm": { 24 | "peerDependencyRules": { 25 | "ignoreMissing": [ 26 | "rollup" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/adapter-cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/adapter-cloudflare 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/adapter-cloudflare/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare global { 4 | interface IWorkerEnv { 5 | 6 | } 7 | } 8 | 9 | interface IWorkerEnv { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /packages/adapter-cloudflare/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testMatch": [ 4 | "**/*.test.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/adapter-cloudflare/src/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerOptions, createHttpCServer } from "@httpc/server"; 2 | 3 | 4 | export type CloudflareWorkerOptions = Pick & { 16 | kit?: boolean 17 | } 18 | 19 | export function createCloudflareWorker(options: CloudflareWorkerOptions): { fetch: ExportedHandlerFetchHandler } { 20 | 21 | const httpc = createHttpCServer({ 22 | ...options, 23 | }); 24 | 25 | return { 26 | fetch(request, env, ctx) { 27 | return httpc.fetch(request, env); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/adapter-cloudflare/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "dist", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src", 13 | "env.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/adapter-cloudflare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": [], 5 | "types": [ 6 | "@cloudflare/workers-types" 7 | ], 8 | "sourceMap": true 9 | }, 10 | "include": [ 11 | "env.d.ts", 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/adapter-netlify/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/adapter-netlify 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/adapter-netlify/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testMatch": [ 4 | "**/*.test.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/adapter-netlify/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/adapter-netlify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | }, 6 | "include": [ 7 | "src", 8 | "tests" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/adapter-vercel/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/adapter-vercel 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/adapter-vercel/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testMatch": [ 4 | "**/*.test.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/adapter-vercel/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from "node:http"; 2 | import { HttpCServerHandler, HttpCServerOptions, createHttpCServerHandler } from "@httpc/server"; 3 | import { createRequestFromNode, writeResponseToNode } from "@httpc/server/node"; 4 | 5 | 6 | export type HttpCVercelAdapterOptions = Pick & { 17 | kit?: boolean 18 | refresh?: boolean 19 | } 20 | 21 | 22 | let server: HttpCServerHandler | undefined; 23 | 24 | //TODO: add a @httpc/kit solution 25 | 26 | export function createHttpCVercelAdapter(options: HttpCVercelAdapterOptions) { 27 | const handler = (!server || options.refresh) 28 | ? (server = createHttpCServerHandler({ path: "api", ...options })) 29 | : server; 30 | 31 | return async (req: IncomingMessage, res: ServerResponse) => { 32 | try { 33 | const request = createRequestFromNode(req); 34 | const response = await handler(request); 35 | await writeResponseToNode(res, response); 36 | } catch (err) { 37 | console.error(err); 38 | 39 | if (!res.headersSent) { 40 | res.writeHead(500); 41 | } 42 | 43 | res.end(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/adapter-vercel/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2020", 5 | "outDir": "dist", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/adapter-vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "ES2020" 6 | ], 7 | "sourceMap": true 8 | }, 9 | "include": [ 10 | "src", 11 | "tests" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/cli 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/cli/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ClientCommand } from "./client"; 2 | export { default as CreateCommand } from "./create"; 3 | export * from "./call"; 4 | -------------------------------------------------------------------------------- /packages/cli/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "cross-fetch/polyfill"; 3 | import { program } from "commander"; 4 | import { VERSION } from "./version"; 5 | import { ClientCommand, CreateCommand, CallCommand } from "./commands"; 6 | 7 | 8 | program.name("httpc") 9 | .version(VERSION, "-v, --version") 10 | .configureHelp({ 11 | subcommandTerm: cmd => cmd.name() 12 | }) 13 | .addCommand(ClientCommand) 14 | .addCommand(CreateCommand) 15 | .addCommand(CallCommand) 16 | ; 17 | 18 | program.parse(); 19 | -------------------------------------------------------------------------------- /packages/cli/src/utils/fsUtils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | 3 | 4 | export async function exists(path: string): Promise { 5 | return await fs.access(path).then( 6 | () => true, 7 | () => false 8 | ); 9 | } 10 | 11 | export async function isDirEmpty(dir: string): Promise { 12 | return !await exists(dir) || (await fs.readdir(dir)).length === 0; 13 | } 14 | 15 | export async function clearDir(dir: string): Promise { 16 | await fs.rm(dir, { recursive: true, force: true }).catch(() => { }); 17 | await fs.mkdir(dir, { recursive: true }); 18 | } 19 | 20 | export async function createDir(dir: string): Promise { 21 | await fs.mkdir(dir, { recursive: true }); 22 | } 23 | 24 | export async function copyDir(source: string, dest: string, exclude?: string[]): Promise { 25 | if (!await exists(dest)) { 26 | await fs.mkdir(dest, { recursive: true }); 27 | } 28 | 29 | await fs.cp(source, dest, { 30 | recursive: true, 31 | filter: exclude && exclude.length > 0 && ((source) => { 32 | return !exclude.some(x => !!source.match(x)); 33 | }) || undefined 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * as log from "./log"; 2 | export * as fsUtils from "./fsUtils"; 3 | export * as packageUtils from "./packageUtils"; 4 | export * as templateUtils from "./template"; 5 | export * from "./process"; 6 | -------------------------------------------------------------------------------- /packages/cli/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import kleur from "kleur"; 2 | 3 | 4 | export function warn(message: string, ...args: any[]) { 5 | console.log(kleur.yellow(`[!] ${message}`), ...args); 6 | } 7 | 8 | export function success(message: string, ...args: any[]) { 9 | console.log(kleur.cyan(`✔️ ${message}`), ...args); 10 | } 11 | 12 | export function done(message: string, ...args: any[]) { 13 | console.log(kleur.cyan(`✔️ ${message}`), ...args); 14 | } 15 | 16 | export function verbose(message: string, ...args: any[]) { 17 | console.log(kleur.gray(`${message}`), ...args); 18 | } 19 | 20 | export function minor(message: string, ...args: any[]) { 21 | console.log(kleur.gray(`${message}`), ...args); 22 | } 23 | 24 | export function error(message: string, ...args: any[]) { 25 | console.log(kleur.red(`❌ ${message}`), ...args); 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/src/utils/packageUtils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { fsUtils } from "."; 4 | 5 | 6 | 7 | export async function read(cwd?: string): Promise { 8 | cwd = path.resolve(cwd || "."); 9 | 10 | const packageFile = path.resolve(cwd, "package.json"); 11 | if (!fsUtils.exists(packageFile)) { 12 | throw new Error("package.json not found in the current path"); 13 | } 14 | 15 | return JSON.parse(await fs.readFile(packageFile, "utf-8")); 16 | } 17 | 18 | export async function patch(dir: string, patch: object | ((json: object) => object)): Promise { 19 | const packageContent = await read(dir); 20 | if (typeof patch === "function") { 21 | patch = patch(packageContent); 22 | } 23 | if (patch) { 24 | Object.assign(packageContent, patch); 25 | } 26 | 27 | await fs.writeFile(path.resolve(dir, "package.json"), JSON.stringify(packageContent, null, 2), "utf-8"); 28 | 29 | return packageContent; 30 | } 31 | -------------------------------------------------------------------------------- /packages/cli/src/utils/process.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | 3 | 4 | export async function run(cmd: string, option?: { 5 | cwd?: string 6 | showOutput?: boolean 7 | }) { 8 | return await new Promise((resolve, reject) => { 9 | exec(cmd, { ...option }, (error, stdout, stderr) => { 10 | if (error) { 11 | console.error(error.stack); 12 | console.log("\nError code: " + error.code); 13 | console.log(stdout); 14 | reject(error); 15 | return; 16 | } 17 | 18 | if (option?.showOutput && stdout) { 19 | console.log(stdout); 20 | } 21 | 22 | resolve(); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/version.ts: -------------------------------------------------------------------------------- 1 | 2 | export const VERSION = "0.0.0"; 3 | -------------------------------------------------------------------------------- /packages/cli/tasks/export-version.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { version } = require("../package.json"); 3 | 4 | const versionFile = "./dist/version.js"; 5 | 6 | let content = fs.readFileSync(versionFile, "utf-8"); 7 | content = content.replace(/VERSION = ".+"/g, `VERSION = "${version}"`); 8 | fs.writeFileSync(versionFile, content, "utf-8"); 9 | 10 | console.log("Exported version: %s", version); -------------------------------------------------------------------------------- /packages/cli/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "src", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": false, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "module": "commonjs", 6 | "noEmit": false, 7 | "lib": [ 8 | "ESNext" 9 | ] 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/client 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@httpc/client", 3 | "description": "httpc client for building function-based API with minimal code and end-to-end type safety", 4 | "version": "0.2.0-pre20230831141721", 5 | "author": { 6 | "name": "Giuseppe La Torre", 7 | "url": "https://github.com/giuseppelt" 8 | }, 9 | "license": "MIT", 10 | "module": "./dist/esm/index.js", 11 | "main": "./dist/cjs/index.js", 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "rimraf dist && tsc -p tsconfig.build.json --module commonjs --outDir ./dist/cjs && tsc -p tsconfig.build.json --module es2020 --outDir ./dist/esm" 15 | }, 16 | "files": [ 17 | "dist", 18 | "README.md", 19 | "package.json" 20 | ], 21 | "homepage": "https://httpc.dev", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/giuseppelt/httpc.git", 25 | "directory": "packages/client" 26 | }, 27 | "bugs": "https://github.com/giuseppelt/httpc/issues", 28 | "keywords": [ 29 | "httpc", 30 | "api", 31 | "rpc", 32 | "rpc-api", 33 | "json", 34 | "json-api", 35 | "typescript", 36 | "javascript", 37 | "client-generation" 38 | ], 39 | "devDependencies": { 40 | "@types/node": "^16.18.23", 41 | "@types/node-fetch": "^2.6.3", 42 | "rimraf": "^5.0.0", 43 | "typescript": "^5.4.5" 44 | } 45 | } -------------------------------------------------------------------------------- /packages/client/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import type * as nf from "node-fetch"; 2 | import type { default as Fetch } from "node-fetch"; 3 | 4 | 5 | let fetch: typeof Fetch = undefined!; 6 | let Headers: typeof nf.Headers = undefined!; 7 | let Request: typeof nf.Request = undefined!; 8 | let Response: typeof nf.Response = undefined!; 9 | 10 | 11 | (function (global: any) { 12 | ({ 13 | fetch, 14 | Headers, 15 | Request, 16 | Response, 17 | } = global || {}); 18 | 19 | //@ts-ignore 20 | }(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this)); 21 | 22 | 23 | if (!fetch) { 24 | throw new Error("Missing fetch API. Be sure fetch is available from the global object or polyfill it (es: with 'cross-fetch' module)"); 25 | } 26 | 27 | 28 | export { 29 | fetch, 30 | Headers, 31 | Request, 32 | Response, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./client"; 3 | export * from "./typed"; 4 | export * from "./factory"; 5 | export * from "./middlewares"; 6 | -------------------------------------------------------------------------------- /packages/client/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { HttpCClientMiddleware } from "./client"; 2 | 3 | 4 | export function Header(header: string, value: string | (() => (string | undefined))): HttpCClientMiddleware { 5 | return (request, next) => { 6 | const val = typeof value === "function" ? value() : value; 7 | if (val !== undefined && val !== null) { 8 | request.headers.set(header, val); 9 | } 10 | 11 | return next(request); 12 | }; 13 | } 14 | 15 | export function AuthHeader(schema: string, value: string | (() => string | undefined)): HttpCClientMiddleware { 16 | return Header("Authorization", () => { 17 | const val = typeof value === "function" ? value() : value; 18 | return val !== undefined && val !== null && val !== "" ? `${schema} ${val}` : undefined; 19 | }); 20 | } 21 | 22 | export function QueryParam(key: string, value: string | number | (() => string | number | undefined)): HttpCClientMiddleware { 23 | return (request, next) => { 24 | const val = typeof value === "function" ? value() : value; 25 | if (val !== undefined && val !== null) { 26 | request.query.set(key, val.toString()); 27 | } 28 | 29 | return next(request); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/client/src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type ClientDef = T extends Record 3 | ? { [k in keyof T]: ClientCall } 4 | : never 5 | 6 | type ClientCall = T extends (...args: any[]) => any 7 | ? (...args: CallParams>) => Promise>>> 8 | : T extends CallPipeline 9 | ? (...args: CallParams>) => Promise>>> 10 | : T extends Record 11 | ? ClientDef 12 | : never 13 | 14 | type CallParams

= P; 15 | 16 | type CallPipeline any> = { 17 | $type?: T 18 | access: string 19 | execute: (...args: any) => any 20 | } 21 | 22 | export type JsonSafeType = 23 | T extends void ? undefined : 24 | T extends Function ? never : 25 | T extends Promise ? never : 26 | T extends number | string | boolean | undefined | null ? T : 27 | T extends Date | BigInt ? string : 28 | T extends [infer H, ...infer R] ? ( 29 | H extends never ? [] : 30 | R extends never[] ? [H] : 31 | [JsonSafeType, ...JsonSafeType]) : 32 | T extends Array ? JsonSafeType[] : 33 | T extends Record ? { [k in keyof T]: JsonSafeType } : 34 | never 35 | -------------------------------------------------------------------------------- /packages/client/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { HttpCallDefinition, HttpCClientMetadata } from "./typed"; 2 | 3 | export function isCall(value: HttpCClientMetadata[string]): value is HttpCallDefinition { 4 | return value && typeof value.access === "string"; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /packages/client/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "rootDir": "src", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/client/tsconfig.dev.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "rootDir": "src", 6 | "module": "commonjs", 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "composite": true, 10 | "tsBuildInfoFile": "./dist/.tsBuildInfoFile.cjs" 11 | } 12 | } -------------------------------------------------------------------------------- /packages/client/tsconfig.dev.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "rootDir": "src", 6 | "module": "ES2020", 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "composite": true, 10 | "tsBuildInfoFile": "./dist/.tsBuildInfoFile.esm" 11 | } 12 | } -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "src", 6 | "module": "ES2020", 7 | "noEmit": false, 8 | "declaration": true, 9 | }, 10 | "include": [ 11 | "src", 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/create/README.md: -------------------------------------------------------------------------------- 1 | # create-httpc 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/create/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { spawn } from "child_process"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | // forward to the @httpc/cli create command 7 | const args = process.argv.slice(2); 8 | 9 | spawn("npx", ["httpc", "create", ...args], { 10 | stdio: "inherit", 11 | shell: true, 12 | cwd: path.dirname(fileURLToPath(import.meta.url)) // path local to the package 13 | }); 14 | -------------------------------------------------------------------------------- /packages/create/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-httpc", 3 | "description": "create httpc API from a template", 4 | "version": "0.1.0", 5 | "author": { 6 | "name": "Giuseppe La Torre", 7 | "url": "https://github.com/giuseppelt" 8 | }, 9 | "license": "MIT", 10 | "type": "module", 11 | "exports": { 12 | ".": "./index.mjs" 13 | }, 14 | "scripts": { 15 | "start": "node index.mjs" 16 | }, 17 | "bin": { 18 | "create": "./index.mjs" 19 | }, 20 | "homepage": "https://httpc.dev", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/giuseppelt/httpc.git", 24 | "directory": "packages/create" 25 | }, 26 | "bugs": "https://github.com/giuseppelt/httpc/issues", 27 | "keywords": [ 28 | "httpc", 29 | "api", 30 | "rpc", 31 | "rpc-api", 32 | "json", 33 | "json-api", 34 | "typescript", 35 | "javascript", 36 | "client-generation" 37 | ], 38 | "dependencies": { 39 | "@httpc/cli": "workspace:*" 40 | }, 41 | "engines": { 42 | "node": ">=16.13" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/kit/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/kit 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/kit/caching-lru.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/caching/lru"; 2 | -------------------------------------------------------------------------------- /packages/kit/caching-redis.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/caching/redis"; 2 | -------------------------------------------------------------------------------- /packages/kit/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/kit/geo-maxmind.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/geo/maxmind"; 2 | -------------------------------------------------------------------------------- /packages/kit/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testMatch": [ 4 | "**/*.test.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/kit/logging-winston.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/logging/winston"; 2 | -------------------------------------------------------------------------------- /packages/kit/node.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/node"; 2 | -------------------------------------------------------------------------------- /packages/kit/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./context"; 3 | export * from "./JwtService"; 4 | export * from "./BearerAuthenticationService"; 5 | export * from "./AuthenticationBearerMiddleware"; 6 | export * from "./AuthenticationApiKeyMiddleware"; 7 | export * from "./BasicAuthenticationMiddleware"; 8 | export * from "./AuthorizationMiddleware"; 9 | export * from "./PermissionsAuthorizationService"; 10 | export * from "./pipeline"; 11 | -------------------------------------------------------------------------------- /packages/kit/src/auth/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { HttpCall, HttpCServerMiddleware, UnauthorizedError, useContext } from "@httpc/server"; 2 | import { useAuthorize } from "./context"; 3 | 4 | 5 | export function Authenticated(permissions?: string | ((...args: any[]) => string)): HttpCServerMiddleware { 6 | return (call, next) => { 7 | const { user } = useContext(); 8 | if (!user) { 9 | throw new UnauthorizedError(); 10 | } 11 | 12 | if (permissions) { 13 | checkAuthorization(call, permissions); 14 | } 15 | 16 | return next(call); 17 | }; 18 | } 19 | 20 | export function Authorized(permissions: string | ((...args: any[]) => string)): HttpCServerMiddleware { 21 | return (call, next) => { 22 | checkAuthorization(call, permissions); 23 | return next(call); 24 | } 25 | } 26 | 27 | function checkAuthorization(call: HttpCall, permissions: string | ((...args: any[]) => string)) { 28 | if (typeof permissions === "function") { 29 | permissions = permissions(...call.params); 30 | } 31 | 32 | useAuthorize(permissions); 33 | } 34 | -------------------------------------------------------------------------------- /packages/kit/src/auth/types.ts: -------------------------------------------------------------------------------- 1 | import { Assertion, Authorization } from "../permissions"; 2 | 3 | 4 | export interface IAuthenticationService { 5 | authenticate(arg: T): Promise 6 | } 7 | 8 | export interface IAuthorizationService { 9 | authorize(user: IUser): Promise 10 | createAuthorization(authorization: string | Auth): Auth 11 | assert(authorization: Auth, assertion: Assert): void 12 | check(authorization: Auth, assertion: Assert): boolean 13 | } 14 | -------------------------------------------------------------------------------- /packages/kit/src/caching/InMemoryCache.ts: -------------------------------------------------------------------------------- 1 | import { ICacheSync } from "./types"; 2 | 3 | 4 | export type InMemoryCacheOptions = { 5 | 6 | } 7 | 8 | export class InMemoryCache extends Map implements ICacheSync { 9 | constructor(options?: InMemoryCacheOptions) { 10 | super(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/kit/src/caching/LogCacheDecorator.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from "../logging"; 2 | import { ICache, ICacheSync } from "./types"; 3 | import { createProxy, isPromise } from "../utils"; 4 | 5 | 6 | export function LogCacheDecorator(name: string, logger: ILogger, cache: T): T { 7 | return createProxy(cache as ICacheSync, { 8 | get(key: string) { 9 | function log(value: any) { 10 | logger.debug("Cache(%s) %s: %s", name, key, value === undefined ? "miss" : "hit"); 11 | return value; 12 | }; 13 | 14 | const value = cache.get(key); 15 | return isPromise(value) 16 | ? value.then(log) 17 | : log(value); 18 | }, 19 | set(key: string, value: any) { 20 | logger.debug("Cache(%s) %s: set", name, key); 21 | return cache.set(key, value); 22 | }, 23 | delete(key: string) { 24 | logger.debug("Cache(%s) %s: deleted", name, key); 25 | return cache.delete(key); 26 | }, 27 | clear() { 28 | logger.debug("Cache(%s) cleared", name); 29 | return cache.clear(); 30 | } 31 | }) as T; 32 | } 33 | -------------------------------------------------------------------------------- /packages/kit/src/caching/WaterfallCache.ts: -------------------------------------------------------------------------------- 1 | import { ICache, ICacheSync } from "./types"; 2 | 3 | 4 | export type WaterfallCacheOptions = { 5 | providers: (ICache | ICacheSync)[] 6 | } 7 | 8 | export class WaterfallCache implements ICache { 9 | readonly providers: ICache[]; 10 | 11 | constructor(options: WaterfallCache) { 12 | this.providers = options.providers; 13 | } 14 | 15 | keys() { 16 | return this.providers[0].keys(); 17 | } 18 | 19 | async has(key: string): Promise { 20 | for (const p of this.providers) { 21 | if (await p.has(key)) { 22 | return true; 23 | } 24 | } 25 | 26 | return false; 27 | } 28 | 29 | async get(key: string): Promise { 30 | for (const p of this.providers) { 31 | const value = await p.get(key); 32 | if (value !== null && value !== undefined) { 33 | return value; 34 | } 35 | } 36 | } 37 | 38 | async set(key: string, value: T): Promise { 39 | for (const p of this.providers) { 40 | await p.set(key, value); 41 | } 42 | } 43 | 44 | async delete(key: string): Promise { 45 | for (const p of this.providers) { 46 | await p.delete(key); 47 | } 48 | } 49 | 50 | async clear(): Promise { 51 | for (const p of this.providers) { 52 | await p.clear(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/kit/src/caching/decorators.ts: -------------------------------------------------------------------------------- 1 | 2 | import { injectWithTransform } from "tsyringe"; 3 | import { KEY } from "../di"; 4 | import { assert } from "../internal"; 5 | import type { CacheKey, ICachingService } from "./types"; 6 | 7 | 8 | export function cache(key: CacheKey, options?: any): ParameterDecorator; 9 | export function cache(): ParameterDecorator; 10 | export function cache(key?: CacheKey, options?: any): ParameterDecorator { 11 | return injectWithTransform(KEY("ICachingService"), GetCacheTransform, key, options); 12 | } 13 | 14 | class GetCacheTransform { 15 | transform(service: ICachingService, key?: string, options?: any) { 16 | const cache = key 17 | ? service.getCache(key, options) 18 | : service.getDefaultCache(); 19 | 20 | assert(cache, key ? `No cache for key '${key}' found to be injected` : "No default cache found to be injected"); 21 | 22 | return cache; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/kit/src/caching/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./InMemoryCache"; 3 | export * from "./LogCacheDecorator"; 4 | export * from "./PrefixCacheDecorator"; 5 | export * from "./WaterfallCache"; 6 | export * from "./CachingService"; 7 | export * from "./context"; 8 | export * from "./decorators"; 9 | export * from "./CacheMiddleware"; 10 | -------------------------------------------------------------------------------- /packages/kit/src/caching/lru/index.ts: -------------------------------------------------------------------------------- 1 | import { CacheSetOptions, ICacheSync } from "../types"; 2 | import { LRUCache as LRU } from "lru-cache"; 3 | 4 | /** 5 | * Configure the LruCache 6 | */ 7 | export type LruCacheOptions = { 8 | /** 9 | * Set the max count of items the cache can hold 10 | * @default 100 11 | */ 12 | size?: number 13 | 14 | /** 15 | * Set the expiration of items in milliseconds, use 0 to have no expiration 16 | * @default 0 17 | */ 18 | ttl?: number 19 | } 20 | 21 | export class LruCache implements ICacheSync { 22 | 23 | constructor(options?: LruCacheOptions) { 24 | const { 25 | size = 100, 26 | ttl = 0 27 | } = options || {}; 28 | 29 | this.provider = new LRU({ 30 | max: size, 31 | ttl, 32 | }); 33 | } 34 | 35 | readonly provider: InstanceType>; 36 | 37 | keys() { 38 | return this.provider.keys(); 39 | } 40 | 41 | has(key: string): boolean { 42 | return this.provider.has(key); 43 | } 44 | 45 | get(key: string): T | undefined { 46 | return this.provider.get(key); 47 | } 48 | 49 | set(key: string, value: T, options?: CacheSetOptions): void { 50 | this.provider.set(key, value, options && { 51 | ttl: options.ttl 52 | }); 53 | } 54 | 55 | delete(key: string): void { 56 | this.provider.delete(key); 57 | } 58 | 59 | clear(): void { 60 | this.provider.clear(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/kit/src/caching/types.ts: -------------------------------------------------------------------------------- 1 | import { ExpandedKeys } from "../env" 2 | 3 | 4 | export type CachingGetOptions = { 5 | prefix?: string 6 | [key: string]: any 7 | } 8 | 9 | export interface ICachingService { 10 | getDefaultCache(): ICache 11 | getCache(key: K, options?: CachingGetOptions): CacheType 12 | register(key: K, factory: () => (CacheType | ICache | ICacheSync)): void 13 | } 14 | 15 | 16 | export type CacheSetOptions = { 17 | /** 18 | * Specify the expiration in milliseconds 19 | * NB: not all providers support this options 20 | */ 21 | ttl?: number 22 | } 23 | 24 | export interface ICacheSync { 25 | keys(): IterableIterator 26 | has(key: string): boolean 27 | get(key: string): T | undefined 28 | set(key: string, value: T, options?: CacheSetOptions): void 29 | delete(key: string): void 30 | clear(): void 31 | } 32 | 33 | export interface ICache { 34 | keys(): AsyncIterableIterator 35 | has(key: string): Promise 36 | get(key: string): Promise 37 | set(key: string, value: T, options?: CacheSetOptions): Promise 38 | delete(key: string): Promise 39 | clear(): Promise 40 | } 41 | 42 | 43 | export type CacheKey = ExpandedKeys 44 | export type CacheType = CacheTypes extends { [k in K]: (ICache | ICacheSync) } ? CacheTypes[K] : ICache 45 | 46 | export type CacheItemKey = ExpandedKeys 47 | export type CacheItemType = CacheItemTypes extends { [k in K]: any } ? CacheItemTypes[K] | undefined : any 48 | -------------------------------------------------------------------------------- /packages/kit/src/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./decorators"; 2 | export * from "./httpController"; -------------------------------------------------------------------------------- /packages/kit/src/di/container.ts: -------------------------------------------------------------------------------- 1 | import { container as globalContainer, DependencyContainer } from "tsyringe"; 2 | import { ConsoleLogService, ILogger } from "../logging"; 3 | import { IInitialize } from "../services"; 4 | import { KEY, RESOLVE, RESOLVE_ALL } from "./keys"; 5 | 6 | 7 | export async function initializeContainer(container: DependencyContainer = globalContainer) { 8 | if (!container.isRegistered(KEY("ILogService"))) { 9 | container.register(KEY("ILogService"), ConsoleLogService); 10 | } 11 | 12 | if (!container.isRegistered(KEY("ApplicationLogger"))) { 13 | const logger = RESOLVE(container, "ILogService").createLogger("Application"); 14 | container.registerInstance(KEY("ApplicationLogger"), logger); 15 | } 16 | 17 | if (container.isRegistered(KEY("IInitialize"))) { 18 | const initializers = RESOLVE_ALL(container, "IInitialize"); 19 | await Promise.all(initializers.map(async service => { 20 | if ((service as IInitialize).initialize) { 21 | await service.initialize(); 22 | 23 | if ("logger" in service) { 24 | ((service as any).logger as ILogger).verbose("Initialized"); 25 | } 26 | } 27 | })); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/kit/src/di/context.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerError, useContext } from "@httpc/server"; 2 | import { DependencyContainer, container as globalContainer } from "tsyringe"; 3 | import { Constructor, EnvVariableKey, RESOLVE_MANY, ServiceInjectToken, ServiceInstance, ServiceInstances, ServiceKey } from "./keys"; 4 | 5 | 6 | export function useContainer(): DependencyContainer; 7 | export function useContainer(scope: "global"): DependencyContainer; 8 | export function useContainer(scope?: string): DependencyContainer { 9 | if (scope === "global") { 10 | return globalContainer; 11 | } 12 | 13 | const { container } = useContext(); 14 | if (!container) { 15 | throw new HttpCServerError("missingContextData", "missing container"); 16 | } 17 | 18 | return container; 19 | } 20 | 21 | export function useInjected(type: T): ServiceInstance; 22 | export function useInjected(type: T): ServiceInstance; 23 | export function useInjected(type: T): ServiceInstance; 24 | export function useInjected(type: T): ServiceInstance; 25 | export function useInjected(type: string): T; 26 | export function useInjected(...types: T): ServiceInstances; 27 | export function useInjected(...tokens: any[]) { 28 | const container = useContainer(); 29 | const instances = RESOLVE_MANY(container, ...tokens); 30 | 31 | return tokens.length === 1 32 | ? instances[0] 33 | : instances; 34 | } 35 | -------------------------------------------------------------------------------- /packages/kit/src/di/index.ts: -------------------------------------------------------------------------------- 1 | import "./tsyringe-patch"; 2 | 3 | export * from "./keys"; 4 | export * from "./decorators"; 5 | export * from "./context"; 6 | export * from "./container"; 7 | -------------------------------------------------------------------------------- /packages/kit/src/di/tsyringe-patch.ts: -------------------------------------------------------------------------------- 1 | import { container as globalContainer, DependencyContainer } from "tsyringe"; 2 | import { CONTAINER_KEY } from "./keys"; 3 | 4 | 5 | /* 6 | * Add extra functionality to tsyringe container 7 | * - self registration, so it can resolve itself 8 | */ 9 | 10 | 11 | // self register 12 | patchSelfRegister(globalContainer); 13 | 14 | function patchSelfRegister(container: DependencyContainer) { 15 | registerSelf(container); 16 | 17 | // self register on child 18 | patch(container, "createChildContainer", function (original) { 19 | const value = original.call(this); 20 | patchSelfRegister(value); 21 | return value; 22 | }); 23 | 24 | patch(container, "reset", function (original) { 25 | original.call(this); 26 | registerSelf(this); 27 | }); 28 | 29 | 30 | function registerSelf(container: DependencyContainer) { 31 | container.registerInstance(CONTAINER_KEY, container); 32 | } 33 | } 34 | 35 | 36 | function patch(instance: T, method: K, body: (this: T, base: T[K], ...args: Parameters any ? T[K] : never>) => ReturnType any ? T[K] : never>) { 37 | const oldMethod = instance[method]; 38 | instance[method] = function (this: T) { 39 | return body.apply(this, [oldMethod, ...arguments] as any); 40 | } as any 41 | } 42 | -------------------------------------------------------------------------------- /packages/kit/src/email/NoopEmailSender.ts: -------------------------------------------------------------------------------- 1 | import { singleton } from "tsyringe"; 2 | import { assert } from "../internal"; 3 | import type { IEmailSender, EmailMessage } from "./types"; 4 | import { type ILogger, logger } from "../logging"; 5 | import { BaseService } from "../services"; 6 | 7 | 8 | @singleton() 9 | export class NoopEmailSender extends BaseService() implements IEmailSender { 10 | constructor( 11 | @logger() logger: ILogger, 12 | ) { 13 | //@ts-expect-error 14 | super(...arguments); 15 | } 16 | 17 | async send(options: EmailMessage) { 18 | assert(!!(options.bodyHtml || options.bodyText), "One of bodyHtml and bodyText is required"); 19 | 20 | if (Array.isArray(options.to)) { 21 | assert(options.to.length > 0, "options.to must have at least an item"); 22 | } 23 | 24 | if (this.logger.isLevelEnabled("debug")) { 25 | this.logger.debug("Email params=%o", { ...options, bodyHtml: "", bodyText: "" }); 26 | } 27 | 28 | this.logger.verbose("Email sent(%s)", options.to); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/kit/src/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./NoopEmailSender"; 3 | export * from "./MailersendEmailSender"; -------------------------------------------------------------------------------- /packages/kit/src/email/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type EmailRecipient = string | Readonly<{ 3 | name?: string 4 | email: string 5 | }> 6 | 7 | export type EmailMessage = { 8 | from: EmailRecipient 9 | to: EmailRecipient | EmailRecipient[] 10 | cc?: EmailRecipient | EmailRecipient[] 11 | bcc?: EmailRecipient | EmailRecipient[] 12 | subject?: string 13 | bodyHtml?: string 14 | bodyText?: string 15 | } 16 | 17 | export interface IEmailSender { 18 | send(params: EmailMessage): Promise 19 | } 20 | -------------------------------------------------------------------------------- /packages/kit/src/events/factory.ts: -------------------------------------------------------------------------------- 1 | import { EventData, EventName, EventType } from "./types"; 2 | 3 | 4 | export function createEvent(event: E, data: EventData>): EventType { 5 | return { $eventName: event, ...data } as any; 6 | } 7 | -------------------------------------------------------------------------------- /packages/kit/src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./handling"; 3 | export * from "./factory"; 4 | export * from "./EventBus"; 5 | -------------------------------------------------------------------------------- /packages/kit/src/events/types.ts: -------------------------------------------------------------------------------- 1 | import { ExpandedKeys } from "../env"; 2 | import { IService } from "../services"; 3 | import { Plain } from "../types"; 4 | 5 | 6 | export interface IEvent { 7 | readonly $eventName: string 8 | } 9 | 10 | export type EventData = Omit 11 | 12 | export type EventName = ExpandedKeys 13 | export type EventType = EventTypes extends { [k in K]: object } 14 | ? Plain> 15 | : IEvent 16 | 17 | 18 | export interface IEventBus extends IService { 19 | addListener(event: E, handler: (payload: EventType) => void): () => void 20 | publish(event: IEvent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/kit/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import type * as nf from "node-fetch"; 2 | import type { default as Fetch } from "node-fetch"; 3 | 4 | 5 | let fetch: typeof Fetch; 6 | let Headers: typeof nf.Headers; 7 | let Request: typeof nf.Request; 8 | let Response: typeof nf.Response; 9 | 10 | (function (global: any) { 11 | ({ 12 | fetch, 13 | Headers, 14 | Request, 15 | Response, 16 | } = global || {}); 17 | 18 | //@ts-ignore 19 | }(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this)); 20 | 21 | 22 | if (!fetch) { 23 | throw new Error("Missing fetch API. Be sure fetch is available from the global object or polyfill it (es: with 'cross-fetch' module)"); 24 | } 25 | 26 | if (!Headers || !Request || !Response) { 27 | throw new Error("Missing fetch API components. Be sure fetch related classes (Request, Response, ...) are available from the global context"); 28 | } 29 | 30 | //@ts-ignore 31 | export type { Headers, Request, Response } from "node-fetch"; 32 | //@ts-ignore 33 | export { fetch, Headers, Request, Response }; 34 | -------------------------------------------------------------------------------- /packages/kit/src/geo/maxmind/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MaxMindGeoService"; 2 | -------------------------------------------------------------------------------- /packages/kit/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@httpc/server"; 2 | export * from "./di"; 3 | export * from "./services"; 4 | export * from "./logging"; 5 | export * from "./events"; 6 | export * from "./controller"; 7 | export * from "./validation"; 8 | export * from "./permissions"; 9 | export * from "./caching"; 10 | export * from "./auth"; 11 | export * from "./email"; 12 | export * from "./testing"; 13 | export * from "./Application"; 14 | -------------------------------------------------------------------------------- /packages/kit/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | -------------------------------------------------------------------------------- /packages/kit/src/internal/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function assert(value: unknown, message?: string | Error): asserts value { 3 | if (!value) { 4 | if (message instanceof Error) { 5 | throw message; 6 | } 7 | 8 | throw new Error(message || "Assertion failed"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/kit/src/logging/NullLoggerService.ts: -------------------------------------------------------------------------------- 1 | import { singleton } from "tsyringe"; 2 | import { ILogger, ILogService } from "./types"; 3 | 4 | 5 | @singleton() 6 | export class NullLoggerService implements ILogService { 7 | createLogger(label: string): ILogger { 8 | return new NullLogger(); 9 | } 10 | } 11 | 12 | 13 | export class NullLogger implements ILogger { 14 | error(message: string | Error, ...args: any[]): void { 15 | } 16 | info(message: string, ...args: any[]): void { 17 | } 18 | warn(message: string, ...args: any[]): void { 19 | } 20 | verbose(message: string, ...args: any[]): void { 21 | } 22 | debug(message: string, ...args: any[]): void { 23 | } 24 | log(level: string, message: string, ...args: any[]): void { 25 | } 26 | isLevelEnabled(level: string): boolean { 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/kit/src/logging/console/LogService.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import { options } from "../../di"; 3 | import { ILogger, ILogService, LogLevel } from "../types"; 4 | import { ConsoleLogger } from "./Logger"; 5 | 6 | 7 | export type ConsoleLogServiceOptions = { 8 | level?: LogLevel | ((label: string) => LogLevel) 9 | } 10 | 11 | @injectable() 12 | export class ConsoleLogService implements ILogService { 13 | constructor( 14 | @options(undefined) protected readonly options?: ConsoleLogServiceOptions 15 | ) { 16 | 17 | } 18 | 19 | createLogger(label: string, properties?: { level?: string }): ILogger { 20 | let level = properties?.level; 21 | 22 | if (!level && this.options?.level) { 23 | if (typeof this.options.level === "string") { 24 | level = this.options.level; 25 | } else { 26 | level = this.options.level(label); 27 | } 28 | } 29 | 30 | if (!level) { 31 | level = process.env.NODE_ENV === "production" ? "info" : "debug"; 32 | } 33 | 34 | return new ConsoleLogger(label, level); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/kit/src/logging/console/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./LogService"; 2 | export * from "./Logger"; 3 | -------------------------------------------------------------------------------- /packages/kit/src/logging/context.ts: -------------------------------------------------------------------------------- 1 | import { KEY, RESOLVE, useContainer, useInjected } from "../di"; 2 | import { ILogger } from "./types"; 3 | 4 | 5 | export function useLogger(label?: string): ILogger { 6 | if (!label) { 7 | return useInjected(KEY("ApplicationLogger")); 8 | } 9 | 10 | const container = useContainer(); 11 | const log = RESOLVE(container, "ILogService"); 12 | 13 | return log.createLogger(label); 14 | } 15 | -------------------------------------------------------------------------------- /packages/kit/src/logging/decorators.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectWithTransform } from "tsyringe"; 2 | import type { ILogService } from "./types"; 3 | import { KEY } from "../di"; 4 | 5 | 6 | export function logger(): ParameterDecorator { 7 | return (target, propertyKey, parameterIndex) => { 8 | injectWithTransform(KEY("ILogService"), CreateLoggerTransform, (target as Function).name)(target, propertyKey, parameterIndex); 9 | }; 10 | } 11 | 12 | class CreateLoggerTransform { 13 | transform(service: ILogService, label: string) { 14 | return service.createLogger(label); 15 | } 16 | } 17 | 18 | 19 | export function appLogger(): ParameterDecorator { 20 | return inject(KEY("ApplicationLogger")); 21 | } 22 | -------------------------------------------------------------------------------- /packages/kit/src/logging/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./context"; 3 | export * from "./decorators"; 4 | export * from "./console"; 5 | export * from "./NullLoggerService"; 6 | export * from "./CallLoggerMiddleware"; 7 | -------------------------------------------------------------------------------- /packages/kit/src/logging/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type LogLevel = 3 | | "error" 4 | | "warn" 5 | | "info" 6 | | "verbose" 7 | | "debug" 8 | 9 | export interface ILogger { 10 | error(message: string | Error, ...args: any[]): void; 11 | info(message: string, ...args: any[]): void; 12 | warn(message: string, ...args: any[]): void; 13 | verbose(message: string, ...args: any[]): void; 14 | debug(message: string, ...args: any[]): void; 15 | log(level: string, message: string, ...args: any[]): void; 16 | isLevelEnabled(level: string): boolean; 17 | } 18 | 19 | export interface ILogService { 20 | createLogger(label: string, properties?: Record): ILogger; 21 | } 22 | -------------------------------------------------------------------------------- /packages/kit/src/logging/winston/WinstonLogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger as ProviderLogger } from "winston"; 2 | import { LogLevel, ILogger } from "../types"; 3 | 4 | 5 | export class WinstonLogger implements ILogger { 6 | constructor( 7 | private readonly logger: ProviderLogger 8 | ) { 9 | } 10 | 11 | error(message: string | Error, ...args: any[]) { 12 | if (typeof message === "string") { 13 | this.log("error", message, ...args); 14 | } else { 15 | this.logger.error(message.stack || message.message); 16 | } 17 | } 18 | 19 | info(message: string, ...args: any[]) { 20 | this.log("info", message, ...args); 21 | } 22 | 23 | warn(message: string, ...args: any[]) { 24 | this.log("warn", message, ...args); 25 | } 26 | 27 | debug(message: string, ...args: any[]) { 28 | this.log("debug", message, ...args); 29 | } 30 | 31 | verbose(message: string, ...args: any[]) { 32 | this.log("verbose", message, ...args); 33 | } 34 | 35 | 36 | log(level: string, message: string, ...args: any[]) { 37 | this.logger.log(level, message, ...args); 38 | } 39 | 40 | isLevelEnabled(level: LogLevel): boolean { 41 | return this.logger.isLevelEnabled(level); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/kit/src/logging/winston/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./WinstonLogService"; 2 | -------------------------------------------------------------------------------- /packages/kit/src/node/fs.ts: -------------------------------------------------------------------------------- 1 | import fss from "fs"; 2 | import fs from "fs/promises"; 3 | import zlib from "zlib"; 4 | import https from "https"; 5 | 6 | 7 | export async function exists(path: string): Promise { 8 | return await fs.access(path).then( 9 | () => true, 10 | () => false 11 | ); 12 | } 13 | 14 | export function unzip(source: string, destination: string): Promise { 15 | return new Promise((resolve, reject) => { 16 | const src = fss.createReadStream(source); 17 | const dest = fss.createWriteStream(destination); 18 | 19 | src.pipe(zlib.createGunzip()).pipe(dest); 20 | 21 | dest.on("close", resolve);; 22 | dest.on("error", reject); 23 | }); 24 | } 25 | 26 | 27 | export function downloadTo(url: string, destination: string): Promise { 28 | return new Promise((resolve, reject) => { 29 | const file = fss.createWriteStream(destination); 30 | 31 | const request = https.get(url, response => { 32 | if ((response.statusCode || 0) >= 400) { 33 | reject(new Error(`HTTP-${response.statusCode}`)); 34 | return; 35 | } 36 | 37 | response.pipe(file); 38 | }); 39 | request.on("error", reject); 40 | 41 | file.on("finish", resolve); 42 | file.on("error", reject); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /packages/kit/src/node/index.internal.ts: -------------------------------------------------------------------------------- 1 | export * from "./fs"; 2 | export * from "./Application"; 3 | -------------------------------------------------------------------------------- /packages/kit/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Application"; 2 | -------------------------------------------------------------------------------- /packages/kit/src/permissions/PermissionSerializer.ts: -------------------------------------------------------------------------------- 1 | import { PermissionToken, AuthClaim, AssertionClaim, Authorization, Assertion } from "./models"; 2 | 3 | 4 | function serializeTokenClaim(token: PermissionToken): string { 5 | return typeof token === "string" ? token : token.join(":"); 6 | } 7 | 8 | function serializeAuthorizationClaim(claim: AuthClaim): string { 9 | let str = serializeTokenClaim(claim.token); 10 | 11 | if (claim.scope) { 12 | str += "@" + serializeTokenClaim(claim.scope); 13 | } 14 | 15 | return str; 16 | } 17 | 18 | function serializeAssertionClaim(assertion: AssertionClaim): string { 19 | let str = serializeAuthorizationClaim(assertion); 20 | 21 | if (assertion.negative) { 22 | str = "!" + str; 23 | } 24 | 25 | return str; 26 | } 27 | 28 | function serialize(what: Authorization | Assertion): string { 29 | const serialize = what instanceof Assertion 30 | ? serializeAssertionClaim 31 | : serializeAuthorizationClaim 32 | 33 | return what.claims.map(serialize).join(" "); 34 | } 35 | 36 | 37 | export default { 38 | serialize, 39 | serializeTokenClaim, 40 | serializeAuthorizationClaim, 41 | serializeAssertionClaim 42 | } 43 | -------------------------------------------------------------------------------- /packages/kit/src/permissions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./models"; 2 | export { default as PermissionSerializer } from "./PermissionSerializer"; 3 | export { PermissionsModel } from "./PermissionModel"; 4 | export { PermissionsChecker, InvalidClaim } from "./PermissionChecker"; 5 | export * from "./builder"; 6 | -------------------------------------------------------------------------------- /packages/kit/src/services/aspects.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger } from "../logging"; 2 | 3 | 4 | export function Catch(defaultValue?: any): MethodDecorator { 5 | return (target, key, descriptor: TypedPropertyDescriptor) => { 6 | const original = descriptor.value; 7 | 8 | descriptor.value = async function (this: { logger?: ILogger }, ...args: any[]) { 9 | try { 10 | return await original.apply(this, args); 11 | } catch (ex: any) { 12 | this.logger?.error(`Catch ${target.constructor.name}.${key.toString()}\n%s\nArguments: %o`, ex.stack || (`(${ex.name}) ${ex.message}`), args); 13 | return defaultValue; 14 | } 15 | }; 16 | 17 | return descriptor; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/kit/src/services/context.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "../internal"; 2 | import { useInjected } from "../di" 3 | import { IService } from "./types"; 4 | 5 | 6 | 7 | type ServiceConstructor = new (...args: any) => T 8 | 9 | type Instances = { 10 | [k in keyof T]: T[k] extends ServiceConstructor ? I 11 | : T[k] extends IService ? T[k] 12 | : never 13 | } 14 | 15 | export function useTransaction(type: T, func: (service: InstanceType) => Promise): Promise 16 | export function useTransaction(type: T, func: (service: T) => Promise): Promise 17 | export function useTransaction(...services: [...T, (...instances: Instances) => Promise]): Promise 18 | export function useTransaction(...args: any[]) { 19 | const func = args.pop(); 20 | assert(typeof func === "function", "last argument must be a function"); 21 | assert(args.length > 0, "you mys specify at least a service"); 22 | 23 | const data = useInjected("IDbService"); 24 | const instances = args.map(x => typeof x === "object" ? x : useInjected(x)); 25 | 26 | return data.startTransaction(data => func(...instances.map(x => x.inTransaction?.(data) || x))); 27 | } 28 | -------------------------------------------------------------------------------- /packages/kit/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./error"; 3 | export * from "./aspects"; 4 | export * from "./context"; 5 | export * from "./catches"; 6 | export * from "./BaseService"; 7 | -------------------------------------------------------------------------------- /packages/kit/src/services/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IService { 3 | inTransaction?(transactionService: ITransactionService): this 4 | } 5 | 6 | export interface IInitialize { 7 | initialize(): Promise; 8 | } 9 | 10 | export interface IDbService { 11 | startTransaction(func: (transactionService: ITransactionService) => Promise): Promise; 12 | } 13 | 14 | export interface ITransactionService { 15 | on(event: "afterTransaction", handler: (() => void | Promise)): void; 16 | } 17 | -------------------------------------------------------------------------------- /packages/kit/src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Plain = T extends any ? { [k in keyof T]: T[k] } : never 3 | 4 | export type Factorable = T | (() => T) 5 | 6 | 7 | declare const __nominal: unique symbol; 8 | 9 | export type Nominal = Type & { 10 | readonly [__nominal]: Name; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/kit/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./objects"; 2 | export * from "./time"; 3 | -------------------------------------------------------------------------------- /packages/kit/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | 2 | export type TimeUnit = 3 | | "ms" 4 | | "s" 5 | | "m" 6 | | "h" 7 | | "d" 8 | 9 | export type HumanDuration = `${number}${TimeUnit}`; 10 | 11 | /** 12 | * Parse the human-duration string into milliseconds 13 | * 14 | * @param {HumanDuration} duration A human readable string representing a duration 15 | * @returns {number} Duration in milliseconds 16 | */ 17 | export function humanDuration(duration: HumanDuration): number { 18 | const [, _amount, _unit] = duration.match(/^(\d+)(\w+)$/) || []; 19 | const unit = _unit as TimeUnit; 20 | const amount = parseInt(_amount, 10); 21 | 22 | switch (unit) { 23 | case "ms": return amount; 24 | case "s": return amount * 1000; 25 | case "m": return amount * 1000 * 60; 26 | case "h": return amount * 1000 * 60 * 60; 27 | case "d": return amount * 1000 * 60 * 60 * 24; 28 | default: 29 | const _: never = unit; 30 | throw new Error("Invalid unit: " + unit); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/kit/src/validation/PredicateValidator.ts: -------------------------------------------------------------------------------- 1 | import { singleton } from "tsyringe"; 2 | import { alias, KEY } from "../di"; 3 | import { IValidator, ValidationResult } from "./types"; 4 | import { isClass } from "./utils"; 5 | 6 | 7 | export type PredicateValidate = (params: any) => (boolean | ValidationResult); 8 | 9 | 10 | @singleton() 11 | @alias(KEY("IValidator")) 12 | export class PredicateValidator implements IValidator { 13 | canValidate(object: any, schema: any, options?: any): boolean { 14 | return typeof schema === "function" && !isClass(schema); 15 | } 16 | 17 | validate(object: any, schema: PredicateValidate, options?: any): ValidationResult { 18 | if (typeof schema !== "function") { 19 | throw new Error("Invalid schema: " + schema); 20 | } 21 | 22 | const result = schema(object); 23 | if (typeof result === "object") { 24 | return result; 25 | } 26 | 27 | return result 28 | ? { success: true, object } 29 | : { success: false, errors: ["Invalid"] } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/kit/src/validation/decorators.ts: -------------------------------------------------------------------------------- 1 | import { decoratorMetadata, decoratorMiddleware } from "../controller"; 2 | import { combine } from "../di"; 3 | import { Validate } from "./pipeline"; 4 | 5 | 6 | export const validate = (...schema: any[]) => combine( 7 | decoratorMetadata("validate:paramtypes", schema.length === 0)(), 8 | decoratorMiddleware(Validate)(...schema), 9 | ); 10 | -------------------------------------------------------------------------------- /packages/kit/src/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./utils"; 3 | export * from "./ValidationService"; 4 | export * from "./pipeline"; 5 | export * from "./decorators"; 6 | export * from "./NativeTypeValidator"; 7 | export * from "./PredicateValidator"; 8 | -------------------------------------------------------------------------------- /packages/kit/src/validation/types.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface IValidator { 4 | canValidate(object: any, schema: any, options?: any): boolean 5 | validate(object: any, schema: any, options?: any): ValidationResult 6 | } 7 | 8 | export type ValidationResult = 9 | | ValidationResultSuccess 10 | | ValidationResultFailed 11 | 12 | 13 | export type ValidationResultSuccess = { 14 | success: true 15 | object: any 16 | } 17 | 18 | export type ValidationResultFailed = { 19 | success: false 20 | errors: string[] 21 | } 22 | -------------------------------------------------------------------------------- /packages/kit/src/validation/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function isClass(func: Function): boolean { 3 | return typeof func === "function" && func.prototype && !Object.getOwnPropertyDescriptor(func, "prototype")?.writable || false; 4 | } 5 | 6 | 7 | export function OptionalSchema(schema: T): T { 8 | return new Optional(schema) as any as T; 9 | } 10 | 11 | class Optional { 12 | constructor(readonly schema: any) { } 13 | } 14 | 15 | export function isOptionalSchema(schema: any): schema is Optional { 16 | return schema && schema instanceof Optional; 17 | } 18 | -------------------------------------------------------------------------------- /packages/kit/src/validation/valibot/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerError } from "@httpc/server"; 2 | import { singleton } from "tsyringe"; 3 | import { alias, KEY } from "../../di"; 4 | import { IValidator, ValidationResult } from "../types"; 5 | import { BaseSchema, safeParse } from "valibot"; 6 | 7 | 8 | @singleton() 9 | @alias(KEY("IValidator")) 10 | export class ValibotValidator implements IValidator { 11 | canValidate(object: any, schema: any, options?: any): boolean { 12 | if (!schema || typeof schema !== "object") { 13 | return false; 14 | } 15 | 16 | const s = schema as BaseSchema & { schema: string }; 17 | if (!s.schema || !s._parse) { 18 | return false; 19 | } 20 | 21 | // async not supported 22 | if (s.async) { 23 | throw new HttpCServerError("notSupported", "Valibot async validation is not supported"); 24 | } 25 | 26 | return true; 27 | } 28 | 29 | validate(object: any, schema: BaseSchema, options?: any): ValidationResult { 30 | const result = safeParse(schema, object); 31 | if (!result.success) { 32 | return { 33 | success: false, 34 | errors: result.error.issues.map(x => x.message), 35 | }; 36 | } 37 | 38 | return { 39 | success: true, 40 | object: result.data, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/kit/src/validation/zod/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerError } from "@httpc/server"; 2 | import { singleton } from "tsyringe"; 3 | import { ZodType } from "zod"; 4 | import { alias, KEY } from "../../di"; 5 | import { IValidator, ValidationResult } from "../types"; 6 | 7 | 8 | @singleton() 9 | @alias(KEY("IValidator")) 10 | export class ZodValidator implements IValidator { 11 | canValidate(object: any, schema: any, options?: any): boolean { 12 | return !!schema && schema instanceof ZodType; 13 | } 14 | 15 | validate(object: any, schema: ZodType, options?: any): ValidationResult { 16 | if (!this.canValidate(object, schema, object)) { 17 | throw new HttpCServerError("invalidState"); 18 | } 19 | 20 | const result = schema.safeParse(object); 21 | if (!result.success) { 22 | return { 23 | success: false, 24 | errors: result.error.errors.map(x => x.message), 25 | }; 26 | } 27 | 28 | return { 29 | success: true, 30 | object: result.data, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/kit/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from "crypto"; 2 | 3 | 4 | export const random = { 5 | number() { 6 | return Math.random(); 7 | }, 8 | string() { 9 | const length = 10 + Math.floor(Math.random() * 10); 10 | const min = "A".charCodeAt(0); 11 | const max = "Z".charCodeAt(0); 12 | return String.fromCharCode(...[...Array(length)].map(() => min + Math.floor(Math.random() * (max - min + 1)))); 13 | }, 14 | uuid() { 15 | return randomUUID(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/kit/tests/validations/ClassValidator.test.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ClassValidator } from "../../src/validation/class"; 3 | 4 | 5 | describe("ClassValidator", () => { 6 | const validator = new ClassValidator(); 7 | 8 | const anyArgs = [ 9 | undefined, 10 | null, 11 | "", 12 | "test", 13 | 0, 14 | 1, 15 | {}, 16 | new Date(), 17 | [], 18 | ]; 19 | 20 | 21 | test("can validate Class schemas", () => { 22 | class Schema { } 23 | 24 | anyArgs.forEach(obj => { 25 | expect(validator.canValidate(obj, Schema)).toBe(true); 26 | }); 27 | }); 28 | 29 | test("can not validate others", () => { 30 | anyArgs.forEach(obj => { 31 | expect(validator.canValidate(obj, String)).toBe(false); 32 | expect(validator.canValidate(obj, Number)).toBe(false); 33 | expect(validator.canValidate(obj, Object)).toBe(false); 34 | expect(validator.canValidate(obj, Boolean)).toBe(false); 35 | expect(validator.canValidate(obj, Date)).toBe(false); 36 | expect(validator.canValidate(obj, Array)).toBe(false); 37 | 38 | expect(validator.canValidate(obj, () => { })).toBe(false); 39 | expect(validator.canValidate(obj, {})).toBe(false); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/kit/tests/validations/ValibotValidator.test.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ValibotValidator } from "../../src/validation/valibot"; 3 | import { object } from "valibot"; 4 | 5 | 6 | describe("ValibotValidator", () => { 7 | const validator = new ValibotValidator(); 8 | 9 | const anyArgs = [ 10 | undefined, 11 | null, 12 | "", 13 | "test", 14 | 0, 15 | 1, 16 | {}, 17 | new Date(), 18 | [], 19 | ]; 20 | 21 | 22 | test("can validate valibot schema", () => { 23 | const schema = object({}); 24 | 25 | anyArgs.forEach(obj => { 26 | expect(validator.canValidate(obj, schema)).toBe(true); 27 | }); 28 | }); 29 | 30 | test("can not validate others", () => { 31 | anyArgs.forEach(obj => { 32 | expect(validator.canValidate(obj, String)).toBe(false); 33 | expect(validator.canValidate(obj, Number)).toBe(false); 34 | expect(validator.canValidate(obj, Object)).toBe(false); 35 | expect(validator.canValidate(obj, Boolean)).toBe(false); 36 | expect(validator.canValidate(obj, Date)).toBe(false); 37 | expect(validator.canValidate(obj, Array)).toBe(false); 38 | 39 | expect(validator.canValidate(obj, () => { })).toBe(false); 40 | expect(validator.canValidate(obj, {})).toBe(false); 41 | 42 | expect(validator.canValidate(obj, class { })).toBe(false); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/kit/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "rootDir": "src", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src", 13 | "env.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/kit/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "rootDir": "src", 6 | "composite": true, 7 | "tsBuildInfoFile": "./dist/cjs.tsBuildInfoFile", 8 | "noEmit": false, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "declarationMap": true 12 | }, 13 | "include": [ 14 | "src" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/kit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationMap": true 8 | }, 9 | "include": [ 10 | "src", 11 | "tests", 12 | "env.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/kit/validation-class.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/validation/class"; 2 | -------------------------------------------------------------------------------- /packages/kit/validation-valibot.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/validation/valibot"; 2 | -------------------------------------------------------------------------------- /packages/kit/validation-zod.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/validation/zod"; 2 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # @httpc/server 2 | This package is part of [httpc](https://httpc.dev) framework. 3 | 4 | ## httpc 5 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 6 | - [Documentation and tutorials](https://httpc.dev/docs) 7 | - [Community](https://httpc.dev/discord) 8 | - [Issues and feature requests](https://httpc.dev/issues) 9 | 10 | ## httpc family 11 | **@httpc/server**: the httpc core component allowing function calls over the standard http protocol 12 | 13 | **@httpc/client**: typed interface used by consumers to interact safely with functions exposed by an httpc server 14 | 15 | **@httpc/kit**: rich toolbox of builtin components to manage common use cases and business concerns like authentication, validation, caching and logging 16 | 17 | **@httpc/cli**: commands to setup a project, generate clients, manage versioning and help with common tasks 18 | 19 | **@httpc/adapter-\***: various [adapters](https://httpc.dev/docs/adapters) to host an httpc API inside environment like vercel, netlify functions, aws lambda and similar 20 | -------------------------------------------------------------------------------- /packages/server/env.d.ts: -------------------------------------------------------------------------------- 1 | namespace HttpC { 2 | type LogLevel = 3 | | "debug" 4 | | "verbose" 5 | | "info" 6 | | "success" 7 | | "warn" 8 | | "error" 9 | | "critical" 10 | 11 | type Logger = (level: LogLevel, message: string, ...args: any[]) => void; 12 | } 13 | 14 | 15 | interface IHttpCContext { 16 | readonly requestId: string 17 | readonly request: Request 18 | readonly startedAt: number 19 | readonly logger?: HttpC.Logger 20 | responseHeaders?: Record; 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testMatch": [ 4 | "**/*.test.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/node.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/cjs/node"; 2 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./errors"; 2 | export * from "./context"; 3 | export { randomUUID } from "./utils"; 4 | export * from "./hooks"; 5 | export * from "./parsers"; 6 | export * from "./requests"; 7 | export * from "./responses"; 8 | export * from "./pipeline"; 9 | export * from "./presets"; 10 | export * from "./middlewares"; 11 | export { useLog } from "./logger"; 12 | export * from "./testing"; 13 | export * from "./server"; 14 | export * from "./node"; 15 | -------------------------------------------------------------------------------- /packages/server/src/index.web.ts: -------------------------------------------------------------------------------- 1 | export * from "./errors"; 2 | export * from "./context"; 3 | export { randomUUID } from "./utils"; 4 | export * from "./hooks"; 5 | export * from "./parsers"; 6 | export * from "./requests"; 7 | export * from "./responses"; 8 | export * from "./pipeline"; 9 | export * from "./presets"; 10 | export * from "./middlewares"; 11 | export { useLog } from "./logger"; 12 | export * from "./testing"; 13 | export * from "./server"; 14 | -------------------------------------------------------------------------------- /packages/server/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | -------------------------------------------------------------------------------- /packages/server/src/internal/utils.ts: -------------------------------------------------------------------------------- 1 | export type MayBeArray = T | T[]; 2 | export type MayBePromise = T | Promise; 3 | export type Optional = T | false | undefined; 4 | 5 | 6 | export function filterOptionals(items?: Optional[]): T[] { 7 | return items?.filter(x => !!x) as T[] || []; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /packages/server/src/middlewares/LogCallMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "../context"; 2 | import { createConsoleColors, useLog, LogOptions } from "../logger"; 3 | import { HttpCServerMiddleware } from "../requests"; 4 | 5 | 6 | export type LogCallMiddlewareOptions = true | LogOptions 7 | 8 | export function LogCallMiddleware(options?: LogCallMiddlewareOptions): HttpCServerMiddleware { 9 | 10 | const { 11 | ansi = true, 12 | } = (options === true ? undefined : options) || {} 13 | 14 | const { 15 | gray, 16 | } = createConsoleColors(ansi); 17 | 18 | 19 | return async (call, next) => { 20 | const result = await next(call); 21 | 22 | const elapsed = Date.now() - useContext().startedAt; 23 | if (result && result instanceof Error) { 24 | useLog("error", `${gray(call.access)}\t${call.path} ${gray(`(${elapsed}ms)`)}`); 25 | } else { 26 | useLog("success", `${gray(call.access)}\t${call.path} ${gray(`(${elapsed}ms)`)}`); 27 | } 28 | 29 | return result; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/server/src/middlewares/PassthroughMiddleware.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCServerMiddleware } from "../requests"; 2 | 3 | 4 | export function PassthroughMiddleware(func: () => Promise | void): HttpCServerMiddleware { 5 | return async (call, next) => { 6 | await func(); 7 | return await next(call); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PassthroughMiddleware"; 2 | export * from "./LogCallMiddleware"; 3 | -------------------------------------------------------------------------------- /packages/server/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./server"; 2 | export * from "./testing"; 3 | -------------------------------------------------------------------------------- /packages/server/src/node/mocks/IncomingMessageMock.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, METHODS } from "node:http"; 2 | import { Readable } from "node:stream"; 3 | 4 | 5 | export type IncomingMessageMockParams = { 6 | method?: "GET" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "DELETE" 7 | path: string 8 | headers?: Record 9 | body?: any 10 | } 11 | 12 | export function createIncomingMessageMock(params: IncomingMessageMockParams): IncomingMessage { 13 | let { 14 | method = "POST", 15 | headers, 16 | body, 17 | path, 18 | } = params; 19 | 20 | if (!METHODS.includes(method)) { 21 | throw new Error(`Http method unknown: ${method}`); 22 | } 23 | 24 | 25 | let buffer; 26 | if (typeof body === "object") { 27 | buffer = Buffer.from(JSON.stringify(body), "utf8"); 28 | headers = { 29 | "content-type": "application/json", 30 | "content-length": buffer.length, 31 | ...headers, 32 | } 33 | } 34 | 35 | const content = Readable.from(buffer || []); 36 | 37 | return Object.assign(content, { 38 | url: path, 39 | method, 40 | headers: normalizeHeaders(headers), 41 | }) as unknown as IncomingMessage; 42 | } 43 | 44 | 45 | function normalizeHeaders(headers?: Record) { 46 | if (!headers) return {}; 47 | return Object.fromEntries( 48 | Object.entries(headers || {}).map(([key, value]) => [key.toLowerCase(), value]) 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/server/src/node/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./IncomingMessageMock"; 2 | export * from "./ServerResponseMock"; 3 | -------------------------------------------------------------------------------- /packages/server/src/node/testing.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCServerOptions } from "../server"; 2 | import { createHttpCNodeServer } from "./server"; 3 | import { createIncomingMessageMock, IncomingMessageMockParams, createServerResponseMock } from "../node/mocks"; 4 | 5 | 6 | export type HttpCNodeServerTesterOptions = HttpCServerOptions 7 | 8 | export type HttpCNodeServerTester = ReturnType 9 | 10 | export function createHttpCNodeServerTester(options: HttpCNodeServerTesterOptions) { 11 | options = { 12 | log: false, 13 | ...options, 14 | }; 15 | 16 | const server = createHttpCNodeServer(options); 17 | const processor = server.requestProcessor; 18 | 19 | return { 20 | async process(request: IncomingMessageMockParams) { 21 | await processor(createIncomingMessageMock(request), createServerResponseMock()); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/server/src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PathMatcher"; 2 | export { default as Parser } from "./Parser"; 3 | export * from "./HttpCCallParser"; 4 | export * from "./FormUrlEncodedParser"; 5 | export * from "./RawBodyParser"; 6 | -------------------------------------------------------------------------------- /packages/server/src/parsers/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function tryParseInt(value: string | undefined | null, defaultValue?: number): number { 3 | if (value) { 4 | try { return parseInt(value!) } 5 | catch { } 6 | } 7 | 8 | return defaultValue ?? 0; 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/src/presets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./static"; 2 | -------------------------------------------------------------------------------- /packages/server/src/renderers/BinaryRenderer.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCServerCallRenderer } from "../requests"; 2 | import { BinaryResponse } from "../responses"; 3 | 4 | 5 | export function BinaryRenderer(): HttpCServerCallRenderer { 6 | return async result => { 7 | 8 | if (result && ( 9 | result instanceof Uint8Array || 10 | result instanceof ArrayBuffer 11 | )) { 12 | return new BinaryResponse(result); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/renderers/ErrorRenderer.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCServerCallRenderer } from "../requests"; 2 | import { ErrorResponse, JsonResponse } from "../responses"; 3 | 4 | 5 | export function ErrorRenderer(): HttpCServerCallRenderer { 6 | return async result => { 7 | if (!result || !(result instanceof Error)) { 8 | return; 9 | } 10 | 11 | return new ErrorResponse(result); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/src/renderers/JsonRenderer.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCServerCallRenderer } from "../requests"; 2 | import { JsonResponse } from "../responses"; 3 | 4 | 5 | export function JsonRenderer(): HttpCServerCallRenderer { 6 | return async result => new JsonResponse(result); 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/src/renderers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./JsonRenderer"; 2 | export * from "./ErrorRenderer"; 3 | export * from "./BinaryRenderer"; 4 | -------------------------------------------------------------------------------- /packages/server/src/requests/coors.ts: -------------------------------------------------------------------------------- 1 | import type { HttpCServerRequestMiddleware } from "./types"; 2 | import { useResponseHeader } from "../hooks"; 3 | import { EmptyResponse } from "../responses"; 4 | 5 | 6 | export type CoorsRequestProcessorOptions = { 7 | maxAge?: number 8 | } 9 | 10 | export function CoorsRequestMiddleware(options?: CoorsRequestProcessorOptions): HttpCServerRequestMiddleware { 11 | const { 12 | maxAge = 86400 // 1 day 13 | } = options || {}; 14 | 15 | return (req, next) => { 16 | if (req.method === "OPTIONS") { 17 | return new EmptyResponse({ 18 | "Access-Control-Allow-Origin": "*", 19 | "Access-Control-Allow-Methods": "*", 20 | "Access-Control-Allow-Headers": "*", 21 | "Access-Control-Max-Age": maxAge.toString(), 22 | "Content-Length": "0", 23 | }); 24 | } 25 | 26 | useResponseHeader("access-control-allow-origin", "*"); 27 | 28 | return next(req); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/server/src/requests/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./coors"; 3 | export * from "./httpc"; 4 | -------------------------------------------------------------------------------- /packages/server/src/requests/types.ts: -------------------------------------------------------------------------------- 1 | import { MayBePromise } from "../internal"; 2 | import { HttpCServerResponse } from "../responses"; 3 | 4 | 5 | export type HttpCServerRequestProcessor = (req: Request) => MayBePromise 6 | export type HttpCServerRequestMiddleware = (req: Request, next: HttpCServerRequestProcessor) => MayBePromise 7 | -------------------------------------------------------------------------------- /packages/server/src/responses/EmptyResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerResponse } from "./HttpCServerResponse"; 2 | 3 | 4 | export class EmptyResponse extends HttpCServerResponse { 5 | constructor(headers?: Record); 6 | constructor(statusCode: number, headers?: Record); 7 | constructor(statusOrHeaders: number | Record = 204, headers?: Record) { 8 | if (typeof statusOrHeaders === "object") { 9 | super({ statusCode: 204, headers: statusOrHeaders }); 10 | } else { 11 | super({ statusCode: statusOrHeaders, headers }); 12 | } 13 | } 14 | 15 | override render() { 16 | return new Response(undefined, { 17 | status: this.statusCode ?? 204, 18 | headers: this.getHeaders() 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/responses/ErrorResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpCallError } from "../errors"; 2 | import { JsonResponse } from "./JsonResponse"; 3 | 4 | 5 | export class ErrorResponse extends JsonResponse { 6 | constructor(error: Error); 7 | constructor(statusCode: number, error: Error); 8 | constructor(status: number | Error, error?: Error) { 9 | if (arguments.length === 1) { 10 | error = status as Error; 11 | status = 0; 12 | } 13 | 14 | status = (status as number) || (error instanceof HttpCallError ? error.status : 500); 15 | super(status, error); 16 | } 17 | 18 | override render() { 19 | const error = this.body as Error; 20 | let { message, stack, data } = error as any; 21 | let errorCode = error instanceof HttpCallError ? 22 | error.errorCode : 23 | "internal_error"; 24 | 25 | const body = this.body = { 26 | error: errorCode, 27 | message: message || undefined, 28 | stack, 29 | data, 30 | }; 31 | 32 | if (process.env.NODE_ENV === "production") { 33 | body.stack = undefined; 34 | 35 | if (!(error instanceof HttpCallError)) { 36 | errorCode = "internal_error"; 37 | body.message = undefined; 38 | } 39 | } 40 | 41 | return super.render(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/server/src/responses/HttpCServerResponse.ts: -------------------------------------------------------------------------------- 1 | import { useContextProperty } from "../context"; 2 | 3 | 4 | export abstract class HttpCServerResponse { 5 | constructor(params: { 6 | statusCode: number 7 | headers?: Record 8 | body?: unknown 9 | }) { 10 | this.statusCode = params.statusCode; 11 | this.headers = params.headers; 12 | this.body = params.body; 13 | } 14 | 15 | private headers?: Record 16 | statusCode: number 17 | body?: unknown 18 | 19 | setHeader(name: string, value: string | number) { 20 | (this.headers ??= {})[name] = value; 21 | } 22 | 23 | abstract render(): Response 24 | 25 | protected getHeaders(): Record | undefined { 26 | let headers = this.headers as {}; 27 | 28 | const contextHeaders = useContextProperty("responseHeaders"); 29 | if (contextHeaders) { 30 | headers = { ...headers, ...contextHeaders }; 31 | } 32 | 33 | return headers; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/server/src/responses/JsonResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerResponse } from "./HttpCServerResponse"; 2 | 3 | 4 | export class JsonResponse extends HttpCServerResponse { 5 | constructor(data: unknown); 6 | constructor(statusCode: number, data: unknown); 7 | constructor(status: unknown, data?: unknown) { 8 | if (arguments.length === 1) { 9 | data = status; 10 | status = 0; 11 | } 12 | 13 | 14 | super({ 15 | statusCode: status as number || 0, 16 | body: data, 17 | }); 18 | } 19 | 20 | override render() { 21 | const status = this.statusCode || 200; 22 | // treat undefined as null, as undefined is not a valid json 23 | const body = JSON.stringify(this.body ?? null); 24 | const headers = { 25 | ...this.getHeaders(), 26 | ...body ? { 27 | "Content-Type": "application/json; charset=utf-8", 28 | "Content-Length": body.length.toString() 29 | } : undefined 30 | }; 31 | 32 | return new Response(body, { 33 | status, 34 | headers 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/server/src/responses/RedirectResponse.ts: -------------------------------------------------------------------------------- 1 | import { EmptyResponse } from "./EmptyResponse"; 2 | 3 | 4 | export class RedirectResponse extends EmptyResponse { 5 | constructor(location: string); 6 | constructor(statusCode: number, location: string); 7 | constructor(status: number | string, location?: string) { 8 | if (typeof status === "string") { 9 | location = status; 10 | status = 301; 11 | } 12 | 13 | super(status, { location: location! }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/responses/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HttpCServerResponse"; 2 | export * from "./EmptyResponse"; 3 | export * from "./JsonResponse"; 4 | export * from "./ErrorResponse"; 5 | export * from "./RedirectResponse"; 6 | export * from "./BinaryResponse"; 7 | -------------------------------------------------------------------------------- /packages/server/src/testing/context.ts: -------------------------------------------------------------------------------- 1 | import { runInContext } from "../context"; 2 | 3 | const testContextData = new Map(); 4 | 5 | export const testContext = { 6 | get(key: string | symbol): T | undefined { 7 | return testContextData.get(key); 8 | }, 9 | set(key: string | symbol, value: any) { 10 | testContextData.set(key, value); 11 | }, 12 | assign(context: object) { 13 | for (const [key, value] of Object.entries(context)) { 14 | if (value !== undefined && value !== null) { 15 | testContext.set(key, value); 16 | } 17 | } 18 | }, 19 | clear() { 20 | testContextData.clear(); 21 | }, 22 | }; 23 | 24 | const testContextProxy = new Proxy(testContext, { 25 | get(target, property) { 26 | return target.get(property); 27 | }, 28 | set(target, property, value) { 29 | target.set(property, value); 30 | return true; 31 | }, 32 | }); 33 | 34 | 35 | export function runInTestContext(func: () => T) { 36 | return runInContext(testContextProxy, func); 37 | } 38 | -------------------------------------------------------------------------------- /packages/server/src/testing/index.ts: -------------------------------------------------------------------------------- 1 | export { testContext, runInTestContext } from "./context"; 2 | export * from "./pipeline"; 3 | export * from "./tester"; 4 | -------------------------------------------------------------------------------- /packages/server/src/testing/tester.ts: -------------------------------------------------------------------------------- 1 | import { HttpCServerOptions, IHttpCServer, createHttpCServerHandler } from "../server"; 2 | 3 | type HttpCServerTesterOptions = HttpCServerOptions 4 | 5 | export function createHttpCServerTester(options: HttpCServerTesterOptions): IHttpCServer { 6 | options = { 7 | log: false, 8 | ...options, 9 | }; 10 | 11 | const handler = createHttpCServerHandler(options); 12 | 13 | return { 14 | fetch: handler 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/server/src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function randomUUID() { 3 | if (globalThis.crypto) { 4 | return crypto.randomUUID(); 5 | } 6 | 7 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (x) { 8 | var r = (Math.random() * 16) | 0, 9 | v = x == 'x' ? r : (r & 0x3) | 0x8; 10 | return v.toString(16); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/tests/middlewares/PassthroughMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from "crypto"; 2 | import { PassthroughMiddleware, httpCallTester } from "../../src"; 3 | 4 | 5 | describe("PassthroughMiddleware", () => { 6 | test("call result value goes through", async () => { 7 | const result = randomUUID(); 8 | 9 | const call = httpCallTester( 10 | PassthroughMiddleware(() => { /* noop */ }), 11 | () => result 12 | ); 13 | 14 | expect(await call()).toBe(result); 15 | }); 16 | 17 | test("call params gets passed", async () => { 18 | const arg1 = randomUUID(); 19 | const arg2 = randomUUID(); 20 | 21 | const handler = jest.fn(() => { /** does nothing */ }); 22 | 23 | const call = httpCallTester( 24 | PassthroughMiddleware(() => { /* noop */ }), 25 | handler 26 | ); 27 | 28 | await call(arg1, arg2); 29 | 30 | expect(handler).toBeCalledWith(arg1, arg2); 31 | }); 32 | }) 33 | -------------------------------------------------------------------------------- /packages/server/tests/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export type RequestInfo = { 3 | path?: string 4 | method: string 5 | body?: any 6 | } 7 | 8 | export function createRequest(method: string, path?: string, body?: any): Request; 9 | export function createRequest(info: RequestInfo): Request; 10 | export function createRequest(method: string | RequestInfo, path?: string, body?: any) { 11 | if (typeof method === "object") { 12 | ({ path, method, body } = method); 13 | } 14 | 15 | if (body) { 16 | body = JSON.stringify(body); 17 | } 18 | 19 | return new Request(new URL(path || "/", "http://localhost"), { 20 | method, 21 | headers: body && { 22 | "content-type": "application/json" 23 | } || undefined, 24 | body 25 | }) 26 | }; 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "rootDir": "src", 6 | "noEmit": false, 7 | "sourceMap": false, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": [ 12 | "src", 13 | "*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/tsconfig.dev.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "rootDir": "src", 6 | "module": "commonjs", 7 | "noEmit": false, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "composite": true, 11 | "tsBuildInfoFile": "./dist/.tsBuildInfoFile.cjs" 12 | }, 13 | "include": [ 14 | "src", 15 | "env.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/server/tsconfig.dev.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.dev.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "module": "esnext", 6 | "tsBuildInfoFile": "./dist/.tsBuildInfoFile.mjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationMap": true 8 | }, 9 | "include": [ 10 | "src", 11 | "tests", 12 | "env.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/www/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.astro", 5 | "options": { 6 | "proseWrap": "preserve", 7 | "singleQuote": false, 8 | "printWidth": 120, 9 | "arrowParens": "avoid" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/www/README.md: -------------------------------------------------------------------------------- 1 | # httpc.dev website 2 | This is the source code of the [httpc.dev](https//httpc.dev) website. 3 | 4 | The web site is the home page of the **httpc** framework and includes: 5 | - Landing page 6 | - Documentation 7 | 8 | The web site is a static/pre-rendered build with [astro](https://astro.build). 9 | 10 | ## httpc 11 | httpc is a javascript/typescript framework for building function-based API with minimal code and end-to-end type safety. 12 | - [Documentation and tutorials](https://httpc.dev/docs) 13 | - [Community](https://httpc.dev/discord) 14 | - [Issues and feature requests](https://httpc.dev/issues) 15 | 16 | ## Contributing 17 | All contributions on the documentation is very appreciated. 18 | 19 | To run the website locally, just clone the repository, install all dependencies and inside the `www` folder execute: 20 | ``` 21 | npm run dev 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-basic-client.ts: -------------------------------------------------------------------------------- 1 | import createClient from "@your-package/api-client"; 2 | 3 | const client = createClient({ 4 | endpoint: "http://api.domain.com" 5 | }); 6 | 7 | let result = await client.add(1, 2); 8 | // result: 3 9 | 10 | let message = await client.greet("Edith"); 11 | // message: "Hello Edith" 12 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-basic-server.ts: -------------------------------------------------------------------------------- 1 | function add(x: number, y: number) { 2 | return x + y; 3 | }; 4 | 5 | function greet(name: string) { 6 | return `Hello ${name}`; 7 | }; 8 | 9 | export default { 10 | add, 11 | greet 12 | } 13 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-context-client.ts: -------------------------------------------------------------------------------- 1 | import createClient, { QueryParam } from "@your-package/api-client"; 2 | 3 | const client = createClient({ 4 | endpoint: "http://api.domain.com", 5 | middleware: [ 6 | QueryParam("api-key", process.env.CLIENT_API_KEY) 7 | ] 8 | }); 9 | 10 | 11 | await client.createEditSession(); 12 | const change1 = await client.edit("#object-id", { 13 | color: "red" 14 | }); 15 | const change2 = await client.edit("#object-id", { 16 | fontSize: "16px" 17 | }); 18 | 19 | await client.undo(change2.id); 20 | await client.commit(); 21 | 22 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-context-server.ts: -------------------------------------------------------------------------------- 1 | import { httpCall, httpGroup, useUser, useContextProperty, Authenticated } from "@httpc/kit"; 2 | import { SessionMiddleware } from "./middlewares"; 3 | import { sessionManager, editor } from "./services"; 4 | 5 | 6 | const commit = httpCall(async () => { 7 | const user = useUser(); 8 | const changes = useContextProperty("changes"); 9 | 10 | if (changes) { 11 | await editor.persist(changes); 12 | useContextProperty("changes", []); 13 | } 14 | 15 | await sessionManager.clear(user.id); 16 | }); 17 | 18 | export default httpGroup( 19 | Authenticated(), 20 | SessionMiddleware(), 21 | { 22 | edit, 23 | undo, 24 | commit, 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-context-session.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useContextProperty, PassThroughMiddleware } from "@httpc/kit"; 2 | import { sessionManager } from "./services"; 3 | 4 | 5 | export function SessionMiddleware() { 6 | return PassThroughMiddleware(async () => { 7 | const { user } = useContext(); 8 | if (!user) { 9 | return; 10 | } 11 | 12 | const session = await sessionManager.retrieve(user.id); 13 | 14 | useContextProperty("sessionId", session.id); 15 | useContextProperty("changes", session.changes); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-middlewares-client.ts: -------------------------------------------------------------------------------- 1 | import createClient, { AuthHeader } from "@your-package/api-client"; 2 | 3 | const client = createClient({ 4 | endpoint: "http://api.domain.com", 5 | middleware: [ 6 | AuthHeader("Bearer", localStorage.getItem("access-token")) 7 | ] 8 | }); 9 | 10 | 11 | const profile = await client.getProfile(); 12 | 13 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-middlewares-server.ts: -------------------------------------------------------------------------------- 1 | import { httpCall, useUser, Authenticated, Cached } from "@httpc/kit"; 2 | import db from "./db"; 3 | 4 | const getProfile = httpCall( 5 | Authenticated(), 6 | Cached("1h"), 7 | async () => { 8 | const user = useUser(); 9 | 10 | return await db.select("profiles") 11 | .where("userId", user.id); 12 | } 13 | ) 14 | 15 | export default { 16 | getProfile 17 | } 18 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-services-client.ts: -------------------------------------------------------------------------------- 1 | import createClient, { AuthHeader } from "@your-service/api-client"; 2 | 3 | const client = createClient({ 4 | endpoint: "https://api.your-service.com", 5 | middlewares: [ 6 | AuthHeader("Bearer", localStorage.getItem("accessToken")) 7 | ] 8 | }); 9 | 10 | 11 | const tickets = await client.tickets.query({ assignedTo: "me" }); 12 | let firstTicket = tickets[0]; 13 | if (firstTicket) { 14 | firstTicket = await client.tickets.close(firstTicket.id, "resolved"); 15 | } 16 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-services-server.ts: -------------------------------------------------------------------------------- 1 | import { httpCall, Authenticated, Validate, useUser, useInjected, Optional, NotFoundError } from "@httpc/server"; 2 | import { TicketService } from "./services"; 3 | import { REASONS } from "./data"; 4 | 5 | const closeTicket = httpCall( 6 | Authenticated("role:admin"), 7 | Validate( 8 | String, 9 | Optional(reason => REASONS.include(reason)) 10 | ), 11 | async (ticketId: string, reason?: "resolved" | "rejected") => { 12 | const user = useUser(); 13 | const tickets = useInjected(TicketService); 14 | 15 | const ticket = await tickets.get(ticketId); 16 | if (!ticket) { 17 | throw new NotFoundError(); 18 | } 19 | 20 | return await tickets.close(ticketId, { 21 | by: user.id, 22 | reason: reason || "resolved" 23 | }); 24 | } 25 | ); 26 | 27 | export default { 28 | tickets: { 29 | close: closeTicket, 30 | query: // ... 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-transaction-client.ts: -------------------------------------------------------------------------------- 1 | import createClient, { AuthHeader } from "@your-service/api-client"; 2 | 3 | const client = createClient({ 4 | endpoint: "https://api.your-service.com", 5 | middlewares: [ 6 | AuthHeader("Bearer", localStorage.getItem("accessToken")) 7 | ] 8 | }); 9 | 10 | 11 | const offers = await client.offers.getLatest(); 12 | const order = await client.orders.create({ 13 | product: { 14 | id: offers.product.id, 15 | quantity: 1 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-transaction-server.ts: -------------------------------------------------------------------------------- 1 | import { httpCall, useUser, useTransaction } from "@httpc/kit"; 2 | import { OrderService, Inventory } from "./services"; 3 | import { OrderCreateSchema } from "./data"; 4 | 5 | const createOrder = httpCall( 6 | Authenticated(), 7 | Validate(OrderCreateSchema), 8 | async (orderData: OrderCreateSchema) => { 9 | const user = useUser(); 10 | 11 | const order = useTransaction(OrderService, Inventory, 12 | async (orders, inventory) => { 13 | await inventory.reserve(order.product.id, order.product.quantity); 14 | 15 | return await orders.create({ 16 | userId: user.id, 17 | productId: order.product.id, 18 | quantity: order.product.quantity, 19 | }); 20 | } 21 | ); 22 | 23 | return order; 24 | } 25 | ); 26 | 27 | export default { 28 | orders: { 29 | create: createOrder 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-vercel-api.ts: -------------------------------------------------------------------------------- 1 | import { createHttpCVercelAdapter } from "@httpc/adapter-vercel"; 2 | import calls from "../calls"; 3 | 4 | 5 | export default createHttpCVercelAdapter({ 6 | calls, 7 | log: "info" 8 | }); 9 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-vercel-client.tsx: -------------------------------------------------------------------------------- 1 | import { createClient, ClientDef } from "@httpc/client"; 2 | import { useQuery, useMutation } from "react-query"; 3 | import type calls from "../calls"; 4 | 5 | 6 | const client = createClient>(); 7 | 8 | export default function Home() { 9 | const posts = useQuery(["posts"], () => client.posts.getLatest()); 10 | const addLike = useMutation((postId: string) => client.posts.addLike(postId), { 11 | onSuccess: () => queryClient.invalidateQueries(["posts"]) 12 | }); 13 | 14 | return ( 15 |

16 | {posts.data.map(post => 17 |
18 |

{post.title}

19 | 20 |
21 | )} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/hero-vercel-server.ts: -------------------------------------------------------------------------------- 1 | import { httpCall, Validate } from "@httpc/kit"; 2 | import db from "./db"; 3 | 4 | 5 | async function getLatest() { 6 | return db.select("posts").take(10) 7 | .orderBy("created_at", "desc"); 8 | } 9 | 10 | const addLike = httpCall( 11 | Validate(String), 12 | async (postId: string) => { 13 | return db.update("posts").where("id", postId) 14 | .increase("likes", 1); 15 | } 16 | ); 17 | 18 | export default { 19 | posts: { 20 | getLatest, 21 | addLike, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/www/assets/code-snippets/posts.ts: -------------------------------------------------------------------------------- 1 | type Post = { 2 | id: string 3 | title: string 4 | } 5 | 6 | interface Comment { 7 | 8 | } 9 | 10 | interface Client { 11 | posts: { 12 | getAll(): Promise 13 | addComment(postId: string, comment: Comment): Promise 14 | } 15 | } 16 | 17 | function AuthHeader(...args: any) { } 18 | function createClient(options: any): Client { 19 | return null!; 20 | } 21 | 22 | 23 | 24 | async function main() { 25 | 26 | const client = createClient({ endpoint: "https://api.super-service.com" }); 27 | 28 | const posts = await client.posts.getAll(); 29 | const newComment = await client.posts.addComment({ 30 | text: "hello world" 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/www/assets/svg/httpc-brand.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /packages/www/astro.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import path from "path"; 3 | import { defineConfig } from "astro/config"; 4 | import mdx from "@astrojs/mdx"; 5 | import preact from "@astrojs/preact"; 6 | import sitemap from "@astrojs/sitemap"; 7 | import lottie from "astro-integration-lottie"; 8 | import compress from "astro-compress"; 9 | import rehypeLinkProcessor from "rehype-link-processor"; 10 | import codeTheme from "./src/code-theme"; 11 | import codeDecoration from "./src/plugins/codeBlockDecorator"; 12 | 13 | 14 | export default defineConfig({ 15 | site: "https://httpc.dev", 16 | server: { 17 | port: Number(process.env.PORT || 3000) 18 | }, 19 | integrations: [ 20 | codeDecoration(), 21 | mdx(), 22 | preact(), 23 | lottie(), 24 | compress({ 25 | html: { 26 | removeAttributeQuotes: false, 27 | }, 28 | css: false 29 | }), 30 | sitemap({ 31 | filter: page => ![ 32 | "/docs/" 33 | ].includes(new URL(page).pathname) 34 | }) 35 | ], 36 | markdown: { 37 | rehypePlugins: [ 38 | rehypeLinkProcessor(), 39 | ], 40 | shikiConfig: { 41 | theme: codeTheme 42 | } 43 | }, 44 | vite: { 45 | plugins: [ 46 | { 47 | name: "watch-assets", 48 | enforce: "post", 49 | handleHotUpdate({ file, server }) { 50 | if (file.includes("/assets/")) { 51 | server.ws.send({ 52 | type: "full-reload", 53 | path: "*" 54 | }); 55 | } 56 | } 57 | } 58 | ], 59 | resolve: { 60 | alias: { 61 | "~": path.resolve("./src").replaceAll("\\", "/") 62 | } 63 | } 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /packages/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@httpc/www", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev --host", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "deploy": "wrangler pages publish dist --project-name httpc-www", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "@astrojs/mdx": "^0.19.6", 16 | "@astrojs/preact": "^2.2.2", 17 | "@astrojs/rss": "^2.4.4", 18 | "@astrojs/sitemap": "^1.2.2", 19 | "@fontsource/mulish": "^4.5.14", 20 | "astro": "^2.10.9", 21 | "astro-compress": "^2.2.3", 22 | "astro-integration-lottie": "^0.2.3", 23 | "lottie-web": "^5.11.0", 24 | "preact": "^10.13.2", 25 | "shiki": "^0.14.1" 26 | }, 27 | "devDependencies": { 28 | "@nanostores/preact": "^0.4.0", 29 | "@types/hast": "^2.3.4", 30 | "@types/mdast": "^3.0.11", 31 | "@types/node": "^16.18.23", 32 | "astro-icon": "^0.8.0", 33 | "dotenv": "^16.0.3", 34 | "hastscript": "^7.2.0", 35 | "nanostores": "^0.8.0", 36 | "node-html-parser": "^6.1.5", 37 | "rehype-link-processor": "^1.0.3", 38 | "sass": "^1.62.0", 39 | "unified": "^10.1.2", 40 | "unist-util-visit": "^4.1.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/www/public/_headers: -------------------------------------------------------------------------------- 1 | # cloudflare pages bug -> force content-type 2 | /*.js 3 | Content-Type: application/javascript 4 | /*.woff2 5 | Content-Type: font/woff2 6 | /*.svg 7 | Content-Type: image/svg+xml 8 | -------------------------------------------------------------------------------- /packages/www/public/_redirects: -------------------------------------------------------------------------------- 1 | # Shortcuts 2 | # /discord https://discord.gg/invite-id 3 | /discuss https://github.com/giuseppelt/httpc/discussions 4 | /issues/create https://github.com/giuseppelt/httpc/issues/new/choose 5 | /issues https://github.com/giuseppelt/httpc/issues 6 | /contribute https://github.com/giuseppelt/httpc 7 | /repository https://github.com/giuseppelt/httpc 8 | -------------------------------------------------------------------------------- /packages/www/public/assets/external-link-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/www/public/assets/file-download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/www/public/assets/httpc-no-boilerplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/packages/www/public/assets/httpc-no-boilerplate.png -------------------------------------------------------------------------------- /packages/www/public/assets/httpc-typesafe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/packages/www/public/assets/httpc-typesafe.png -------------------------------------------------------------------------------- /packages/www/public/assets/iosevka-custom-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/packages/www/public/assets/iosevka-custom-bold.woff2 -------------------------------------------------------------------------------- /packages/www/public/assets/iosevka-custom.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/packages/www/public/assets/iosevka-custom.woff2 -------------------------------------------------------------------------------- /packages/www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppelt/httpc/5d000b1795b1263b5d6e6b66c532f11bac48d196/packages/www/public/favicon.ico -------------------------------------------------------------------------------- /packages/www/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/www/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://httpc.dev/sitemap-index.xml 5 | -------------------------------------------------------------------------------- /packages/www/src/code-theme.ts: -------------------------------------------------------------------------------- 1 | import { loadTheme } from "shiki"; 2 | 3 | const theme = await loadTheme("themes/nord.json"); 4 | 5 | // change comment color: make lighter 6 | const colorComment = "#818ca2"; 7 | theme.settings.find(x => x.name === "Comment")!.settings.foreground = colorComment; 8 | theme.settings.find(x => x.name === "Punctuation Definition Comment")!.settings.foreground = colorComment; 9 | 10 | export default theme; 11 | -------------------------------------------------------------------------------- /packages/www/src/components/AdapterList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getDocs } from "../content"; 3 | 4 | const items = (await getDocs()).filter(x => x.slug.startsWith("adapters/")); 5 | --- 6 | 7 |
    8 | { 9 | items.map(x => ( 10 |
  • 11 |

    12 | {x.data.title} 13 |

    14 | {x.data.description} 15 |
  • 16 | )) 17 | } 18 |
19 | -------------------------------------------------------------------------------- /packages/www/src/components/ArticleNavigation.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Icon from "astro-icon"; 3 | import { getSidebar } from "../sidebar"; 4 | 5 | export interface Props { 6 | currentPage: string; 7 | page: "docs" | "blog"; 8 | } 9 | 10 | const { page, currentPage } = Astro.props; 11 | 12 | const { prev, next } = (await getSidebar(page)).getNavLinks(`/${page}/${currentPage}`); 13 | --- 14 | 15 | 43 | -------------------------------------------------------------------------------- /packages/www/src/components/Aside.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon"; 3 | 4 | const ICONS = { 5 | info: "ci:info-square", 6 | message: "eva:message-square-fill", 7 | magic: "bi:stars", 8 | warning: "ph:warning-fill", 9 | guide: "mdi:drive-document", 10 | question: "mdi:question-mark-box", 11 | }; 12 | 13 | export type AsideIcons = keyof typeof ICONS; 14 | 15 | export interface Props { 16 | type: keyof typeof VARIANTS; 17 | title?: string | false; 18 | icon?: AsideIcons; 19 | small?: boolean; 20 | } 21 | 22 | const VARIANTS = { 23 | info: { 24 | title: "Note", 25 | icon: "info", 26 | }, 27 | tip: { 28 | title: "Tip", 29 | icon: "magic", 30 | }, 31 | warn: { 32 | title: "Warning", 33 | icon: "warning", 34 | }, 35 | }; 36 | 37 | const TYPES = Object.keys(VARIANTS); 38 | 39 | const { type = "info", small, title: _title, icon: _icon } = Astro.props; 40 | if (!TYPES.includes(type)) { 41 | throw new Error(`Aside type "${type}" invalid(available: "${TYPES.join('", "')}")`); 42 | } 43 | 44 | const title = _title === false ? false : _title || VARIANTS[type].title; 45 | const icon = (ICONS as any)[_icon || VARIANTS[type].icon]; 46 | if (!icon) { 47 | console.warn("Aside icon '%s' not defined", icon); 48 | } 49 | --- 50 | 51 | 64 | -------------------------------------------------------------------------------- /packages/www/src/components/BrandIcon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Svg from "./Svg.astro"; 3 | --- 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/www/src/components/CodeBlock.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code } from "astro/components"; 3 | import { readFile } from "../utils"; 4 | import theme from "../code-theme"; 5 | 6 | export interface Props { 7 | title?: string; 8 | src: string; 9 | language?: any; 10 | } 11 | 12 | const { title, src, language = src.split(".").pop() } = Astro.props; 13 | const code = (await readFile(src, "")) || ""; 14 | --- 15 | 16 |
17 | {title &&

{title}

} 18 | 19 |
20 | 21 | 49 | -------------------------------------------------------------------------------- /packages/www/src/components/IconMenu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon"; 3 | 4 | interface Props { 5 | container: string; 6 | className?: string; 7 | } 8 | 9 | const { container, className } = Astro.props; 10 | --- 11 | 12 | 13 | 14 | 39 | -------------------------------------------------------------------------------- /packages/www/src/components/Navbar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BrandIcon from "./BrandIcon.astro"; 3 | import IconMenu from "./IconMenu.astro"; 4 | import NavbarIcons from "./NavbarIcons.astro"; 5 | import NavbarLinks from "./NavbarLinks.astro"; 6 | 7 | interface Props { 8 | currentPage: string; 9 | width?: "page" | "docs" | "blog"; 10 | } 11 | 12 | const { currentPage, width = "page" } = Astro.props; 13 | --- 14 | 15 | 33 | 34 | 47 | -------------------------------------------------------------------------------- /packages/www/src/components/NavbarIcons.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon"; 3 | --- 4 | 5 |
6 | 7 | 8 |
9 | 10 | 23 | -------------------------------------------------------------------------------- /packages/www/src/components/NavbarLinks.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | currentPage: string; 4 | } 5 | 6 | const { currentPage = "/" } = Astro.props; 7 | 8 | function isSection(prefix: string) { 9 | return currentPage.startsWith(prefix); 10 | } 11 | --- 12 | 13 | 18 | -------------------------------------------------------------------------------- /packages/www/src/components/PackageButton.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon"; 3 | import { Package, packageMeta } from "../constants"; 4 | 5 | export interface Props { 6 | href: string; 7 | title: Package; 8 | description?: string; 9 | } 10 | 11 | const { href, title, description = packageMeta[title].description } = Astro.props; 12 | const { icon } = packageMeta[title]; 13 | --- 14 | 15 | 16 |
17 | {icon && } 18 |
19 |

{title}

20 |

{description}

21 |
22 |
23 |
24 | 25 | 52 | -------------------------------------------------------------------------------- /packages/www/src/components/PackageCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon"; 3 | import { Package, packageMeta } from "../constants"; 4 | 5 | export interface Props { 6 | href: string; 7 | title: Package; 8 | description?: string; 9 | } 10 | 11 | const { href, title, description = packageMeta[title].description } = Astro.props; 12 | const { icon } = packageMeta[title]; 13 | --- 14 | 15 | 16 |
17 |
18 | {icon && } 19 |
20 |

{title}

21 |

{description}

22 |
23 |
24 | { 25 | Astro.slots.has("default") && ( 26 | <> 27 |
28 |
29 | 30 |
31 | 32 | ) 33 | } 34 |
35 |
36 | -------------------------------------------------------------------------------- /packages/www/src/components/PostList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ContentItemBlog } from "../content"; 3 | import TagList from "./TagList.astro"; 4 | 5 | interface Props { 6 | items: ContentItemBlog[]; 7 | showTags?: boolean; 8 | className?: string; 9 | } 10 | 11 | const { items, className, showTags = true } = Astro.props; 12 | --- 13 | 14 |
    15 | { 16 | items.map(x => ( 17 |
  • 18 | 23 |
    24 | 25 | {x.data.title} 26 | 27 | {showTags && ({ text: x, href: `/blog/tags/${x}` }))} />} 28 |

    {x.data.summary || x.data.description}

    29 |
    30 |
  • 31 | )) 32 | } 33 |
34 | -------------------------------------------------------------------------------- /packages/www/src/components/Tag.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | className?: string; 4 | prefix?: string; 5 | text: string; 6 | href?: string; 7 | } 8 | 9 | const { className, prefix, text, href } = Astro.props; 10 | --- 11 | 12 | { 13 | href ? ( 14 | 15 | {prefix && {prefix}} 16 | {text} 17 | 18 | ) : ( 19 | 20 | {prefix && {prefix}} 21 | {text} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/www/src/components/TagList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Tag from "./Tag.astro"; 3 | 4 | interface TagItem { 5 | text: string; 6 | href?: string; 7 | } 8 | 9 | export interface Props { 10 | items?: (string | TagItem)[]; 11 | className?: string; 12 | } 13 | 14 | const { className, items: _items } = Astro.props; 15 | const items = _items?.map(x => (typeof x === "string" ? { text: x } : x)); 16 | --- 17 | 18 |
19 | {items && items.map(x => )} 20 | 21 |
22 | -------------------------------------------------------------------------------- /packages/www/src/components/TutorialList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getDocs } from "../content"; 3 | 4 | const items = (await getDocs()).filter(x => x.slug.startsWith("tutorials/")); 5 | --- 6 | 7 |
    8 | { 9 | items.map(x => ( 10 |
  • 11 |

    12 | {x.data.title} 13 |

    14 | {x.data.description} 15 |
  • 16 | )) 17 | } 18 |
19 | -------------------------------------------------------------------------------- /packages/www/src/components/tabs/InstallPackage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code } from "astro/components"; 3 | import PackageManagerTabs from "./PackageManagerTabs.astro"; 4 | import theme from "../../code-theme"; 5 | 6 | interface Props { 7 | package: string; 8 | } 9 | 10 | const { package: packageName } = Astro.props; 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/www/src/components/tabs/PackageManagerTabs.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Tabs from "./Tabs"; 3 | --- 4 | 5 | 6 | npm 7 | pnpm 8 | yarn 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/www/src/components/tabs/store.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from "@nanostores/preact"; 2 | import { atom, map } from "nanostores"; 3 | import { useState } from "preact/hooks"; 4 | 5 | type TabStore = { 6 | [key: string]: { 7 | curr: string; 8 | }; 9 | }; 10 | 11 | export const tabId = atom(0); 12 | export const tabStore = map({}); 13 | 14 | export function genTabId() { 15 | const id = tabId.get(); 16 | tabId.set(id + 1); 17 | return id; 18 | } 19 | 20 | export function useTabState(initialCurr: string, storeKey?: string): [string, (curr: string) => void] { 21 | const $tabStore = useStore(tabStore); 22 | 23 | const localState = useState(initialCurr); 24 | if (!storeKey) { 25 | return localState; 26 | } 27 | 28 | const curr = $tabStore[storeKey]?.curr ?? initialCurr; 29 | const setCurr = (curr: string) => { 30 | if (storeKey) { 31 | tabStore.setKey(storeKey, { curr }); 32 | } else { 33 | throw new Error("[Tabs] Looks like a sharedStore key is no longer present on your tab view! If your store key is dynamic, consider using a static string value instead."); 34 | } 35 | } 36 | 37 | return [curr, setCurr]; 38 | } 39 | -------------------------------------------------------------------------------- /packages/www/src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Package = 3 | | "@httpc/server" 4 | | "@httpc/kit" 5 | | "@httpc/client" 6 | | "@httpc/cli" 7 | 8 | 9 | export type PackageMeta = Readonly<{ 10 | icon: string 11 | description: string 12 | }> 13 | 14 | export const packageMeta: Record = { 15 | "@httpc/server": { 16 | icon: "ri:server-fill", 17 | description: "The httpc core component allowing function calls over the standard http protocol", 18 | }, 19 | "@httpc/kit": { 20 | icon: "ion:construct", 21 | description: "Rich toolbox of builtin components to manage common use cases and business concerns", 22 | }, 23 | "@httpc/client": { 24 | icon: "ic:baseline-outbound", 25 | description: "Typed interface used by consumers to interact safely with functions exposed by an httpc server", 26 | }, 27 | "@httpc/cli": { 28 | icon: "ri:terminal-box-fill", 29 | description: "Commands to setup a project, generate clients, manage versioning and help with common tasks", 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /packages/www/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { z, defineCollection } from "astro:content"; 2 | 3 | const docs = defineCollection({ 4 | schema: z.object({ 5 | draft: z.boolean().optional(), 6 | title: z.string(), 7 | shortTitle: z.string().optional(), 8 | status: z.enum(["working in progress"]).optional(), 9 | description: z.string().optional(), 10 | summary: z.string().optional(), 11 | }) 12 | }); 13 | 14 | export const BlogTags = ([ 15 | "announcements", 16 | "ergonomics", 17 | "patterns", 18 | "release", 19 | "typescript", 20 | "type-safety", 21 | ] as const); 22 | 23 | const blog = defineCollection({ 24 | schema: z.object({ 25 | draft: z.boolean().optional(), 26 | title: z.string(), 27 | shortTitle: z.string().optional(), 28 | description: z.string().optional(), 29 | summary: z.string().optional(), 30 | tags: z.array(z.enum(BlogTags)).min(1), 31 | publishedAt: z.date(), 32 | }) 33 | }); 34 | 35 | export const collections = { 36 | docs, 37 | blog, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/adapters/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adapters 3 | description: Adapters to host httpc API inside environments like Vercel, Netlify functions or AWS Lambda 4 | --- 5 | 6 | import AdapterList from "~/components/AdapterList.astro" 7 | 8 | An adapter allows to host a full **httpc** api inside serverless providers or other environments. 9 | 10 | ## Adapter list 11 | 12 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/adapters/netlify.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify Adapter 3 | shortTitle: Netlify 4 | description: Host an httpc api inside Netlify functions 5 | --- 6 | 7 | import Aside from "~/components/Aside.astro"; 8 | 9 | // TODO 10 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/adapters/next.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Next Adapter 3 | shortTitle: Next 4 | description: Integrate an httpc api with the Next framework and host it on Vercel 5 | --- 6 | 7 | import Aside from "~/components/Aside.astro"; 8 | 9 | // TODO 10 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/adapters/vercel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vercel Adapter 3 | shortTitle: Vercel 4 | description: Host an httpc api with Vercel serverless functions 5 | status: working in progress 6 | --- 7 | 8 | import Aside from "~/components/Aside.astro"; 9 | 10 | ## Quick usage 11 | Create a `[[...slug]].ts` file inside your `pages/api` directory. 12 | 13 | ```ts 14 | import { createHttpCVercelAdapter } from "@httpc/adapter-vercel"; 15 | import calls from "./_calls"; 16 | 17 | export default createHttpCVercelAdapter({ 18 | calls: 19 | log: true 20 | }) 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/client-generation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client generation 3 | shortTitle: Generation 4 | description: description 5 | --- 6 | 7 | 8 | TODO 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/client-publishing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client publishing 3 | shortTitle: Publishing 4 | description: description 5 | --- 6 | 7 | 8 | TODO 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/client-usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client usage 3 | shortTitle: Usage 4 | description: description 5 | --- 6 | 7 | 8 | TODO 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/httpc-family.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: httpc family 3 | description: description 4 | --- 5 | 6 | import Aside from "~/components/Aside.astro"; 7 | 8 | //TODO 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/kit-authorization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authorization 3 | description: description 4 | --- 5 | 6 | import Aside from "~/components/Aside.astro"; 7 | 8 | TODO 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/kit-extending.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extending 3 | description: description 4 | --- 5 | import Aside from "~/components/Aside.astro"; 6 | 7 | TODO 8 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/kit-logging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logging 3 | description: description 4 | --- 5 | 6 | import Aside from "~/components/Aside.astro"; 7 | 8 | TODO 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/package-httpc-cli.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@httpc/cli" 3 | description: package 4 | --- 5 | 6 | TODO 7 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/package-httpc-client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@httpc/client" 3 | description: package 4 | --- 5 | 6 | TODO 7 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/package-httpc-kit.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@httpc/kit" 3 | description: package 4 | --- 5 | 6 | TODO 7 | 8 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/package-httpc-server.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@httpc/server" 3 | description: package 4 | --- 5 | TODO 6 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/server-request-context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request context 3 | description: description 4 | --- 5 | 6 | 7 | TODO 8 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/server-testing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Testing 3 | description: description 4 | --- 5 | 6 | 7 | TODO 8 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/tutorials/guide-project-organization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Organizing your project 3 | description: description 4 | draft: true 5 | --- 6 | 7 | TODO 8 | 9 | -------------------------------------------------------------------------------- /packages/www/src/content/docs/tutorials/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | description: httpc guides and tutorials 4 | --- 5 | import TutorialList from "~/components/TutorialList.astro"; 6 | 7 | ## Tutorial list 8 | 9 | -------------------------------------------------------------------------------- /packages/www/src/content/index.ts: -------------------------------------------------------------------------------- 1 | import { getCollection, CollectionEntry } from "astro:content"; 2 | 3 | export type ContentItemDocs = CollectionEntry<"docs"> 4 | export type ContentItemBlog = CollectionEntry<"blog"> 5 | export { BlogTags } from "./config"; 6 | 7 | 8 | export type ContentItem = 9 | | ContentItemDocs 10 | | ContentItemBlog 11 | 12 | export async function getDocs() { 13 | return await getCollection("docs", x => !x.data.draft); 14 | } 15 | 16 | export async function getPosts() { 17 | return (await getCollection("blog", x => !x.data.draft)).sort((x, y) => { 18 | // sort from newest to oldest 19 | return y.data.publishedAt.getTime() - x.data.publishedAt.getTime(); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/www/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // interface Frontmatter { 5 | // title: string 6 | // shortTitle?: string 7 | // description?: string 8 | // summary?: string 9 | // draft?: boolean 10 | // status?: string 11 | // tags?: string[] 12 | // publishedAt?: Date 13 | // } 14 | -------------------------------------------------------------------------------- /packages/www/src/icons/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/www/src/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/www/src/layouts/Docs.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { ContentItemDocs } from "../content"; 3 | import Layout from "./Layout.astro"; 4 | import Rightbar from "../components/Rightbar.astro"; 5 | import Sidebar from "../components/Sidebar.astro"; 6 | import ArticleNavigation from "../components/ArticleNavigation.astro"; 7 | import TagList from "../components/TagList.astro"; 8 | import Tag from "../components/Tag.astro"; 9 | 10 | interface Props { 11 | entry: ContentItemDocs; 12 | } 13 | 14 | const { entry } = Astro.props; 15 | const meta = entry.data; 16 | 17 | const currentPage = entry.slug; 18 | const { Content, headings } = await entry.render(); 19 | 20 | // ensure no h1 is present 21 | if (headings.some(x => x.depth === 1)) { 22 | throw new Error(`H1 not allowed in docs(${currentPage})`); 23 | } 24 | --- 25 | 26 | 27 |
28 | 29 |
30 |
31 | {meta &&

{meta.title}

} 32 | { 33 | meta && meta.status && ( 34 | 35 | 36 | 37 | ) 38 | } 39 |
40 | 41 | 42 |
43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /packages/www/src/layouts/Redirect.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | to: string; 4 | fallbackText?: string; 5 | } 6 | 7 | const { to, fallbackText = "Redirecting..." } = Astro.props; 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/www/src/pages/404.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ~/layouts/Page.astro 3 | title: Not Found 4 | description: Nothing to look for here, you need to go Home 5 | --- 6 | 7 |
8 | You Lost
9 | But don't worry
10 | 11 |
12 | You can always come [home](/) 13 | -------------------------------------------------------------------------------- /packages/www/src/pages/blog/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getPosts } from "../../content"; 3 | import Layout from "../../layouts/Blog.astro"; 4 | 5 | export async function getStaticPaths() { 6 | const items = (await getPosts()).map(entry => ({ 7 | params: { slug: entry.slug }, 8 | props: { entry }, 9 | })); 10 | 11 | return items; 12 | } 13 | 14 | const { entry } = Astro.props; 15 | --- 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/www/src/pages/blog/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | import Sidebar from "../../components/Sidebar.astro"; 4 | import PostList from "../../components/PostList.astro"; 5 | import { getPosts } from "../../content"; 6 | import { Icon } from "astro-icon"; 7 | 8 | const posts = await getPosts(); 9 | --- 10 | 11 | 17 |
18 | 19 |
20 |
21 |

Posts

22 | 23 |
24 | 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /packages/www/src/pages/blog/tags/[tag].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { BlogTags, getPosts } from "../../../content"; 3 | import Layout from "../../../layouts/Layout.astro"; 4 | import Sidebar from "../../../components/Sidebar.astro"; 5 | import PostList from "../../../components/PostList.astro"; 6 | 7 | export function getStaticPaths() { 8 | return BlogTags.map(tag => ({ 9 | params: { tag }, 10 | })); 11 | } 12 | 13 | const { tag } = Astro.params; 14 | const items = (await getPosts()).filter(x => x.data.tags.includes(tag as any)); 15 | --- 16 | 17 | 18 |
19 | 20 |
21 |
22 |

Posts / {tag}

23 |

{items.length} {items.length > 1 ? "posts" : "post"} tagged

24 |
25 | 26 | 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /packages/www/src/pages/changelog.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ~/layouts/Page.astro 3 | title: Changelog 4 | description: httpc development changelog and releases 5 | --- 6 | 7 | ## Upcoming 8 | - feat(kit): caching hooks 9 | - feat(kit): caching redis provider 10 | - feat(server): updated request processor with pre/post processors 11 | - feat(server): global error handler configurable 12 | - feat(server): updated server logging with errors trace points 13 | 14 | 15 | ## v0.1.0 First release 16 | This is the first release of the httpc framework. This release includes: 17 | - v0.1.0 **`@httpc/server`** -- The core httpc component, the server handles httpc calls 18 | - v0.1.0 **`@httpc/kit`** -- Toolbox with many builtin components addressing authentication, validation, caching... 19 | - v0.1.0 **`@httpc/client`** -- The client tailored to make httpc function calls with natural javascript 20 | - v0.1.0 **`@httpc/cli`** -- CLI to automate common tasks like client generation or testing 21 | - v0.1.0 **`create-httpc`** -- Create a new httpc project from a template 22 | 23 | ### Pre-release 24 | Adapters are released with a prerelease tag. 25 | - v0.0.1-pre **`@httpc/adapter-vercel`** -- Host on Vercel serverless function with or without Next integration 26 | - v0.0.1-pre **`@httpc/adapter-netlify`** -- Host on Netlify functions 27 | 28 | ### Documentation 29 | The home site with documentation is live at [httpc.dev](https://httpc.dev). 30 | -------------------------------------------------------------------------------- /packages/www/src/pages/discord.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ~/layouts/Page.astro 3 | title: Discord community 4 | description: httpc discord community 5 | --- 6 | 7 | A discord community around **httpc** is not yet active. 8 |
9 | If you interested in joining such community, please leave a message in the [external:Github discussions](/discuss). 10 |
11 | 12 | The community will focus on: 13 | - httpc releases, architecture and behind the scene inner workings 14 | - guides and tutorials with full project examples based on httpc 15 | - support and clarification on existing httpc features 16 | - feature requests 17 | - general javascript/typescript API design 18 | 19 |
20 | If you're looking for something more or different, please specify it in message at [external:Github discussions](/discuss). 21 | -------------------------------------------------------------------------------- /packages/www/src/pages/docs/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getDocs } from "../../content"; 3 | import Layout from "../../layouts/Docs.astro"; 4 | 5 | export async function getStaticPaths() { 6 | const items = (await getDocs()).map(entry => ({ 7 | params: { slug: entry.slug }, 8 | props: { entry }, 9 | })); 10 | 11 | return items; 12 | } 13 | 14 | const { entry } = Astro.props; 15 | --- 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/www/src/pages/docs/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Redirect from "../../layouts/Redirect.astro"; 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/www/src/pages/rss.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from "@astrojs/rss"; 2 | import { APIContext } from "astro"; 3 | import { getPosts } from "../content"; 4 | 5 | 6 | export async function get(context: APIContext) { 7 | const posts = await getPosts(); 8 | 9 | return rss({ 10 | title: "httpc blog", 11 | description: "Articles and best practices about httpc framework, node, typescript and API design", 12 | site: context.site!.toString(), 13 | stylesheet: "/rss-style.xsl", 14 | customData: "en-us", 15 | items: posts.map(x => ({ 16 | link: "https://httpc.dev/blog/" + x.slug, 17 | title: x.data.title, 18 | pubDate: x.data.publishedAt, 19 | description: x.data.summary || x.data.description, 20 | content: (x.data.summary || x.data.description) 21 | ? ` 22 |

${x.data.summary || x.data.description}

23 |

Read more

24 | ` : undefined 25 | })) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/www/src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | 3 | export function getGithubEditLink(url: string) { 4 | return `https://github.com/giuseppelt/httpc/blob/master/packages/www/src/pages${url}.mdx`; 5 | } 6 | 7 | 8 | export async function readFile(path: string, fallback?: string) { 9 | const content = await fs.readFile(path, "utf8").catch(err => { 10 | if (import.meta.env.DEV) { 11 | console.warn("Cant read %s", path, err); 12 | return fallback; 13 | } 14 | 15 | throw err; 16 | }); 17 | 18 | return content; 19 | } 20 | -------------------------------------------------------------------------------- /packages/www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "importsNotUsedAsValues": "remove", 6 | "jsx": "react-jsx", 7 | "jsxImportSource": "preact" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - templates/* 4 | -------------------------------------------------------------------------------- /scripts/generateTemplateCatalogue.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import type { TemplateInfo } from "../packages/cli/src/commands/create"; 3 | 4 | 5 | async function main() { 6 | const items = await fs.readdir("templates"); 7 | 8 | const templateDirs = []; 9 | // filter directories 10 | for (const item of items) { 11 | if ((await fs.stat(`templates/${item}`)).isDirectory()) { 12 | templateDirs.push(item); 13 | } 14 | } 15 | 16 | const templates: TemplateInfo[] = []; 17 | for (const dir of templateDirs) { 18 | const packageJson = JSON.parse(await fs.readFile(`templates/${dir}/package.json`, "utf8")) as any; 19 | templates.push({ 20 | id: dir, 21 | name: packageJson.name, 22 | title: packageJson.title, 23 | description: packageJson.description, 24 | type: packageJson.templateType || "server", 25 | path: `templates/${dir}`, 26 | version: packageJson.version, 27 | }); 28 | } 29 | 30 | await fs.writeFile("templates/templates.json", JSON.stringify(templates, null, 4), "utf8"); 31 | console.log("Template catalogue generated: %d templates", templates.length); 32 | console.log(templates.map(x => `- ${x.id} (${x.type})`).join("\n")); 33 | } 34 | 35 | 36 | main(); 37 | 38 | -------------------------------------------------------------------------------- /scripts/prerelease.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | 3 | 4 | const packages = [ 5 | "server", 6 | "kit", 7 | "cli", 8 | "client", 9 | "adapter-cloudflare" 10 | ] 11 | 12 | type PackageJson = { 13 | version: string 14 | } 15 | 16 | const TIMESTAMP = new Date().toISOString().substring(0, 19) 17 | .replaceAll("-", "") 18 | .replaceAll(":", "") 19 | .replaceAll("T", ""); 20 | 21 | const VERSION_TAG = "-pre"; 22 | 23 | async function main() { 24 | const released: string[] = []; 25 | 26 | for (const p of packages) { 27 | const path = `packages/${p}/package.json`; 28 | const json = JSON.parse(await fs.readFile(path, "utf8")) as PackageJson; 29 | if (json.version.includes(VERSION_TAG)) { 30 | json.version = json.version.substring(0, json.version.indexOf(VERSION_TAG) + VERSION_TAG.length) + TIMESTAMP; 31 | released.push(p); 32 | 33 | await fs.writeFile(path, JSON.stringify(json, undefined, 4), "utf8"); 34 | } 35 | } 36 | 37 | if (released.length === 0) { 38 | console.log("No package to prerelease"); 39 | process.exit(1); 40 | } 41 | 42 | console.log("PreReleased with timestamp: " + TIMESTAMP); 43 | for (const release of released) { 44 | console.log("- %s", release); 45 | } 46 | } 47 | 48 | main(); 49 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "lib": [ 6 | "ESNext" 7 | ] 8 | }, 9 | "include": [ 10 | "*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /templates/client/index.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | 4 | const { createClient, AuthHeader, Header, QueryParam, HttpCClientError } = require("@httpc/client"); 5 | const metadata = require("./types/metadata.json"); 6 | 7 | exports.AuthHeader = AuthHeader; 8 | exports.Header = Header; 9 | exports.QueryParam = QueryParam; 10 | exports.HttpCClientError = HttpCClientError; 11 | exports.createClient = function (options) { 12 | return createClient({ ...options, metadata }); 13 | }; 14 | -------------------------------------------------------------------------------- /templates/client/index.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | type HttpCallPipelineDefinition = T 3 | } 4 | 5 | import type { HttpCClientOptions, HttpCTypedClient, ClientDef } from "@httpc/client"; 6 | export type { HttpCClientMiddleware, JsonSafeType } from "@httpc/client"; 7 | export { AuthHeader, Header, QueryParam, HttpCClientError } from "@httpc/client"; 8 | 9 | import type api from "./types"; 10 | 11 | export type ClientOptions = HttpCClientOptions; 12 | export type ClientApi = HttpCTypedClient & ClientDef; 13 | 14 | export function createClient(options?: ClientOptions): ClientApi; 15 | -------------------------------------------------------------------------------- /templates/client/index.mjs: -------------------------------------------------------------------------------- 1 | import { createClient as _createClient } from "@httpc/client"; 2 | import metadata from "./types/metadata.json"; 3 | 4 | export { AuthHeader, Header, QueryParam, HttpCClientError } from "@httpc/client"; 5 | export function createClient(options) { 6 | return _createClient({ ...options, metadata }); 7 | } 8 | -------------------------------------------------------------------------------- /templates/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@httpc/template-client", 3 | "templateType": "client", 4 | "version": "0.1.0", 5 | "main": "index.cjs", 6 | "module": "index.mjs", 7 | "license": "MIT", 8 | "sideEffects": false, 9 | "dependencies": { 10 | "@httpc/client": "^0.1.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/client/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export { } -------------------------------------------------------------------------------- /templates/kit-blank/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@httpc/template-kit-blank", 3 | "description": "Black template based on @httpc/kit", 4 | "title": "Kit blank", 5 | "templateType": "server", 6 | "version": "0.1.0", 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node dist/index.js", 11 | "dev": "ts-node-dev --transpile-only src/index.ts", 12 | "build": "tsc", 13 | "generate:client": "httpc client generate" 14 | }, 15 | "engines": { 16 | "node": ">=16.13" 17 | }, 18 | "dependencies": { 19 | "@httpc/kit": "^0.1.0", 20 | "reflect-metadata": "^0.1.13", 21 | "tsyringe": "^4.8.0" 22 | }, 23 | "devDependencies": { 24 | "@httpc/cli": "^0.1.0", 25 | "@types/node": "^16.18.23", 26 | "ts-node-dev": "^2.0.0", 27 | "typescript": "^5.4.5" 28 | }, 29 | "httpc": { 30 | "name": "client", 31 | "entry": "src/calls/index.ts", 32 | "dest": "client" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/kit-blank/src/calls/index.ts: -------------------------------------------------------------------------------- 1 | import { httpCall } from "@httpc/kit"; 2 | 3 | 4 | const echo = httpCall(async (message: string) => { 5 | return message; 6 | }); 7 | 8 | export default { 9 | echo, 10 | } 11 | -------------------------------------------------------------------------------- /templates/kit-blank/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | global { 4 | interface IHttpCContext { 5 | // custom context field here 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/kit-blank/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "@httpc/kit"; 2 | import calls from "./calls"; 3 | 4 | 5 | const PORT = Number(process.env.PORT) || 3000; 6 | 7 | const app = new Application({ 8 | port: PORT, 9 | calls, 10 | coors: true, 11 | middlewares: [ 12 | 13 | ], 14 | }); 15 | 16 | app.initialize().then(() => { 17 | app.start(); 18 | }); 19 | -------------------------------------------------------------------------------- /templates/kit-blank/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "target": "es2022", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /templates/server-blank/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@httpc/template-server-blank", 3 | "description": "Black template based on @httpc/server", 4 | "title": "Server blank", 5 | "templateType": "server", 6 | "version": "0.1.0", 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node dist/index.js", 11 | "dev": "ts-node-dev --transpile-only src/index.ts", 12 | "build": "tsc", 13 | "generate:client": "httpc client generate" 14 | }, 15 | "engines": { 16 | "node": ">=16.13" 17 | }, 18 | "dependencies": { 19 | "@httpc/server": "^0.1.0" 20 | }, 21 | "devDependencies": { 22 | "@httpc/cli": "^0.1.0", 23 | "@types/node": "^16.18.23", 24 | "ts-node-dev": "^2.0.0", 25 | "typescript": "^5.4.5" 26 | }, 27 | "httpc": { 28 | "name": "client", 29 | "entry": "src/calls/index.ts", 30 | "dest": "client" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /templates/server-blank/src/calls/index.ts: -------------------------------------------------------------------------------- 1 | 2 | function echo(message: string) { 3 | return message; 4 | } 5 | 6 | function greet(name: string) { 7 | return `Hello ${name}`; 8 | } 9 | 10 | 11 | export default { 12 | echo, 13 | greet, 14 | } 15 | -------------------------------------------------------------------------------- /templates/server-blank/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | global { 4 | interface IHttpCContext { 5 | // custom context field here 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/server-blank/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createHttpCServer } from "@httpc/server"; 2 | import calls from "./calls"; 3 | 4 | 5 | const PORT = Number(process.env.PORT) || 3000; 6 | 7 | const server = createHttpCServer({ 8 | calls 9 | }); 10 | 11 | 12 | server.listen(PORT); 13 | console.log("Server started: http//localhost:%d", PORT); 14 | -------------------------------------------------------------------------------- /templates/server-blank/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "target": "es2022", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /templates/templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "client", 4 | "name": "@httpc/template-client", 5 | "type": "client", 6 | "path": "templates/client", 7 | "version": "0.1.0" 8 | }, 9 | { 10 | "id": "kit-blank", 11 | "name": "@httpc/template-kit-blank", 12 | "title": "Kit blank", 13 | "description": "Black template based on @httpc/kit", 14 | "type": "server", 15 | "path": "templates/kit-blank", 16 | "version": "0.1.0" 17 | }, 18 | { 19 | "id": "server-blank", 20 | "name": "@httpc/template-server-blank", 21 | "title": "Server blank", 22 | "description": "Black template based on @httpc/server", 23 | "type": "server", 24 | "path": "templates/server-blank", 25 | "version": "0.1.0" 26 | } 27 | ] -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "allowJs": false, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "noFallthroughCasesInSwitch": false, 10 | "isolatedModules": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "useUnknownInCatchVariables": false, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "module": "ESNext", 19 | "moduleResolution": "node", 20 | "sourceMap": false, 21 | "noEmit": true 22 | }, 23 | "watchOptions": { 24 | "watchFile": "useFsEvents", 25 | "watchDirectory": "useFsEvents" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./packages/client/tsconfig.dev.cjs.json" 6 | }, 7 | { 8 | "path": "./packages/client/tsconfig.dev.esm.json" 9 | }, 10 | { 11 | "path": "./packages/server/tsconfig.dev.cjs.json" 12 | }, 13 | { 14 | "path": "./packages/server/tsconfig.dev.esm.json" 15 | }, 16 | { 17 | "path": "./packages/cli/tsconfig.json" 18 | }, 19 | { 20 | "path": "./packages/kit/tsconfig.dev.json" 21 | }, 22 | { 23 | "path": "./packages/adapter-vercel/tsconfig.build.json" 24 | }, 25 | { 26 | "path": "./packages/adapter-netlify/tsconfig.build.json" 27 | }, 28 | { 29 | "path": "./packages/adapter-cloudflare/tsconfig.build.json" 30 | } 31 | ] 32 | } 33 | --------------------------------------------------------------------------------