Skip to content
This repository has been archived by the owner on Feb 13, 2025. It is now read-only.

Route definition based requests #40

Merged
merged 11 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["./node_modules/@lokalise/biome-config/configs/biome-base.jsonc"],
"linter": {
"rules": {
"performance": {
"noBarrelFile": "off",
"noReExportAll": "off"
},
"style": {
"noUnusedTemplateLiteral": "off"
}
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["./node_modules/@lokalise/biome-config/configs/biome-base.jsonc"],
"linter": {
"rules": {
"performance": {
"noBarrelFile": "off",
"noReExportAll": "off"
},
"style": {
"noUnusedTemplateLiteral": "off"
}
}
}
}
}
10 changes: 9 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export { sendPost, sendGet, sendPut, sendDelete, sendPatch, UNKNOWN_SCHEMA } from './src/client.js'
export {
sendPost,
sendGet,
sendPut,
sendDelete,
sendPatch,
sendByRouteDefinition,
UNKNOWN_SCHEMA,
} from './src/client.js'
122 changes: 61 additions & 61 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,63 +1,63 @@
{
"name": "@lokalise/frontend-http-client",
"version": "2.1.0",
"description": "Opinionated HTTP client for the frontend",
"files": ["dist/**", "LICENSE", "README.md"],
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"module": "./dist/index.mjs",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"author": {
"name": "Lokalise",
"url": "https://lokalise.com/"
},
"homepage": "https://github.com/lokalise/frontend-http-client",
"repository": {
"type": "git",
"url": "git://github.com/lokalise/frontend-http-client.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build:dev": "tsc",
"build:release": "tsup",
"clean": "rimraf dist",
"lint": "biome check . && tsc --project tsconfig.lint.json --noEmit",
"lint:fix": "biome check --write",
"test": "vitest run --coverage",
"prepublishOnly": "npm run clean && npm run build:release"
},
"dependencies": {
"fast-querystring": "^1.1.2"
},
"peerDependencies": {
"wretch": "^2.8.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@lokalise/biome-config": "^1.0.0",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^2.0.1",
"jest-fail-on-console": "^3.1.2",
"mockttp": "^3.13.0",
"rimraf": "^6.0.0",
"tsup": "8.3.5",
"typescript": "~5.7.2",
"vitest": "^2.0.1"
},
"keywords": ["frontend", "web", "browser", "http", "client", "zod", "validation", "typesafe"]
"name": "@lokalise/frontend-http-client",
Copy link
Collaborator Author

@kibertoad kibertoad Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to do reformatting, as library repos do not use package-lock file, so CI picked up the latest version of biome and rules file, and CI broke

"version": "2.1.0",
"description": "Opinionated HTTP client for the frontend",
"files": ["dist/**", "LICENSE", "README.md"],
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"module": "./dist/index.mjs",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"author": {
"name": "Lokalise",
"url": "https://lokalise.com/"
},
"homepage": "https://github.com/lokalise/frontend-http-client",
"repository": {
"type": "git",
"url": "git://github.com/lokalise/frontend-http-client.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build:dev": "tsc",
"build:release": "tsup",
"clean": "rimraf dist",
"lint": "biome check . && tsc --project tsconfig.lint.json --noEmit",
"lint:fix": "biome check --write",
"test": "vitest run --coverage",
"prepublishOnly": "npm run clean && npm run build:release"
},
"dependencies": {
"fast-querystring": "^1.1.2"
},
"peerDependencies": {
"wretch": "^2.8.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@lokalise/biome-config": "^1.5.0",
"@types/node": "^22.10.3",
"@vitest/coverage-v8": "^2.1.8",
"jest-fail-on-console": "^3.1.2",
"mockttp": "^3.15.5",
"rimraf": "^6.0.0",
"tsup": "8.3.5",
"typescript": "~5.7.2",
"vitest": "^2.1.8"
},
"keywords": ["frontend", "web", "browser", "http", "client", "zod", "validation", "typesafe"]
}
87 changes: 86 additions & 1 deletion src/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import wretch from 'wretch'
import { z } from 'zod'

import { sendDelete, sendGet, sendPatch, sendPost, sendPut } from './client.js'
import {
sendByRouteDefinition,
sendDelete,
sendGet,
sendPatch,
sendPost,
sendPut,
} from './client.js'
import { buildRouteDefinition } from './routeDefinition.js'

