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 f38e291..410a606 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 82d8936..feec2a0 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ "release": "bun run build && changeset publish" }, "devDependencies": { - "@changesets/cli": "^2.27.1", + "@changesets/cli": "^2.27.7", "@repo/eslint-config": "*", - "@zemd/tsconfig": "^1.2.0", - "prettier": "^3.1.1", + "@zemd/tsconfig": "^1.3.0", + "prettier": "^3.3.3", "turbo": "latest" }, "engines": { "node": ">=20" }, - "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/**"]