From fb750a7c1ba9f7edff9cf087de48dff4e3d3c28f Mon Sep 17 00:00:00 2001 From: Leo Lamprecht Date: Mon, 6 May 2024 12:21:40 +0200 Subject: [PATCH] Added Hono client --- .github/actions/bump-version/action.yml | 56 ++++++++++++ .github/dependabot.yml | 21 +++++ .github/workflows/publish-experimental.yml | 51 +++++++++++ .github/workflows/publish-manually.yml | 71 +++++++++++++++ .github/workflows/remove-experimental.yml | 28 ++++++ .github/workflows/update.yml | 23 +++++ .github/workflows/validate.yml | 73 +++++++++++++++ .gitignore | 5 + .husky/pre-commit | 1 + .vscode/extensions.json | 3 + .vscode/settings.json | 12 +++ biome.json | 21 +++++ bun.lockb | Bin 0 -> 81129 bytes bunfig.toml | 7 ++ package.json | 53 +++++++++++ src/index.ts | 59 ++++++++++++ tests/env.d.ts | 20 ++++ tests/integration/index.test.ts | 101 +++++++++++++++++++++ tests/preload.ts | 30 ++++++ tsconfig.json | 30 ++++++ 20 files changed, 665 insertions(+) create mode 100644 .github/actions/bump-version/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/publish-experimental.yml create mode 100644 .github/workflows/publish-manually.yml create mode 100644 .github/workflows/remove-experimental.yml create mode 100644 .github/workflows/update.yml create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 .husky/pre-commit create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 biome.json create mode 100755 bun.lockb create mode 100644 bunfig.toml create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tests/env.d.ts create mode 100644 tests/integration/index.test.ts create mode 100644 tests/preload.ts create mode 100644 tsconfig.json diff --git a/.github/actions/bump-version/action.yml b/.github/actions/bump-version/action.yml new file mode 100644 index 0000000..bcf895d --- /dev/null +++ b/.github/actions/bump-version/action.yml @@ -0,0 +1,56 @@ +name: Publish Experimental Packages +description: Bumps the version of an npm package and publishes it with an experimental tag. + +on: + workflow_call: [] + +inputs: + package_dir: + description: Directory of the Package to Publish + required: true + default: './' + +outputs: + VERSION_TAG: + description: Generated Experimental Version Tag for This Package + value: ${{ steps.version-tag.outputs.VERSION_TAG }} + +runs: + using: composite + steps: + - name: Construct Experimental Version Tag + id: version-tag + shell: bash + run: | + BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | sed -r 's/([a-z0-9])([A-Z])/\1-\L\2/g' | sed 's/_/-/g' | sed 's/\//-/g') + + echo "VERSION_TAG=$(echo $BRANCH_NAME)-experimental" >> $GITHUB_ENV + echo "VERSION_TAG=$(echo $VERSION_TAG)" >> $GITHUB_OUTPUT + + - name: Bump ${{ inputs.package_dir }} + shell: bash + run: | + cd ${{ inputs.package_dir }} + + PACKAGE_NAME=$(cat package.json | grep "name" | cut -d':' -f 2 | cut -d'"' -f 2) + echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV + + # Matches any version that ends with "-author-ron-123-experimental.". e.g. 1.0.0-leo-ron-123-experimental.0 + REGEX="\-$VERSION_TAG.[0-9]\{1,\}$" + + # Get all versions + ALL_VERSIONS=$(npm view $PACKAGE_NAME versions --json | jq -r '.[]') + + # Check if the experimental version already exists + if ! echo "$ALL_VERSIONS" | grep -q "$REGEX"; then + # If not, create it + npm version prerelease --preid=$VERSION_TAG --no-git-tag-version + else + # Otherwise up it + LATEST_VERSION=$(echo "$ALL_VERSIONS" | grep "$REGEX" | tail -1) + npm version $(npx semver $LATEST_VERSION -i prerelease --preid="$VERSION_TAG") --no-git-tag-version + fi + + - name: Publish to npm + shell: bash + run: cd ${{ inputs.package_dir }} && npm publish --tag="$VERSION_TAG" \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3ff39e8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + + # Open a new pull request every week, if there are updates available. + schedule: + interval: 'weekly' + timezone: 'Europe/Berlin' + + labels: + - 'enhancement' + + # Create one PR for all production dependencies, and one for all + # development dependencies. The latter is easier/faster to merge because it + # usually only affects local development. + groups: + production: + dependency-type: 'production' + development: + dependency-type: 'development' \ No newline at end of file diff --git a/.github/workflows/publish-experimental.yml b/.github/workflows/publish-experimental.yml new file mode 100644 index 0000000..c38f1b7 --- /dev/null +++ b/.github/workflows/publish-experimental.yml @@ -0,0 +1,51 @@ +name: Publish Experimental Packages + +on: + pull_request: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + permissions: + # Required for `actions/checkout@v3` + contents: read + # Required for `thollander/actions-comment-pull-request@v2` + pull-requests: write + + steps: + - name: Code Checkout + uses: actions/checkout@v3.1.0 + + # Needed only for bumping package version and publishing npm packages. + - name: Set up Node.js + uses: actions/setup-node@v3.5.1 + - run: echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN_READ_AND_WRITE }}' > ~/.npmrc + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Install Dependencies + run: bun install --frozen-lockfile + + - name: Build Package + run: bun run build + + - name: Bump `hono-ronin` + id: bump-ronin + uses: ./.github/actions/bump-version + + - name: Comment on PR + uses: thollander/actions-comment-pull-request@v2 + with: + comment_tag: packages_announcement + message: | + Released an experimental package: + + ```bash + bun add hono-ronin@${{ env.VERSION_TAG }} + ``` + + This package will be removed after the pull request has been merged. \ No newline at end of file diff --git a/.github/workflows/publish-manually.yml b/.github/workflows/publish-manually.yml new file mode 100644 index 0000000..06483a1 --- /dev/null +++ b/.github/workflows/publish-manually.yml @@ -0,0 +1,71 @@ +name: Manually Publish npm Packages + +on: + workflow_dispatch: + inputs: + version_type: + description: Choose a version type to bump the package version by. + required: true + default: 'patch' + type: choice + options: + - major + - minor + - patch + - prerelease + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Generate GitHub App Token + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.ORG_GH_RONIN_APP_ID }} + private_key: ${{ secrets.ORG_GH_RONIN_APP_PRIVATE_KEY }} + + - name: Code Checkout + uses: actions/checkout@v3.1.0 + with: + token: ${{ steps.generate_token.outputs.token }} + + # Needed only for bumping package version and publishing npm packages. + - name: Set up Node.js + uses: actions/setup-node@v3.5.1 + - run: echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN_READ_AND_WRITE }}' > ~/.npmrc + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Install Dependencies + run: bun install --frozen-lockfile + + - name: Build Package + run: bun run build + + - name: Set Git Config + run: | + # See where these config values come from at https://stackoverflow.com/a/74071223 + git config --global user.name "ronin-app[bot]" + git config --global user.email 135042755+ronin-app[bot]@users.noreply.github.com + + - name: Bump `hono-ronin` + run: | + npm version ${{ inputs.version_type }} --git-tag-version=false + echo "NEW_VERSION=$(npm pkg get version --workspaces=false | tr -d \")" >> $GITHUB_ENV + + - name: Push New Version + run: | + git fetch + git checkout ${GITHUB_HEAD_REF} + git pull origin ${GITHUB_HEAD_REF} + git commit -a -m '${{ env.NEW_VERSION }}' --no-verify + git tag -a ${{ env.NEW_VERSION }} -m '${{ env.NEW_VERSION }}' + git push origin ${GITHUB_HEAD_REF} + # Push tag + git push origin ${{ env.NEW_VERSION }} + + - name: Publish npm Package + run: npm publish \ No newline at end of file diff --git a/.github/workflows/remove-experimental.yml b/.github/workflows/remove-experimental.yml new file mode 100644 index 0000000..879a632 --- /dev/null +++ b/.github/workflows/remove-experimental.yml @@ -0,0 +1,28 @@ +name: Remove Experimental Packages + +on: + pull_request: + branches: + - main + types: [closed] + +jobs: + deploy: + runs-on: ubuntu-latest + name: Remove Experimental Packages + steps: + - name: Code Checkout + uses: actions/checkout@v3.1.0 + + - name: Set up Node.js + uses: actions/setup-node@v3.5.1 + - run: echo '//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN_READ_AND_WRITE }}' > ~/.npmrc + + # This converts the branch name into dash-case, so it can be used as a + # valid dist-tag. + - name: Extract Branch Name + shell: bash + run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | sed -r 's/([a-z0-9])([A-Z])/\1-\L\2/g' | sed 's/_/-/g' | sed 's/\//-/g')" >> $GITHUB_ENV + + - name: Remove Experimental Packages + run: npm dist-tag rm hono-ronin $BRANCH_NAME-experimental \ No newline at end of file diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml new file mode 100644 index 0000000..6ae0ffc --- /dev/null +++ b/.github/workflows/update.yml @@ -0,0 +1,23 @@ +name: Dependabot Auto-Merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1 + with: + github-token: '${{ github.token }}' + + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ github.token }} \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..d717956 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,73 @@ +name: Validate + +on: + pull_request: + branches: + - main + +jobs: + lint: + name: Linting + runs-on: ubuntu-latest + steps: + - name: Code Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.1.6 + + # Cache the local `node_modules` directory. + - name: Restore npm cache + id: cache-node-modules + uses: actions/cache@v4 + env: + cache-name: cache-node-modules + with: + path: node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('patches/**') }}-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('patches/**') }}- + ${{ runner.os }}-build-${{ hashFiles('patches/**') }}- + ${{ runner.os }}-${{ hashFiles('patches/**') }}- + + - name: Install Dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: bun install --frozen-lockfile + + - name: Code Linting + run: bun run lint + + test: + name: Testing + runs-on: ubuntu-latest + steps: + - name: Code Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.1.6 + + # Cache the local `node_modules` directory. + - name: Restore npm cache + id: cache-node-modules + uses: actions/cache@v4 + env: + cache-name: cache-node-modules + with: + path: node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('patches/**') }}-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('patches/**') }}- + ${{ runner.os }}-build-${{ hashFiles('patches/**') }}- + ${{ runner.os }}-${{ hashFiles('patches/**') }}- + + - name: Install Dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: bun install --frozen-lockfile + + - name: Testing + run: bun run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43bb788 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build output +dist + +# Dependencies +node_modules \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..f54fc9c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +bunx lint-staged \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..807043d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome", "oven.bun-vscode", "redhat.vscode-yaml"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4a6e40f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "editor.rulers": [80, 120], + + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + + "javascript.updateImportsOnFileMove.enabled": "always", + "javascript.preferences.importModuleSpecifier": "non-relative", + + "typescript.updateImportsOnFileMove.enabled": "always", + "typescript.preferences.importModuleSpecifier": "non-relative" +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..dd33abc --- /dev/null +++ b/biome.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.1/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "warn", + "noUnusedVariables": "warn" + } + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..0b2ac0224a2dc0c0e9c399b9024cf8b71d543a88 GIT binary patch literal 81129 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 E16(v+IRF3v literal 0 HcmV?d00001 diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..55b87f2 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,7 @@ +[install] +exact = true + +[test] +coverage = true +coverageThreshold = 1.0 +preload = ['./tests/preload.ts'] diff --git a/package.json b/package.json new file mode 100644 index 0000000..2107b87 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "hono-ronin", + "version": "0.0.1", + "license": "Apache-2.0", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "description": "Access the RONIN data platform via Hono.", + "scripts": { + "dev": "bun run build -- --watch", + "build": "bunchee", + "check": "bunx @biomejs/biome check --apply {./**/*.ts,**.json}", + "format": "bunx @biomejs/biome format --write {./**/*.ts,**.json}", + "lint": "bunx @biomejs/biome lint --apply {./**/*.ts,**.json}", + "test": "bun test", + "prepare": "husky" + }, + "files": ["dist"], + "repository": "ronin-co/hono-ronin", + "homepage": "https://github.com/ronin-co/hono-ronin", + "keywords": ["ronin", "hono", "client", "database", "orm"], + "lint-staged": { + "**/*": ["bunx @biomejs/biome format --write {./**/*.ts,**.json}"] + }, + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "dependencies": { + "ronin": "3.0.1" + }, + "devDependencies": { + "@biomejs/biome": "1.7.2", + "@types/bun": "1.1.1", + "bunchee": "5.1.5", + "hono": "4.3.2", + "husky": "9.0.11", + "lint-staged": "15.2.2", + "msw": "2.2.14", + "typescript": "5.4.5" + }, + "peerDependencies": { + "hono": ">=3.9.0" + }, + "peerDependenciesMeta": { + "hono": { + "optional": false + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..baa2cd8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,59 @@ +import { createMiddleware } from "hono/factory"; +import createFactory from "ronin"; + +type Factory = ReturnType; + +export type Bindings = { + RONIN_TOKEN: string; +}; + +export type Variables = { + ronin: Factory; +}; + +type Env = { + Bindings: Bindings; + Variables: Variables; +}; + +type QueryHandlerOptions = Omit[0], "token">; + +/** + * Create a Hono middleware that injects a RONIN client into the Hono context variables. + * + * @param options - An optional object that can be used to configure the RONIN client. + * + * @example .env + * ```bash + * RONIN_TOKEN=your-ronin-token + * ``` + * + * @example + * ```ts + * import { Hono } from 'hono'; + * import { ronin, type Bindings, type Variables } from 'hono-ronin'; + * + * const app = new Hono() + * .use('*', ronin()) + * .get('/', async (c) => { + * const posts = await c.var.ronin.get.posts(); + * return c.json(posts); + * }); + * ``` + * + * @returns A Hono middleware + */ +export const ronin = (options: QueryHandlerOptions = {}) => + createMiddleware(async (c, next) => { + if (!c.env.RONIN_TOKEN) + throw new Error("Missing `RONIN_TOKEN` in environment variables"); + + const client = createFactory({ + token: c.env.RONIN_TOKEN, + ...options, + }); + + c.set("ronin", client); + + await next(); + }); diff --git a/tests/env.d.ts b/tests/env.d.ts new file mode 100644 index 0000000..7d6dd3a --- /dev/null +++ b/tests/env.d.ts @@ -0,0 +1,20 @@ +import type { RONIN } from "ronin"; + +interface ImportMetaEnv { + RONIN_TOKEN: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +declare module "ronin" { + interface UsersGetter + extends RONIN.IGetterPlural, "users", never> {} + + export namespace RONIN { + export interface Getter { + users: UsersGetter; + } + } +} diff --git a/tests/integration/index.test.ts b/tests/integration/index.test.ts new file mode 100644 index 0000000..dc1ddb3 --- /dev/null +++ b/tests/integration/index.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "bun:test"; +import { createFactory } from "hono/factory"; +import { testClient } from "hono/testing"; + +import { ronin } from "@/index"; + +import type { Bindings, Variables } from "@/index"; + +describe("use `ronin` middleware", () => { + const factory = createFactory<{ Bindings: Bindings; Variables: Variables }>(); + + it("with a missing `RONIN_TOKEN`", async () => { + const app = factory + .createApp() + .use("*", ronin()) + .get("/", async (c) => { + expect(c.var.ronin).toBeDefined(); + + const posts = (await c.var.ronin.get.blogPosts()) as Array; + + return c.json(posts); + }); + + const response = await testClient(app, { + RONIN_TOKEN: null, + }).index.$get(); + + expect(response.ok).toBe(false); + expect(response.status).toBe(500); + }); + + it("with default options", async () => { + const app = factory + .createApp() + .use("*", ronin()) + .get("/", async (c) => { + expect(c.var.ronin).toBeDefined(); + + const users = await c.var.ronin.get.users(); + + expect(users).toBeDefined(); + expect(users).toBeInstanceOf(Array); + + return c.json(users); + }); + + const response = await testClient(app, { + RONIN_TOKEN: import.meta.env.RONIN_TOKEN, + }).index.$get(); + + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const json = await response.json(); + + expect(json).toBeDefined(); + expect(json).toBeInstanceOf(Array); + expect(json).toHaveLength(0); + }); + + it("with a custom options", async () => { + const app = factory + .createApp() + .use( + "*", + ronin({ + hooks: { + user: { + beforeGet: (query) => { + query.limitedTo = 1; + return query; + }, + }, + }, + }), + ) + .get("/", async (c) => { + expect(c.var.ronin).toBeDefined(); + + const users = await c.var.ronin.get.users(); + + expect(users).toBeDefined(); + expect(users).toBeInstanceOf(Array); + + return c.json(users); + }); + + const response = await testClient(app, { + RONIN_TOKEN: import.meta.env.RONIN_TOKEN, + }).index.$get(); + + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const json = await response.json(); + + expect(json).toBeDefined(); + expect(json).toBeInstanceOf(Array); + expect(json).toHaveLength(1); + }); +}); diff --git a/tests/preload.ts b/tests/preload.ts new file mode 100644 index 0000000..b796e28 --- /dev/null +++ b/tests/preload.ts @@ -0,0 +1,30 @@ +import { afterAll, afterEach, beforeAll } from "bun:test"; +import { setupServer } from "msw/node"; +import { http, HttpResponse } from "msw"; + +import.meta.env.RONIN_TOKEN = "secret-token"; + +const mockHttpServer = setupServer( + http.post("https://data.ronin.co/", async ({ request }) => { + const queries = (await request.json()) as { + queries: Array<{ + get: { + users: { + limitedTo?: number; + }; + }; + }>; + }; + + const numRecords = queries?.queries?.[0].get?.users?.limitedTo || 0; + const records = Array.from({ length: numRecords }).fill(null); + + return HttpResponse.json({ + results: [{ records }], + }); + }), +); + +beforeAll(() => mockHttpServer.listen()); +afterEach(() => mockHttpServer.resetHandlers()); +afterAll(() => mockHttpServer.close()); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2743176 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "resolveJsonModule": true, + "baseUrl": ".", + + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + "noUnusedLocals": true, + "noUnusedParameters": true, + + "paths": { + "@/*": ["./src/*"] + } + } +}