From 01691d65b9b74dd42e7c6cd1414563e51dc089c1 Mon Sep 17 00:00:00 2001 From: Dmytro Zelenetskyi Date: Fri, 23 Aug 2024 17:01:53 +0200 Subject: [PATCH] Better result typing (#5) --- .changeset/cold-pandas-exist.md | 5 ++ README.md | 8 +-- apis/figma/README.md | 2 - apis/figma/package.json | 6 +- apis/figma/src/api/comments.ts | 14 ++-- apis/figma/src/api/components.ts | 20 +++--- apis/figma/src/api/dev_resources.ts | 10 +-- apis/figma/src/api/files.ts | 10 +-- apis/figma/src/api/logs.ts | 4 +- apis/figma/src/api/payments.ts | 4 +- apis/figma/src/api/projects.ts | 6 +- apis/figma/src/api/users.ts | 4 +- apis/figma/src/api/variables.ts | 8 +-- apis/figma/src/api/versions.ts | 4 +- apis/figma/src/api/webhooks.ts | 14 ++-- apis/figma/src/index.ts | 12 ++-- apis/flickr/README.md | 2 - apis/flickr/package.json | 6 +- apis/flickr/src/api/activity.ts | 6 +- apis/flickr/src/api/photosets.ts | 30 ++++----- apis/flickr/src/index.ts | 10 +-- bun.lockb | Bin 208920 -> 192720 bytes package.json | 8 +-- packages/http-client/README.md | 11 ++-- packages/http-client/package.json | 4 +- packages/http-client/src/example.ts | 96 ++++++++++++++++++++++++++++ packages/http-client/src/index.ts | 88 +++++++++++++++++++++---- turbo.json | 2 +- 28 files changed, 274 insertions(+), 120 deletions(-) create mode 100644 .changeset/cold-pandas-exist.md create mode 100644 packages/http-client/src/example.ts diff --git a/.changeset/cold-pandas-exist.md b/.changeset/cold-pandas-exist.md new file mode 100644 index 0000000..2cde8c4 --- /dev/null +++ b/.changeset/cold-pandas-exist.md @@ -0,0 +1,5 @@ +--- +"@zemd/http-client": major +--- + +Make it possible to map and validate response, and make typescript to highlight proper types diff --git a/README.md b/README.md index d828aec..b45648e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ This repository contains a collection of libs that help to work with different A of api clients. At the moment there are two main libraries: - - [`@zemd/http-client`](./packages/http-client) - a lib which follows simple concept for building http clients - - [`@zemd/figma-rest-api`](./apis/figma) - a lib that is built on top of `@zemd/http-client` and provides a client for working with Figma REST API. - ## License +- [`@zemd/http-client`](./packages/http-client) - a lib which follows simple concept for building http clients +- [`@zemd/figma-rest-api`](./apis/figma) - a lib that is built on top of `@zemd/http-client` and provides a client for working with Figma REST API. + +## License All the code in the repository released under the Apache 2.0 license @@ -15,4 +16,3 @@ All the code in the repository released under the Apache 2.0 license [![](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/red_rabbit) [![](https://img.shields.io/static/v1?label=UNITED24&message=support%20Ukraine&color=blue)](https://u24.gov.ua/) - diff --git a/apis/figma/README.md b/apis/figma/README.md index 47d0913..66f2ccb 100644 --- a/apis/figma/README.md +++ b/apis/figma/README.md @@ -28,7 +28,6 @@ some experimental features. In this case, you can construct your own api call us Since the library is built on top of `@zemd/http-client` you can compose different configurations together. - ## License `@zemd/figma-rest-api` released under the Apache 2.0 license @@ -37,4 +36,3 @@ Since the library is built on top of `@zemd/http-client` you can compose differe [![](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/red_rabbit) [![](https://img.shields.io/static/v1?label=UNITED24&message=support%20Ukraine&color=blue)](https://u24.gov.ua/) - diff --git a/apis/figma/package.json b/apis/figma/package.json index fad3661..72795a8 100644 --- a/apis/figma/package.json +++ b/apis/figma/package.json @@ -34,11 +34,11 @@ }, "devDependencies": { "@types/bun": "latest", - "@zemd/tsconfig": "^1.2.0", - "typescript": "^5.3.3" + "@zemd/tsconfig": "^1.3.0", + "typescript": "^5.5.4" }, "dependencies": { "@zemd/http-client": "^1.0.0", - "zod": "^3.22.4" + "zod": "^3.23.8" } } \ No newline at end of file diff --git a/apis/figma/src/api/comments.ts b/apis/figma/src/api/comments.ts index 42371fc..cfa7078 100644 --- a/apis/figma/src/api/comments.ts +++ b/apis/figma/src/api/comments.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { body, method, query, type TEndpointDec } from "@zemd/http-client"; +import { body, method, query, type TEndpointDecTuple } from "@zemd/http-client"; const GetCommentsQuerySchema = z.object({ as_md: z.coerce.boolean().optional(), @@ -10,7 +10,7 @@ export interface GetCommentsQuery extends z.infer /** * Gets a list of comments left on the file. */ -export const getComments = (key: string, options?: GetCommentsQuery): TEndpointDec => { +export const getComments = (key: string, options?: GetCommentsQuery): TEndpointDecTuple => { const transformers = [method("GET")]; if (options) { transformers.push(query(GetCommentsQuerySchema.passthrough().parse(options))); @@ -57,7 +57,7 @@ export interface PostCommentsQuery extends z.infer { +export const postComments = (key: string, message: PostCommentsQuery): TEndpointDecTuple => { return [ `/v1/files/${key}/comments`, [method("POST"), body(JSON.stringify(PostCommentsQuerySchema.passthrough().parse(message)))], @@ -67,7 +67,7 @@ export const postComments = (key: string, message: PostCommentsQuery): TEndpoint /** * Deletes a specific comment. Only the person who made the comment is allowed to delete it. */ -export const deleteComments = (key: string, commendId: string): TEndpointDec => { +export const deleteComments = (key: string, commendId: string): TEndpointDecTuple => { return [`/v1/files/${key}/comments/${commendId}`, [method("DELETE")]]; }; @@ -84,7 +84,7 @@ export const getCommentsReactions = ( key: string, commentId: string, options?: GetCommentsReactionsQuery, -): TEndpointDec => { +): TEndpointDecTuple => { const transformers = [method("GET")]; if (options) { transformers.push(query(GetCommentsReactionsQuerySchema.passthrough().parse(options))); @@ -105,7 +105,7 @@ export const postCommentsReactions = ( key: string, commentId: string, options: PostCommentsReactionsQuery, -): TEndpointDec => { +): TEndpointDecTuple => { return [ `/v1/files/${key}/comments/${commentId}/reactions`, [method("POST"), body(JSON.stringify(PostCommentsReactionsQuerySchema.passthrough().parse(options)))], @@ -120,7 +120,7 @@ export const deleteCommentsReactions = ( key: string, commentId: string, options: PostCommentsReactionsQuery, -): TEndpointDec => { +): TEndpointDecTuple => { return [ `/v1/files/${key}/comments/${commentId}/reactions`, [method("DELETE"), query(PostCommentsReactionsQuerySchema.passthrough().parse(options))], diff --git a/apis/figma/src/api/components.ts b/apis/figma/src/api/components.ts index b660545..b2814ac 100644 --- a/apis/figma/src/api/components.ts +++ b/apis/figma/src/api/components.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; export const PaginationQuerySchema = z.object({ page_size: z.number().optional(), @@ -12,21 +12,21 @@ export interface GetTeamComponentsQuery extends z.infer { +export const getTeamComponents = (teamId: string, options: GetTeamComponentsQuery): TEndpointDecTuple => { return [`/v1/teams/${teamId}/components`, [method("GET"), query(PaginationQuerySchema.passthrough().parse(options))]]; }; /** * Get a list of published components within a file library */ -export const getFileComponents = (key: string): TEndpointDec => { +export const getFileComponents = (key: string): TEndpointDecTuple => { return [`/v1/files/${key}/components`, [method("GET")]]; }; /** * Get metadata on a component by key. */ -export const getComponent = (key: string): TEndpointDec => { +export const getComponent = (key: string): TEndpointDecTuple => { return [`/v1/components/${key}`, [method("GET")]]; }; @@ -34,7 +34,7 @@ export interface GetTeamComponentSetsQuery extends z.infer { +export const getTeamComponentSets = (teamId: string, options: GetTeamComponentSetsQuery): TEndpointDecTuple => { return [ `/v1/teams/${teamId}/component_sets`, [method("GET"), query(PaginationQuerySchema.passthrough().parse(options))], @@ -44,14 +44,14 @@ export const getTeamComponentSets = (teamId: string, options: GetTeamComponentSe /** * Get a list of published component sets within a file library */ -export const getFileComponentSets = (key: string): TEndpointDec => { +export const getFileComponentSets = (key: string): TEndpointDecTuple => { return [`/v1/files/${key}/component_sets`, [method("GET")]]; }; /** * Get metadata on a component set by key. */ -export const getComponentSet = (key: string): TEndpointDec => { +export const getComponentSet = (key: string): TEndpointDecTuple => { return [`/v1/component_sets/${key}`, [method("GET")]]; }; @@ -60,20 +60,20 @@ export interface GetTeamStylesQuery extends z.infer { +export const getTeamStyles = (teamId: string, options: GetTeamStylesQuery): TEndpointDecTuple => { return [`/v1/teams/${teamId}/styles`, [method("GET"), query(PaginationQuerySchema.passthrough().parse(options))]]; }; /** * Get a list of published styles within a file library */ -export const getFileStyles = (key: string): TEndpointDec => { +export const getFileStyles = (key: string): TEndpointDecTuple => { return [`/v1/files/${key}/styles`, [method("GET")]]; }; /** * Get metadata on a style by key. */ -export const getStyle = (key: string): TEndpointDec => { +export const getStyle = (key: string): TEndpointDecTuple => { return [`/v1/styles/${key}`, [method("GET")]]; }; diff --git a/apis/figma/src/api/dev_resources.ts b/apis/figma/src/api/dev_resources.ts index 2a0f559..f3d18b3 100644 --- a/apis/figma/src/api/dev_resources.ts +++ b/apis/figma/src/api/dev_resources.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { body, method, query, type TEndpointDec } from "@zemd/http-client"; +import { body, method, query, type TEndpointDecTuple } from "@zemd/http-client"; export const GetDevResourcesQuerySchema = z.object({ node_ids: z.string().optional(), @@ -10,7 +10,7 @@ export interface GetDevResourcesQuery extends z.infer { +export const getDevResources = (key: string, options?: GetDevResourcesQuery): TEndpointDecTuple => { const transformers = [method("GET")]; if (options) { transformers.push(query(GetDevResourcesQuerySchema.passthrough().parse(options))); @@ -46,7 +46,7 @@ export interface PostDevResources extends z.infer { +export const postDevResources = (options: PostDevResources): TEndpointDecTuple => { return [ `/v1/dev_resources`, [method("POST"), body(JSON.stringify(PostDevResourcesBodySchema.passthrough().parse(options)))], @@ -74,7 +74,7 @@ export interface PutDevResourcesBody extends z.infer { +export const putDevResources = (options: PutDevResourcesBody): TEndpointDecTuple => { return [ `/v1/dev_resources`, [method("PUT"), body(JSON.stringify(PutDevResourcesBodySchema.passthrough().parse(options)))], @@ -84,6 +84,6 @@ export const putDevResources = (options: PutDevResourcesBody): TEndpointDec => { /** * Delete a dev resources from a file. */ -export const deleteDevResources = (key: string, devResourceId: string): TEndpointDec => { +export const deleteDevResources = (key: string, devResourceId: string): TEndpointDecTuple => { return [`/v1/files/${key}/dev_resources/${devResourceId}`, [method("DELETE")]]; }; diff --git a/apis/figma/src/api/files.ts b/apis/figma/src/api/files.ts index 2f9b00a..84e054f 100644 --- a/apis/figma/src/api/files.ts +++ b/apis/figma/src/api/files.ts @@ -1,4 +1,4 @@ -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; import { z } from "zod"; export const GetFileQuerySchema = z.object({ @@ -23,7 +23,7 @@ export interface GetFileQuery extends z.infer {} * The components key contains a mapping from node IDs to component metadata. * This is to help you determine which components each instance comes from. */ -export const getFile = (key: string, options?: GetFileQuery): TEndpointDec => { +export const getFile = (key: string, options?: GetFileQuery): TEndpointDecTuple => { const transformers = [method("GET")]; if (options) { @@ -66,7 +66,7 @@ export interface GetFileNodesQuery extends z.infer { +export const getFileNodes = (key: string, options: GetFileNodesQuery): TEndpointDecTuple => { return [`/files/${key}/nodes`, [method("GET"), query(GetFileNodesQuerySchema.passthrough().parse(options))]]; }; @@ -103,7 +103,7 @@ export interface GetImageQuery extends z.infer {} * To render multiple images from the same file, use the ids query parameter * to specify multiple node ids. */ -export const getImage = (key: string, options: GetImageQuery): TEndpointDec => { +export const getImage = (key: string, options: GetImageQuery): TEndpointDecTuple => { return [`/images/${key}`, [method("GET"), query(GetImageQuerySchema.passthrough().parse(options))]]; }; @@ -119,6 +119,6 @@ export const getImage = (key: string, options: GetImageQuery): TEndpointDec => { * references are located in the output of the GET files endpoint under the imageRef * attribute in a Paint. */ -export const getImageFills = (key: string): TEndpointDec => { +export const getImageFills = (key: string): TEndpointDecTuple => { return [`/files/${key}/images`, [method("GET")]]; }; diff --git a/apis/figma/src/api/logs.ts b/apis/figma/src/api/logs.ts index d33ad06..e90dbc1 100644 --- a/apis/figma/src/api/logs.ts +++ b/apis/figma/src/api/logs.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; export const GetActivityLogsQuerySchema = z.object({ events: z.string().optional(), @@ -14,7 +14,7 @@ export interface GetActivityLogsQuery extends z.infer { +export const getActivityLogs = (options?: GetActivityLogsQuery): TEndpointDecTuple => { const transformers = [method("GET")]; if (options) { transformers.push(query(GetActivityLogsQuerySchema.passthrough().parse(options))); diff --git a/apis/figma/src/api/payments.ts b/apis/figma/src/api/payments.ts index 87a58c8..e0bef91 100644 --- a/apis/figma/src/api/payments.ts +++ b/apis/figma/src/api/payments.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; export const GetPaymentsQuerySchema = z.object({ user_id: z.number(), @@ -13,6 +13,6 @@ export interface GetPaymentsQuery extends z.infer /** * Fetch the payment information of a user on your resource using IDs. */ -export const getPayments = (options: GetPaymentsQuery): TEndpointDec => { +export const getPayments = (options: GetPaymentsQuery): TEndpointDecTuple => { return [`/v1/payments`, [method("GET"), query(GetPaymentsQuerySchema.passthrough().parse(options))]]; }; diff --git a/apis/figma/src/api/projects.ts b/apis/figma/src/api/projects.ts index f28748a..a2ae3a5 100644 --- a/apis/figma/src/api/projects.ts +++ b/apis/figma/src/api/projects.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; /** * You can use this Endpoint to get a list of all the Projects within @@ -10,7 +10,7 @@ import { method, query, type TEndpointDec } from "@zemd/http-client"; * team you are a part of. The team id will be present in the URL after * the word team and before your team name. */ -export const getTeamProjects = (teamId: string): TEndpointDec => { +export const getTeamProjects = (teamId: string): TEndpointDecTuple => { return [`/v1/teams/${teamId}/projects`, [method("GET")]]; }; @@ -23,7 +23,7 @@ export interface GetProjectFilesQuery extends z.infer { +export const getProjectFiles = (projectId: string, options?: GetProjectFilesQuery): TEndpointDecTuple => { const transformers = [method("GET")]; if (options) { transformers.push(query(GetProjectFilesQuerySchema.passthrough().parse(options))); diff --git a/apis/figma/src/api/users.ts b/apis/figma/src/api/users.ts index fc0097e..5d1a1db 100644 --- a/apis/figma/src/api/users.ts +++ b/apis/figma/src/api/users.ts @@ -1,9 +1,9 @@ -import { method, type TEndpointDec } from "@zemd/http-client"; +import { method, type TEndpointDecTuple } from "@zemd/http-client"; /** * If you are using OAuth for authentication, this endpoint can be * used to get user information for the authenticated user. */ -export const getMe = (): TEndpointDec => { +export const getMe = (): TEndpointDecTuple => { return [`/v1/me`, [method("GET")]]; }; diff --git a/apis/figma/src/api/variables.ts b/apis/figma/src/api/variables.ts index 55f7e25..e289c58 100644 --- a/apis/figma/src/api/variables.ts +++ b/apis/figma/src/api/variables.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { body, method, type TEndpointDec } from "@zemd/http-client"; +import { body, method, type TEndpointDecTuple } from "@zemd/http-client"; import { ColorProp, VariableAliasProp } from "../schema.js"; /** @@ -19,7 +19,7 @@ import { ColorProp, VariableAliasProp } from "../schema.js"; * Instead, you will need to use the GET /v1/files/:file_key/variables/local * endpoint, in the same file, to examine the mode values. */ -export const getLocalVariables = (key: string): TEndpointDec => { +export const getLocalVariables = (key: string): TEndpointDecTuple => { return [`/v1/files/${key}/variables/local`, [method("GET")]]; }; @@ -45,7 +45,7 @@ export const getLocalVariables = (key: string): TEndpointDec => { * change to a variable was published. For variable collections, this timestamp will * change any time a variable in the collection is changed. */ -export const getPublishedVariables = (key: string): TEndpointDec => { +export const getPublishedVariables = (key: string): TEndpointDecTuple => { return [`/v1/files/${key}/variables/published`, [method("GET")]]; }; @@ -170,7 +170,7 @@ export interface PostVariablesBody extends z.infer { +export const postVariables = (key: string, options?: PostVariablesBody): TEndpointDecTuple => { const transformers = [method("POST")]; if (options) { transformers.push(body(JSON.stringify(PostVariablesBodySchema.passthrough().parse(options)))); diff --git a/apis/figma/src/api/versions.ts b/apis/figma/src/api/versions.ts index 8ff7b89..49de686 100644 --- a/apis/figma/src/api/versions.ts +++ b/apis/figma/src/api/versions.ts @@ -1,8 +1,8 @@ -import type { TEndpointDec } from "@zemd/http-client"; +import type { TEndpointDecTuple } from "@zemd/http-client"; /** * A list of the versions of a file. */ -export const getFileVersion = (key: string): TEndpointDec => { +export const getFileVersion = (key: string): TEndpointDecTuple => { return [`/v1/files/${key}/versions`, []]; }; diff --git a/apis/figma/src/api/webhooks.ts b/apis/figma/src/api/webhooks.ts index a3cac64..c229ed9 100644 --- a/apis/figma/src/api/webhooks.ts +++ b/apis/figma/src/api/webhooks.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { body, method, type TEndpointDec } from "@zemd/http-client"; +import { body, method, type TEndpointDecTuple } from "@zemd/http-client"; const WebhookV2Event = z.enum([ "FILE_UPDATE", @@ -29,14 +29,14 @@ export interface PostWebhooksBody extends z.infer * behavior is not desired, you can create the webhook and set the * status to PAUSED and reactivate it later. */ -export const postWebhooks = (obj: PostWebhooksBody): TEndpointDec => { +export const postWebhooks = (obj: PostWebhooksBody): TEndpointDecTuple => { return [`/v2/webhooks`, [method("POST"), body(JSON.stringify(PostWebhooksBodySchema.passthrough().parse(obj)))]]; }; /** * Returns the WebhookV2 corresponding to the ID provided, if it exists. */ -export const getWebhooks = (webhookId: string): TEndpointDec => { +export const getWebhooks = (webhookId: string): TEndpointDecTuple => { return [`/v2/webhooks/${webhookId}`, [method("GET")]]; }; @@ -53,7 +53,7 @@ export interface PutWebhooksBody extends z.infer { /** * Updates the webhook with the specified properties. */ -export const putWebhooks = (webhookId: string, obj?: PutWebhooksBody): TEndpointDec => { +export const putWebhooks = (webhookId: string, obj?: PutWebhooksBody): TEndpointDecTuple => { const transformers = [method("PUT")]; if (obj) { transformers.push(body(JSON.stringify(PutWebhooksBodySchema.passthrough().parse(obj)))); @@ -64,20 +64,20 @@ export const putWebhooks = (webhookId: string, obj?: PutWebhooksBody): TEndpoint /** * Deletes the specified webhook. This operation cannot be reversed. */ -export const deleteWebhooks = (webhookId: string): TEndpointDec => { +export const deleteWebhooks = (webhookId: string): TEndpointDecTuple => { return [`/v2/webhooks/${webhookId}`, [method("DELETE")]]; }; /** * Returns all webhooks registered under the specified team. */ -export const getTeamWebhooks = (teamId: string): TEndpointDec => { +export const getTeamWebhooks = (teamId: string): TEndpointDecTuple => { return [`/v2/teams/${teamId}/webhooks`, [method("GET")]]; }; /** * Returns all webhook requests sent within the last week. Useful for debugging. */ -export const getWebhooksRequests = (webhookId: string): TEndpointDec => { +export const getWebhooksRequests = (webhookId: string): TEndpointDecTuple => { return [`/v2/webhooks/${webhookId}/requests`, [method("GET")]]; }; diff --git a/apis/figma/src/index.ts b/apis/figma/src/index.ts index f31380a..8d0a547 100644 --- a/apis/figma/src/index.ts +++ b/apis/figma/src/index.ts @@ -15,13 +15,13 @@ import { header, json, prefix, - type TEndpointDec, + type TEndpointDecTuple, type TEndpointDeclarationFn, type TEndpointResFn, - type TTransformer, + type TFetchTransformer, } from "@zemd/http-client"; -export const figmaToken = (value: string): TTransformer => { +export const figmaToken = (value: string): TFetchTransformer => { return header("X-Figma-Token", value); }; @@ -29,7 +29,7 @@ export const figma = (accessToken: string, opts?: { debug?: boolean; url?: strin const build = >( fn: ArgFn, ): TEndpointResFn => { - const globalTransformers: Array = [ + const globalTransformers: Array = [ prefix(opts?.url ?? "https://api.figma.com"), json(), figmaToken(accessToken), @@ -39,8 +39,8 @@ export const figma = (accessToken: string, opts?: { debug?: boolean; url?: strin globalTransformers.push(debug()); } - const endpointDecFn = (...params: ArgFnParams): TEndpointDec => { - const [path, transformers]: TEndpointDec = fn(...params); + const endpointDecFn = (...params: ArgFnParams): TEndpointDecTuple => { + const [path, transformers]: TEndpointDecTuple = fn(...params); return [path, [...transformers, ...globalTransformers]]; }; diff --git a/apis/flickr/README.md b/apis/flickr/README.md index b929cff..3c4bd67 100644 --- a/apis/flickr/README.md +++ b/apis/flickr/README.md @@ -68,7 +68,6 @@ See documentation [https://www.flickr.com/services/api/](https://www.flickr.com/ - [ ] testimonials - [ ] urls - ## License `@zemd/flickr-rest-api` released under the Apache 2.0 license @@ -77,4 +76,3 @@ See documentation [https://www.flickr.com/services/api/](https://www.flickr.com/ [![](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/red_rabbit) [![](https://img.shields.io/static/v1?label=UNITED24&message=support%20Ukraine&color=blue)](https://u24.gov.ua/) - diff --git a/apis/flickr/package.json b/apis/flickr/package.json index d4d907e..de943dc 100644 --- a/apis/flickr/package.json +++ b/apis/flickr/package.json @@ -34,11 +34,11 @@ }, "devDependencies": { "@types/bun": "latest", - "@zemd/tsconfig": "^1.2.0", - "typescript": "^5.3.3" + "@zemd/tsconfig": "^1.3.0", + "typescript": "^5.5.4" }, "dependencies": { "@zemd/http-client": "^1.0.0", - "zod": "^3.22.4" + "zod": "^3.23.8" } } \ No newline at end of file diff --git a/apis/flickr/src/api/activity.ts b/apis/flickr/src/api/activity.ts index 85ddedf..aec38a1 100644 --- a/apis/flickr/src/api/activity.ts +++ b/apis/flickr/src/api/activity.ts @@ -1,4 +1,4 @@ -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; import { z } from "zod"; export const GetUserCommentsQuerySchema = z.object({ @@ -24,7 +24,7 @@ export interface GetUserCommentsQuery extends z.infer { +export const userComments = (params: GetUserCommentsQuery): TEndpointDecTuple => { return [ `/`, [ @@ -62,7 +62,7 @@ export const GetUserPhotosQuerySchema = z.object({ export interface GetUserPhotosQuery extends z.infer {} -export const userPhotos = (params: GetUserPhotosQuery): TEndpointDec => { +export const userPhotos = (params: GetUserPhotosQuery): TEndpointDecTuple => { return [ `/`, [ diff --git a/apis/flickr/src/api/photosets.ts b/apis/flickr/src/api/photosets.ts index 5209a99..788e9a9 100644 --- a/apis/flickr/src/api/photosets.ts +++ b/apis/flickr/src/api/photosets.ts @@ -1,4 +1,4 @@ -import { method, query, type TEndpointDec } from "@zemd/http-client"; +import { method, query, type TEndpointDecTuple } from "@zemd/http-client"; import { z } from "zod"; const PRIVACY_FILTER_PUBLIC_PHOTOS = "1"; @@ -54,7 +54,7 @@ export interface GetPhotosQuery extends z.infer {} /** * Get the list of photos in a set. */ -export const getPhotos = (params: GetPhotosQuery): TEndpointDec => { +export const getPhotos = (params: GetPhotosQuery): TEndpointDecTuple => { return [ `/`, [ @@ -75,7 +75,7 @@ export interface AddPhotoQuery extends z.infer {} /** * Add a photo to the end of an existing photoset. */ -export const addPhoto = (params: AddPhotoQuery): TEndpointDec => { +export const addPhoto = (params: AddPhotoQuery): TEndpointDecTuple => { return [ `/`, [ @@ -99,7 +99,7 @@ export interface CreateQuery extends z.infer { /** * Create a new photoset for the calling user. */ -export const createPhotoset = (params: CreateQuery): TEndpointDec => { +export const createPhotoset = (params: CreateQuery): TEndpointDecTuple => { return [ "/", [ @@ -119,7 +119,7 @@ export interface DeletePhotosetQuery extends z.infer { +export const deletePhotoset = (params: DeletePhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -141,7 +141,7 @@ export interface EditMetaPhotosetQuery extends z.infer { +export const editMeta = (params: EditMetaPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -171,7 +171,7 @@ export interface EditPhotosPhotosetQuery extends z.infer { +export const editPhotos = (params: EditPhotosPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -192,7 +192,7 @@ export interface GetContextPhotosetQuery extends z.infer { +export const getContext = (params: GetContextPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -213,7 +213,7 @@ export interface GetInfoPhotosetQuery extends z.infer { +export const getInfo = (params: GetInfoPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -268,7 +268,7 @@ export interface GetListPhotosetQuery extends z.infer { +export const getList = (params: GetListPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -292,7 +292,7 @@ export interface OrderSetsPhotosetQuery extends z.infer { +export const orderSets = (params: OrderSetsPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -313,7 +313,7 @@ export interface RemovePhotoPhotosetQuery extends z.infer { +export const removePhoto = (params: RemovePhotoPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -334,7 +334,7 @@ export interface RemovePhotosPhotosetQuery extends z.infer { +export const removePhotos = (params: RemovePhotosPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -356,7 +356,7 @@ export const ReorderPhotosPhotosetQuerySchema = z.object({ export interface ReorderPhotosPhotosetQuery extends z.infer {} -export const reorderPhotos = (params: ReorderPhotosPhotosetQuery): TEndpointDec => { +export const reorderPhotos = (params: ReorderPhotosPhotosetQuery): TEndpointDecTuple => { return [ "/", [ @@ -374,7 +374,7 @@ export interface SetPrimaryPhotoPhotosetQuery extends z.infer { +export const setPrimaryPhoto = (params: SetPrimaryPhotoPhotosetQuery): TEndpointDecTuple => { return [ "/", [ diff --git a/apis/flickr/src/index.ts b/apis/flickr/src/index.ts index 3a4f108..2f70d3b 100644 --- a/apis/flickr/src/index.ts +++ b/apis/flickr/src/index.ts @@ -6,17 +6,17 @@ import { json, prefix, query, - type TEndpointDec, + type TEndpointDecTuple, type TEndpointDeclarationFn, type TEndpointResFn, - type TTransformer, + type TFetchTransformer, } from "@zemd/http-client"; export const flickr = (apiKey: string, opts?: { url?: string; debug?: boolean }) => { const build = >( fn: ArgFn, ): TEndpointResFn => { - const globalTransformers: Array = [ + const globalTransformers: Array = [ prefix(opts?.url ?? "https://api.flickr.com/services/rest"), query({ api_key: apiKey, format: "json", nojsoncallback: 1 }), json(), @@ -26,8 +26,8 @@ export const flickr = (apiKey: string, opts?: { url?: string; debug?: boolean }) globalTransformers.push(debug()); } - const endpointDecFn = (...params: ArgFnParams): TEndpointDec => { - const [path, transformers]: TEndpointDec = fn(...params); + const endpointDecFn = (...params: ArgFnParams): TEndpointDecTuple => { + const [path, transformers]: TEndpointDecTuple = fn(...params); return [path, [...transformers, ...globalTransformers]]; }; diff --git a/bun.lockb b/bun.lockb index f38e291eadc26a8162623c56f806f39a48af2d80..410a60646b12b38360d406bb8710dfacfc5ac2a3 100755 GIT binary patch delta 41917 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 delta 51540 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" }, - "packageManager": "bun@1.0.26", + "packageManager": "bun@1.1.21", "workspaces": [ "apis/*", "packages/*" diff --git a/packages/http-client/README.md b/packages/http-client/README.md index 54d3bbe..49bff99 100644 --- a/packages/http-client/README.md +++ b/packages/http-client/README.md @@ -6,10 +6,10 @@ providing an api around native fetch(but can be overridden by setting your calli ## Installation ```sh -bun install @zemd/http-client +bun add @zemd/http-client npm install @zemd/http-client yarn install @zemd/http-client -pnpm install @zemd/http-client +pnpm add @zemd/http-client ``` ## Usage @@ -17,10 +17,7 @@ pnpm install @zemd/http-client ```ts import { compose, method, json } from "@zemd/http-client"; -const myfetch = compose([ - method("POST"), - json(), -], fetch); +const myfetch = compose([method("POST"), json()], fetch); const resp = await myfetch("https://example.com"); ``` @@ -30,6 +27,7 @@ together and creating your client. A real example you can find in `../apis/` folder. +An example of how you can use the library in your project see [src/example.ts](./src/example.ts) ## License @@ -39,4 +37,3 @@ A real example you can find in `../apis/` folder. [![](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/red_rabbit) [![](https://img.shields.io/static/v1?label=UNITED24&message=support%20Ukraine&color=blue)](https://u24.gov.ua/) - diff --git a/packages/http-client/package.json b/packages/http-client/package.json index d5f47d2..28ae052 100644 --- a/packages/http-client/package.json +++ b/packages/http-client/package.json @@ -33,7 +33,7 @@ "dev": "tsc --watch" }, "devDependencies": { - "@zemd/tsconfig": "^1.2.0", - "typescript": "^5.3.3" + "@zemd/tsconfig": "^1.3.0", + "typescript": "^5.5.4" } } diff --git a/packages/http-client/src/example.ts b/packages/http-client/src/example.ts new file mode 100644 index 0000000..fdb563c --- /dev/null +++ b/packages/http-client/src/example.ts @@ -0,0 +1,96 @@ +import { + body, + createBuildEndpointFn, + method, + type TEndpointDecTuple, + type TFetchFn, + type TFetchFnParams, + type TFetchTransformer, +} from "./index.js"; + +type Token = { + access_token: string; + scope: string; + expires_at: number; +}; + +const isExpired = (token: Token): boolean => { + return token.expires_at - Date.now() < 0; +}; + +const tokenEndpoint = (payload: { + client_id: string; + client_secret: string; + audience: string; + //... +}): TEndpointDecTuple => { + return ["/token", [method("POST"), body(JSON.stringify(payload))]]; +}; + +const createOAuth2Client = (opts: { + debug?: boolean; + credentials: { client_id: string; client_secret: string; audience: string }; +}) => { + const build = createBuildEndpointFn({ + baseUrl: "https://auth.example.com/oauth", + debug: !!opts.debug, + exractJsonFromResponse: true, + }); + + let cachedToken: Token | null = null; + const fetchToken = build(tokenEndpoint, (json: any): Token => { + return json; + }); + + return { + token: async (skipCache: boolean = false): Promise => { + if (!cachedToken || skipCache || isExpired(cachedToken)) { + const data = await fetchToken(opts.credentials); + cachedToken = data; + } + return cachedToken; + }, + }; +}; + +const bearerToken = (oauthClient: ReturnType): TFetchTransformer => { + return async (fetchFn: TFetchFn, ...args: TFetchFnParams) => { + const [input, init] = args; + const token = await oauthClient.token(); + return fetchFn(input, { + ...init, + headers: { + ...init?.headers, + Authorization: `Bearer ${token.access_token}`, + }, + }); + }; +}; + +function apiAction(param: number): TEndpointDecTuple { + return [`/some/path/${param}`, [method("GET")]]; +} + +const createApiSdk = (oauthClient: ReturnType, opts: { debug?: boolean } = {}) => { + const build = createBuildEndpointFn({ + baseUrl: "https://api.example.com", + transformers: [bearerToken(oauthClient)], + debug: !!opts.debug, + exractJsonFromResponse: false, + }); + return { + someAction: build(apiAction), + }; +}; + +const oauth = createOAuth2Client({ + credentials: { + audience: "test", + client_id: "asdasd", + client_secret: "asdasdasd", + }, +}); +const sdk = createApiSdk(oauth, {}); + +// @ts-ignore +const result = await sdk.someAction(123); diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index f53ec51..7da1bc3 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -1,6 +1,6 @@ export type TFetchFn = typeof fetch; export type TFetchFnParams = Parameters; -export type TTransformer = (fetchFn: TFetchFn, ...params: TFetchFnParams) => ReturnType; +export type TFetchTransformer = (fetchFn: TFetchFn, ...params: TFetchFnParams) => ReturnType; /** * Compose a list of transformers into a single fetch function. @@ -13,7 +13,7 @@ export type TTransformer = (fetchFn: TFetchFn, ...params: TFetchFnParams) => Ret * ], fetch); * ``` */ -export const compose = (transformers: Array, fetchFn: TFetchFn = fetch): TFetchFn => { +export const compose = (transformers: Array, fetchFn: TFetchFn = fetch): TFetchFn => { return transformers.reduceRight((acc, transformer) => { const res: TFetchFn = (...params: TFetchFnParams) => { return transformer(acc, ...params); @@ -22,7 +22,7 @@ export const compose = (transformers: Array, fetchFn: TFetchFn = f }, fetchFn); }; -export const method = (name: string): TTransformer => { +export const method = (name: string): TFetchTransformer => { return (fetchFn: TFetchFn, ...params: TFetchFnParams) => { const [input, init] = params; return fetchFn(input, { @@ -32,7 +32,7 @@ export const method = (name: string): TTransformer => { }; }; -export const header = (key: string, value: string): TTransformer => { +export const header = (key: string, value: string): TFetchTransformer => { return (fetchFn: TFetchFn, ...params: TFetchFnParams) => { const [input, init] = params; return fetchFn(input, { @@ -45,7 +45,7 @@ export const header = (key: string, value: string): TTransformer => { }; }; -export const json = (): TTransformer => { +export const json = (): TFetchTransformer => { return (fetchFn: TFetchFn, ...params: TFetchFnParams) => { const [input, init] = params; return fetchFn(input, { @@ -110,7 +110,7 @@ const modifyUrlQuery = (input: TFetchFnParams[0], query: object): TFetchFnParams return `${pathname}?${new URLSearchParams([...new URLSearchParams(search).entries(), ...Object.entries(query)])}`; }; -export const prefix = (value: string): TTransformer => { +export const prefix = (value: string): TFetchTransformer => { return (fetchFn: TFetchFn, ...params: TFetchFnParams) => { const [input, init] = params; @@ -118,14 +118,14 @@ export const prefix = (value: string): TTransformer => { }; }; -export const query = (obj: object): TTransformer => { +export const query = (obj: object): TFetchTransformer => { return (fetchFn: TFetchFn, ...params: TFetchFnParams) => { const [input, init] = params; return fetchFn(modifyUrlQuery(input, obj), init); }; }; -export const body = (obj: BodyInit): TTransformer => { +export const body = (obj: BodyInit): TFetchTransformer => { return (fetchFn: TFetchFn, ...params: TFetchFnParams) => { const [input, init] = params; return fetchFn(input, { @@ -135,21 +135,27 @@ export const body = (obj: BodyInit): TTransformer => { }; }; -export const debug = (fn: Function = (p: any) => console.debug(JSON.stringify(p, null, 4))): TTransformer => { +export const debug = (fn: Function = (p: any) => console.debug(JSON.stringify(p, null, 4))): TFetchTransformer => { return async (fetchFn: TFetchFn, ...params: TFetchFnParams) => { fn(params); return fetchFn(...params); }; }; -export type TEndpointDec = [string, Array]; +export type TEndpointDecTuple /**/ = [string, Array, ...any[]]; // [url, transformers, ...rest] -export type TEndpointDeclarationFn = (...params: Array) => TEndpointDec; +export type TEndpointDeclarationFn = (...params: Array) => TEndpointDecTuple /**/; -export type TEndpointResFn> = ( +export type TEndpointResFn, ArgResult = Response> = ( ...params: ArgEndpointDeclarationFnParams -) => Promise; +) => Promise; +/** + * Endpoint is a function identical to fetch with pre-defined parameters and input arguments. + * Endpoint function follow the fetch logic, so it is unaware of the type that .json() method returns. + * If you want have additional type-checking for the response, you can create another wrapper. + * An example of such a wrapper see `createBuildEndpointFn` function. + */ export const endpoint = < ArgEndpointDeclarationFn extends TEndpointDeclarationFn, ArgEndpointDeclarationFnParams extends Parameters, @@ -158,9 +164,63 @@ export const endpoint = < fn: ArgEndpointDeclarationFn, ): ArgResFn => { const res = (...params: ArgEndpointDeclarationFnParams): Promise => { - const [url, transformers] = fn(...params); + const declaration = fn(...params); + const [url, transformers] = declaration; const fetchFn = compose(transformers, fetch); return fetchFn(url); }; return res as ArgResFn; }; + +type TCreateBuildOptions = { + baseUrl: string; + transformers?: Array; + debug?: boolean; + exractJsonFromResponse?: ArgExtractJsonType; +}; + +/** + * Creates a build function that wraps endpoint function and provides common transformers, + * and handles some configuration options that can ease the development. + * Is not required to use, and can be used as an example for custom build function. + */ +export const createBuildEndpointFn = ({ + baseUrl, + exractJsonFromResponse, + ...opts +}: TCreateBuildOptions) => { + return < + ArgFn extends TEndpointDeclarationFn, + ArgMapOrValidateFn extends (...args: any[]) => any, + ArgResult extends ArgExtractJsonType extends true + ? ArgMapOrValidateFn extends undefined + ? never + : ReturnType + : Response, + >( + fn: ArgFn, + mapOrValidateJSON?: ArgMapOrValidateFn, + ): TEndpointResFn, ArgResult> => { + const commonTransformers: Array = [prefix(baseUrl), json()]; + if (opts.debug) { + commonTransformers.push(debug()); + } + + const endpointDecFn = (...params: Parameters): TEndpointDecTuple => { + const [path, transformers, ...rest] = fn(...params); + return [path, [...commonTransformers, ...(opts.transformers ?? []), ...transformers], ...rest]; + }; + + if (exractJsonFromResponse) { + return async (...params: Parameters): Promise => { + const res = await endpoint(endpointDecFn as ArgFn)(...params); + if (mapOrValidateJSON) { + return mapOrValidateJSON(await res.json()); + } + return res.json() as ArgResult; + }; + } + + return endpoint(endpointDecFn as ArgFn) as TEndpointResFn, ArgResult>; + }; +}; diff --git a/turbo.json b/turbo.json index 781f274..754fad8 100644 --- a/turbo.json +++ b/turbo.json @@ -1,7 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], - "pipeline": { + "tasks": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**"]