describe('frontend-http-client', () => {
const mockServer = getLocal()
Expand All @@ -17,6 +25,83 @@ describe('frontend-http-client', () => {
beforeEach(() => mockServer.start())
afterEach(() => mockServer.stop())

describe('sendByRouteDefinition', () => {
it('returns deserialized response', async () => {
const client = wretch(mockServer.url)

await mockServer.forPost('/users/1').thenJson(200, { data: { code: 99 } })

const requestBodySchema = z.object({
isActive: z.boolean(),
})

const responseBodySchema = z.object({
data: z.object({
code: z.number(),
}),
})

const pathSchema = z.object({
userId: z.number(),
})

const routeDefinition = buildRouteDefinition({
kibertoad marked this conversation as resolved.
Show resolved Hide resolved
method: 'post',
isEmptyResponseExpected: false,
isNonJSONResponseExpected: false,
responseBodySchema,
requestPathParamsSchema: pathSchema,
requestBodySchema: requestBodySchema,
pathResolver: (pathParams) => `/users/${pathParams.userId}`,
})

const responseBody = await sendByRouteDefinition(client, routeDefinition, {
pathParams: {
userId: 1,
},
body: {
isActive: true,
},
})

expect(responseBody).toEqual({
data: {
code: 99,
},
})
})

it('returns deserialized response without body or path params', async () => {
const client = wretch(mockServer.url)

await mockServer.forPost('/users').thenJson(200, { data: { code: 99 } })

const responseBodySchema = z.object({
data: z.object({
code: z.number(),
}),
})

const routeDefinition = buildRouteDefinition({
method: 'post',
isEmptyResponseExpected: false,
isNonJSONResponseExpected: false,
responseBodySchema,
requestPathParamsSchema: undefined,
kibertoad marked this conversation as resolved.
Show resolved Hide resolved
requestBodySchema: undefined,
pathResolver: () => `/users`,
})

const responseBody = await sendByRouteDefinition(client, routeDefinition, {})

expect(responseBody).toEqual({
data: {
code: 99,
},
})
})
})

describe('sendPost', () => {
it('returns deserialized response', async () => {
const client = wretch(mockServer.url)
Expand Down
53 changes: 53 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { z } from 'zod'

import type { InferSchemaOutput, ResourceChangeRouteDefinition } from './routeDefinition.js'
import type {
DeleteParams,
FreeDeleteParams,
FreeQueryParams,
QueryParams,
RequestResultType,
ResourceChangeByDefinitionParams,
ResourceChangeParams,
WretchInstance,
} from './types.js'
Expand Down Expand Up @@ -301,3 +303,54 @@ export function sendDelete<
return bodyParseResult.result
}) as Promise<RequestResultType<ResponseBody, IsNonJSONResponseExpected, IsEmptyResponseExpected>>
}

export function sendByRouteDefinition<
T extends WretchInstance,
IsNonJSONResponseExpected extends boolean,
IsEmptyResponseExpected extends boolean,
RequestBodySchema extends z.Schema | undefined,
ResponseBodySchema extends z.Schema | undefined = undefined,
PathParamsSchema extends z.Schema | undefined = undefined,
RequestQuerySchema extends z.Schema | undefined = undefined,
RequestHeaderSchema extends z.Schema | undefined = undefined,
>(
wretch: T,
routeDefinition: ResourceChangeRouteDefinition<
IsNonJSONResponseExpected,
IsEmptyResponseExpected,
InferSchemaOutput<PathParamsSchema>,
RequestBodySchema,
ResponseBodySchema,
PathParamsSchema,
RequestQuerySchema,
RequestHeaderSchema
>,
params: ResourceChangeByDefinitionParams<
InferSchemaOutput<PathParamsSchema>,
InferSchemaOutput<RequestBodySchema>,
InferSchemaOutput<RequestQuerySchema>,
InferSchemaOutput<RequestHeaderSchema>
>,
): Promise<
RequestResultType<
InferSchemaOutput<ResponseBodySchema>,
IsNonJSONResponseExpected,
IsEmptyResponseExpected
>
> {
return sendResourceChange(wretch, routeDefinition.method, {
// @ts-expect-error magic type inferring happening
body: params.body,
isEmptyResponseExpected: routeDefinition.isEmptyResponseExpected,
isNonJSONResponseExpected: routeDefinition.isNonJSONResponseExpected,
// biome-ignore lint/suspicious/noExplicitAny: FixMe try to find a solution
requestBodySchema: routeDefinition.requestBodySchema as any,
// biome-ignore lint/suspicious/noExplicitAny: FixMe try to find a solution
responseBodySchema: routeDefinition.responseBodySchema as any,
// @ts-expect-error magic type inferring happening
queryParams: params.queryParams,
queryParamsSchema: routeDefinition.requestQuerySchema,
// @ts-expect-error magic type inferring happening
path: routeDefinition.pathResolver(params.pathParams),
})
}
61 changes: 61 additions & 0 deletions src/routeDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ZodSchema, z } from 'zod'

export type RoutePathResolver<PathParams> = (pathParams: PathParams) => string

export type InferSchemaOutput<T extends ZodSchema | undefined> = T extends ZodSchema<infer U>
? U
: T extends undefined
? undefined
: never

export function buildRouteDefinition<
IsNonJSONResponseExpected extends boolean,
IsEmptyResponseExpected extends boolean,
PathParams,
RequestBodySchema extends z.Schema | undefined = undefined,
ResponseBodySchema extends z.Schema | undefined = undefined,
PathParamsSchema extends z.Schema<PathParams> | undefined = undefined,
RequestQuerySchema extends z.Schema | undefined = undefined,
RequestHeaderSchema extends z.Schema | undefined = undefined,
>(
params: ResourceChangeRouteDefinition<
IsNonJSONResponseExpected,
IsEmptyResponseExpected,
RequestBodySchema,
ResponseBodySchema,
PathParamsSchema,
RequestQuerySchema,
RequestHeaderSchema
>,
): ResourceChangeRouteDefinition<
IsNonJSONResponseExpected,
IsEmptyResponseExpected,
RequestBodySchema,
ResponseBodySchema,
PathParamsSchema,
RequestQuerySchema,
RequestHeaderSchema
> {
return params
}

export type ResourceChangeRouteDefinition<
IsNonJSONResponseExpected extends boolean,
IsEmptyResponseExpected extends boolean,
PathParams,
RequestBodySchema extends z.Schema | undefined = undefined,
ResponseBodySchema extends z.Schema | undefined = undefined,
PathParamsSchema extends z.Schema<PathParams> | undefined = undefined,
RequestQuerySchema extends z.Schema | undefined = undefined,
RequestHeaderSchema extends z.Schema | undefined = undefined,
> = {
method: 'post' | 'put' | 'patch'
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
requestBodySchema: RequestBodySchema
responseBodySchema: ResponseBodySchema
requestPathParamsSchema: PathParamsSchema
requestQuerySchema?: RequestQuerySchema
requestHeaderSchema?: RequestHeaderSchema
pathResolver: RoutePathResolver<InferSchemaOutput<PathParamsSchema>>
}
Loading
Loading