diff --git a/.gitignore b/.gitignore index d8df4a5421..2372d9f0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ yarn-error.log* .yarn/ .idea/ -src/*/*.json \ No newline at end of file +src/*/*.json +.vscode/ diff --git a/docs/guides/client-tuning.md b/docs/guides/client-tuning.md index d8b94a24d0..54fd3f7b9d 100644 --- a/docs/guides/client-tuning.md +++ b/docs/guides/client-tuning.md @@ -1,5 +1,6 @@ --- -title: Tuning Client for Performance +title: Client Tuning +description: Tuning client for performance --- ### HTTP (Hypertext Transfer Protocol) diff --git a/docs/guides/http-cache.md b/docs/guides/http-cache.md new file mode 100644 index 0000000000..4477df0106 --- /dev/null +++ b/docs/guides/http-cache.md @@ -0,0 +1,56 @@ +--- +title: Http Cache +description: A comprehensive guide to leverage HTTP cache for REST APIs using Tailcall +--- + +HTTP Caching in Tailcall is designed to enhance performance and minimize the frequency of requests to upstream services by caching HTTP responses. This guide explains the concept, benefits, and how to effectively implement HTTP caching within Tailcall. + +### Understanding HTTP Caching + +HTTP Caching involves saving copies of HTTP responses to serve identical future requests directly from the cache, bypassing the need for new API calls. This reduces latency, conserves bandwidth, and alleviates the load on upstream services by utilizing a cache keyed by request URLs and headers. + +By default, HTTP caching is turned off in Tailcall. Enabling it requires setting the `httpCache` parameter to `true` in the `@upstream` configuration. Tailcall employs a in-memory _Least_Recently_Used_ (LRU) cache mechanism to manage stored responses, adhering to upstream-provided caching directives like `Cache-Control` to optimize the caching process and minimize redundant upstream API requests. + +### Enabling HTTP Caching + +To activate HTTP caching, adjust the upstream configuration in Tailcall by setting `httpCache` to `true`, as shown in the following example: + +```graphql +schema + @server(port: 4000) + @upstream( + baseURL: "https://api.example.com" + # highlight-start + httpCache: true + # highlight-end + ) { + query: Query +} +``` + +This configuration instructs Tailcall to cache responses from the designated upstream API. + +### Cache-Control headers in responses + +Enabling the `cacheControlHeader` setting in Tailcall ensures that [Cache-Control] headers are included in the responses returned to clients. When activated, Tailcall dynamically sets the `max-age` directive in the `Cache-Control` header to the minimum `max-age` value encountered in any of the responses from upstream services. This approach guarantees that the caching duration for the composite response is conservative, aligning with the shortest cache validity period provided by the upstream services. By default, this feature is disabled (`false`), meaning Tailcall will not modify or add `Cache-Control` headers unless explicitly instructed to do so. This setting is distinct from the general HTTP cache setting, which controls whether responses are cached internally by Tailcall; `cacheControlHeader` specifically controls the caching instructions sent to clients. + +Here is how you can enable the `cacheControlHeader` setting within your Tailcall schema to apply these caching instructions: + +```graphql +schema @server(cacheControlHeader: true) { + query: Query + mutation: Mutation +} +``` + +[cache-control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + +### Best Practices for Enhancing REST API Performance with Tailcall + +The combination of `httpCache` and `cacheControlHeader` provides a comprehensive caching solution. While `httpCache` focuses on internal caching to reduce the impact of high latency and frequent requests, `cacheControlHeader` manages client-side caching policies, ensuring an optimal balance between performance, data freshness, and efficient resource use. + +These caching primitives are beneficial for REST APIs that are latency-sensitive, have a high rate of request repetition, or come with explicit caching headers indicating cacheable responses. Together, they tackle the common challenges of optimizing REST API performance by minimizing unnecessary network traffic and server load while ensuring response accuracy. + +To further enhance the performance of any API with Tailcall, integrating the [@cache] directive offers protocol agnostic control over caching at the field level within a GraphQL schema. + +[@cache]: /docs/operators/cache.md diff --git a/docs/guides/operator-composition.md b/docs/guides/operator-composition.md deleted file mode 100644 index c8d81e0941..0000000000 --- a/docs/guides/operator-composition.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Operator Composition" ---- - -# Composition - -You can combine operators to create new and powerful transformations. - -This example demonstrates the concept of composition in GraphQL, allowing the combination of operations (known as "operators") to construct more complex data transformations. - -The given schema is defining two data types - `User` and `Post`. The `User` type has fields `id` and `name`, and the `Post` type initially has fields `user` and `userId`. - -```graphql showLineNumbers -type User { - id: Int - name: String -} - -type Post @addField(name: "userName", path: ["user", "name"]) { - user: User @modify(omit: true) @http(path: "/users/{{userId}}") - userId: Int! -} -``` - -It uses a series of operators to modify the `user` field. - -1. The `@addField(name: "userName", path: ["user", "name"])` operator extracts the `name` field from `user` and adds a field called `userName` to the `Post` - -2. The `@modify(omit: true)` operator removes the `user` field from the final Schema. - -3. The `@http(path: "/users/{{userId}}")` operator instructs the resolver to make an HTTP request to fetch the user data from a specified path (i.e., `/users/{{userId}}`), with `{{userId}}` serving as a placeholder that the system replaces with the actual `userId` upon making the request. - -The schema after this transformation looks like this: - -```graphql showLineNumbers -type User { - id: Int - name: String -} - -type Post { - userName: String - userId: Int! -} -``` - -We've used composition of operators to take a complex object (the `User` inside the `Post`), extract a specific part of it (`name`), name that part (`userName`), and then instruct GraphQL how to fetch the data using an HTTP request. - -:::info -It's important to note that the order of the operators `@modify` and `@http` doesn't matter. The resulting schema will always be the same. -::: - -This is a powerful mechanism that allows you to make your GraphQL schema more precise, easier to understand, and more suitable for the specific needs of your application. diff --git a/docs/operators/index.md b/docs/operators/index.md index 7d4d406e1c..c8879c1de1 100644 --- a/docs/operators/index.md +++ b/docs/operators/index.md @@ -18,6 +18,7 @@ Certainly! Here's the table with hyperlinks added back to the operator names: | [@graphQL](graphql.md) | Resolves a field or node by a GraphQL API. | | [@grpc](grpc.md) | Resolves a field or node by a gRPC API. | | [@http](http.md) | Resolves a field or node by a REST API. | +| [@link](link.md) | Imports external resources such as config files, certs, protobufs, etc in the schema. | | [@modify](modify.md) | Enables changes to attributes of fields or nodes in the schema. | | [@omit](omit.md) | Excludes fields or nodes from the generated schema, making them inaccessible through the GraphQL API. | | [@server](server.md) | Provides server configurations for behavior tuning and tailcall optimization in specific use-cases. | diff --git a/docs/operators/link.md b/docs/operators/link.md new file mode 100644 index 0000000000..f060eba9ec --- /dev/null +++ b/docs/operators/link.md @@ -0,0 +1,68 @@ +--- +title: "@link" +--- + +The **@link** operator is used for bringing external resources into your GraphQL schema. It makes it easier to include configurations, .proto files for gRPC services, and other files into your schema. With this operator, external resources are either merged with or used effectively in the importing configuration. + +## How it Works + +The `@link` directive requires specifying a source `src`, the resource's type `type`, and an optional identifier `id`. + +- `src`: The source of the link is defined here. It can be either a URL or a file path. When a file path is given, it's relative to the file's location that is importing the link. + +- `type`: This specifies the link's type, which determines how the imported resource is integrated into the schema. For a list of supported types, see the [Supported Types](#supported-types) section. + +- `id`: This is an optional field that assigns a unique identifier to the link. It's helpful for referring to the link within the schema. + +## Example + +The following example illustrates how to utilize the `@link` directive to incorporate a Protocol Buffers (.proto) file for a gRPC service into your GraphQL schema. + +```graphql showLineNumbers +schema + @server(port: 8000, graphiql: true) + @upstream(baseURL: "http://news.local", httpCache: true, batch: {delay: 10}) + @link(id: "news", src: "../src/grpc/news.proto", type: Protobuf) { + query: Query +} + +type Query { + news: NewsData! @grpc(method: "news.NewsService.GetAllNews") +} + +type News { + id: Int + title: String + body: String + postImage: String +} + +type NewsData { + news: [News]! +} +``` + +## Supported Types + +The `@link` directive supports the following types of links: + +- `Config`: Imports a schema configuration file. During the merge, settings from the imported file override those in the main schema for any overlaps, facilitating a modular and scalable approach to schema configuration. The operation is morally equivalent to tailcall's [compose](/docs/guides/cli.md#compose) command. + +- `Protobuf`: Imports a .proto file for gRPC services. This type facilitates the integration of gRPC services into your GraphQL schema by allowing the inclusion of Protocol Buffers definitions. It enables the GraphQL server to communicate with gRPC services directly. For integrating gRPC services, refer to [gRPC Integration Guide](/docs/guides/grpc.md). + +- `Script`: A link to an external JavaScript file that listens on every HTTP request response event. This allows for the execution of custom logic or filters based on the request and response. Example usage: + + ```javascript showLineNumbers + function onRequest({request}) { + // Add a custom header for all outgoing responses + request.headers["X-Custom-Header"] = "Processed" + + // Return the updated request + return {request} + } + ``` + +- `Cert`: Imports a SSL/TLS certificate for HTTPS. +- `Key`: Imports a SSL/TLS private key for HTTPS. + +Each type serves a specific purpose, enabling the flexible integration of external resources into your GraphQL schema. diff --git a/src/components/contact/Hello.tsx b/src/components/contact/Hello.tsx index b9cbc84999..a8e5e84834 100644 --- a/src/components/contact/Hello.tsx +++ b/src/components/contact/Hello.tsx @@ -3,15 +3,20 @@ import Heading from "@theme/Heading" import toast, {Toaster} from "react-hot-toast" import Grid from "@site/static/images/about/grid-large.svg" import LinkButton from "../shared/LinkButton" -import {analyticsHandler} from "@site/src/utils" +import {analyticsHandler, validateEmail} from "@site/src/utils" import {Theme, radioOptions, zapierLink} from "@site/src/constants" const Hello = (): JSX.Element => { const [email, setEmail] = useState("") const [message, setMessage] = useState("") const [stage, setStage] = useState("") + const [isValid, setIsValid] = useState(true) const sendData = useCallback(async () => { + if (!validateEmail(email)) { + setIsValid(false) + return + } const response = await fetch(zapierLink, { method: "POST", body: JSON.stringify({ @@ -31,6 +36,7 @@ const Hello = (): JSX.Element => { setEmail("") setMessage("") setStage("") + setIsValid(true) } }, [email, message, stage]) @@ -56,10 +62,17 @@ const Hello = (): JSX.Element => { name="email" type="email" value={email} - onChange={(e) => setEmail(e.target.value)} - className="border border-solid border-tailCall-border-light-500 rounded-lg font-space-grotesk h-11 w-[95%] sm:w-[480px] p-SPACE_03 text-content-small outline-none focus:border-x-tailCall-light-700" + onChange={(e) => { + setEmail(e.target.value) + if (!isValid) setIsValid(true) + }} + className={`border border-solid border-tailCall-border-light-500 rounded-lg font-space-grotesk h-11 w-[95%] sm:w-[480px] + p-SPACE_03 text-content-small outline-none focus:border-x-tailCall-light-700 ${ + isValid ? "is-valid" : "is-invalid" + }`} placeholder="you@company.com" /> + {!isValid &&
Please enter a valid email.
}
diff --git a/src/components/home/Configuration.tsx b/src/components/home/Configuration.tsx index 9241db7e39..7c74c7cad6 100644 --- a/src/components/home/Configuration.tsx +++ b/src/components/home/Configuration.tsx @@ -1,6 +1,8 @@ import React from "react" import Heading from "@theme/Heading" import CodeBlock from "@theme/CodeBlock" +import Tabs from "@theme/Tabs" +import TabItem from "@theme/TabItem" import Link from "@docusaurus/Link" const Configuration = (): JSX.Element => { @@ -17,10 +19,33 @@ const Configuration = (): JSX.Element => {
npm i -g @tailcallhq/tailcall - - {`# app.graphql -schema + + {CodeTabItem({code: GRAPHQL_CONFIG, language: "graphql"})} + {CodeTabItem({code: YML_CONFIG, language: "yaml"})} + {CodeTabItem({code: JSON_CONFIG, language: "json"})} + +
+ + ) +} + +const CodeTabItem = ({code, language}: {code: string; language: "json" | "yaml" | "graphql"}) => ( + + + {code} + + tailcall start ./app.{language} + +) + +export default Configuration + +const GRAPHQL_CONFIG = `schema @server(port: 8000, graphiql: true) @upstream(baseURL: "http://jsonplaceholder.typicode.com") { query: Query @@ -48,12 +73,166 @@ type Post { # Expand a post with user information user: User @http(path: "/users/{{value.userId}}") } - `} - - tailcall start ./app.graphql - - - ) -} +` -export default Configuration +const YML_CONFIG = `server: + graphiql: true + port: 8000 +upstream: + baseURL: http://jsonplaceholder.typicode.com +schema: + query: Query +types: + Post: + fields: + body: + type: String + required: true + cache: null + id: + type: Int + required: true + cache: null + title: + type: String + required: true + cache: null + user: + type: User + http: + path: /users/{{value.userId}} + cache: null + userId: + type: Int + required: true + cache: null + cache: null + Query: + fields: + posts: + type: Post + list: true + http: + path: /posts + cache: null + users: + type: User + list: true + http: + path: /users + cache: null + cache: null + User: + fields: + email: + type: String + required: true + cache: null + id: + type: Int + required: true + cache: null + name: + type: String + required: true + cache: null + username: + type: String + required: true + cache: null + cache: null +` + +const JSON_CONFIG = `{ + "server": { + "graphiql": true, + "port": 8000 + }, + "upstream": { + "baseURL": "http://jsonplaceholder.typicode.com" + }, + "schema": { + "query": "Query" + }, + "types": { + "Post": { + "fields": { + "body": { + "type": "String", + "required": true, + "cache": null + }, + "id": { + "type": "Int", + "required": true, + "cache": null + }, + "title": { + "type": "String", + "required": true, + "cache": null + }, + "user": { + "type": "User", + "http": { + "path": "/users/{{value.userId}}" + }, + "cache": null + }, + "userId": { + "type": "Int", + "required": true, + "cache": null + } + }, + "cache": null + }, + "Query": { + "fields": { + "posts": { + "type": "Post", + "list": true, + "http": { + "path": "/posts" + }, + "cache": null + }, + "users": { + "type": "User", + "list": true, + "http": { + "path": "/users" + }, + "cache": null + } + }, + "cache": null + }, + "User": { + "fields": { + "email": { + "type": "String", + "required": true, + "cache": null + }, + "id": { + "type": "Int", + "required": true, + "cache": null + }, + "name": { + "type": "String", + "required": true, + "cache": null + }, + "username": { + "type": "String", + "required": true, + "cache": null + } + }, + "cache": null + } + } +} +` diff --git a/src/components/home/LegacyGateway.tsx b/src/components/home/LegacyGateway.tsx index 26c90abe47..0927f42366 100644 --- a/src/components/home/LegacyGateway.tsx +++ b/src/components/home/LegacyGateway.tsx @@ -32,11 +32,7 @@ const LegacyGateway = (): JSX.Element => {
- +
) diff --git a/src/css/custom.css b/src/css/custom.css index 99436b8554..61437a1fcd 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -55,7 +55,7 @@ } code { - border: none; + border: 1px solid #eee; letter-spacing: 0.00001px; vertical-align: baseline; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 54b0fc01da..28e61c85e4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -19,3 +19,8 @@ export const setBodyOverflow = (value: "initial" | "hidden") => { export const getSearchInputRef = () => { return document.getElementById("search_input_react") } + +export const validateEmail = (email: string) => { + const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + return regex.test(email) +}