|
| 1 | +# pinia-colada-graphql |
| 2 | + |
| 3 | +> **EXPERIMENTAL**: This library is in early development and the API may change significantly. Use at your own risk. |
| 4 | +
|
| 5 | +GraphQL integration for [Pinia Colada](https://github.com/posva/pinia-colada) with normalized cache support. |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +- Relay-style DX with data masking via `useFragment` |
| 10 | +- Apollo-style normalized cache |
| 11 | +- Type-safe GraphQL operations with `TypedDocumentNode` |
| 12 | +- Optimistic updates |
| 13 | +- Async dynamic headers (for auth token injection) |
| 14 | +- `.graphql` file loader via unplugin |
| 15 | + |
| 16 | +## Packages |
| 17 | + |
| 18 | +| Package | Description | |
| 19 | +| --------------------------------------- | ----------------------------------------------- | |
| 20 | +| `@pinia-colada-graphql/core` | Core composables, client, and cache | |
| 21 | +| `@pinia-colada-graphql/unplugin` | Vite/Rollup/esbuild plugin for `.graphql` files | |
| 22 | +| `@pinia-colada-graphql/tagged-template` | `graphql` tagged template literal | |
| 23 | + |
| 24 | +## Installation |
| 25 | + |
| 26 | +comming soon... |
| 27 | + |
| 28 | +```bash |
| 29 | +bun add @pinia-colada-graphql/core @pinia/colada pinia graphql |
| 30 | +``` |
| 31 | + |
| 32 | +## Quick Start |
| 33 | + |
| 34 | +### Setup |
| 35 | + |
| 36 | +```typescript |
| 37 | +import { createApp } from "vue"; |
| 38 | +import { createPinia } from "pinia"; |
| 39 | +import { PiniaColada } from "@pinia/colada"; |
| 40 | +import { PiniaColadaGraphQL } from "@pinia-colada-graphql/core"; |
| 41 | + |
| 42 | +const app = createApp(App); |
| 43 | +const pinia = createPinia(); |
| 44 | + |
| 45 | +app.use(pinia); |
| 46 | +app.use(PiniaColada); |
| 47 | +app.use(PiniaColadaGraphQL, { |
| 48 | + client: { |
| 49 | + url: "/graphql", |
| 50 | + // Dynamic headers for auth (async supported) |
| 51 | + headers: async () => { |
| 52 | + const token = await getAuthToken(); |
| 53 | + return { |
| 54 | + Authorization: `Bearer ${token}`, |
| 55 | + }; |
| 56 | + }, |
| 57 | + }, |
| 58 | + typePolicies: { |
| 59 | + User: { keyFields: ["id"] }, |
| 60 | + Post: { keyFields: ["id"] }, |
| 61 | + }, |
| 62 | +}); |
| 63 | +``` |
| 64 | + |
| 65 | +### Query |
| 66 | + |
| 67 | +```typescript |
| 68 | +import { gql } from "@pinia-colada-graphql/tagged-template"; |
| 69 | +import { useGraphQLQuery } from "@pinia-colada-graphql/core"; |
| 70 | + |
| 71 | +const GetUsersQuery = gql` |
| 72 | + query GetUsers { |
| 73 | + users { |
| 74 | + id |
| 75 | + name |
| 76 | + email |
| 77 | + } |
| 78 | + } |
| 79 | +`; |
| 80 | + |
| 81 | +const { data, isLoading, error, refetch } = useGraphQLQuery({ |
| 82 | + document: GetUsersQuery, |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +### Mutation |
| 87 | + |
| 88 | +```typescript |
| 89 | +import { useGraphQLMutation } from "@pinia-colada-graphql/core"; |
| 90 | + |
| 91 | +const CreateUserMutation = gql` |
| 92 | + mutation CreateUser($name: String!, $email: String!) { |
| 93 | + createUser(name: $name, email: $email) { |
| 94 | + id |
| 95 | + name |
| 96 | + email |
| 97 | + } |
| 98 | + } |
| 99 | +`; |
| 100 | + |
| 101 | +const { mutate, isLoading } = useGraphQLMutation({ |
| 102 | + document: CreateUserMutation, |
| 103 | + // Invalidate queries after mutation |
| 104 | + invalidateQueries: [["graphql", "GetUsers"]], |
| 105 | + // Optimistic update |
| 106 | + optimisticResponse: (variables) => ({ |
| 107 | + createUser: { |
| 108 | + __typename: "User", |
| 109 | + id: "temp-id", |
| 110 | + ...variables, |
| 111 | + }, |
| 112 | + }), |
| 113 | +}); |
| 114 | + |
| 115 | +// Execute mutation |
| 116 | +await mutate({ name: "Alice", email: "alice@example.com" }); |
| 117 | +``` |
| 118 | + |
| 119 | +### Fragment (Data Masking) |
| 120 | + |
| 121 | +```typescript |
| 122 | +import { useFragment, useGraphQLQuery } from "@pinia-colada-graphql/core"; |
| 123 | + |
| 124 | +// Parent component fetches data |
| 125 | +const UserFragment = gql` |
| 126 | + fragment UserFields on User { |
| 127 | + id |
| 128 | + name |
| 129 | + avatar |
| 130 | + } |
| 131 | +`; |
| 132 | + |
| 133 | +const GetUserQuery = gql` |
| 134 | + query GetUser($id: ID!) { |
| 135 | + user(id: $id) { |
| 136 | + ...UserFields |
| 137 | + } |
| 138 | + } |
| 139 | + ${UserFragment} |
| 140 | +`; |
| 141 | + |
| 142 | +// In parent |
| 143 | +const { data } = useGraphQLQuery({ |
| 144 | + document: GetUserQuery, |
| 145 | + variables: { id: "1" }, |
| 146 | +}); |
| 147 | + |
| 148 | +// In child component - unmask fragment data |
| 149 | +const user = useFragment(UserFragment, () => props.userRef); |
| 150 | +``` |
| 151 | + |
| 152 | +## Vite Plugin |
| 153 | + |
| 154 | +```typescript |
| 155 | +// vite.config.ts |
| 156 | +import { defineConfig } from "vite"; |
| 157 | +import vue from "@vitejs/plugin-vue"; |
| 158 | +import graphql from "@pinia-colada-graphql/unplugin/vite"; |
| 159 | + |
| 160 | +export default defineConfig({ |
| 161 | + plugins: [vue(), graphql()], |
| 162 | +}); |
| 163 | +``` |
| 164 | + |
| 165 | +Then import `.graphql` files directly: |
| 166 | + |
| 167 | +```typescript |
| 168 | +import GetUsersQuery from "./queries/GetUsers.graphql"; |
| 169 | +``` |
| 170 | + |
| 171 | +## Dynamic Headers |
| 172 | + |
| 173 | +The client supports async header functions for dynamic auth token injection: |
| 174 | + |
| 175 | +```typescript |
| 176 | +PiniaColadaGraphQL({ |
| 177 | + client: { |
| 178 | + url: "/graphql", |
| 179 | + headers: async (context) => { |
| 180 | + // Fetch token from auth provider |
| 181 | + const token = await authProvider.getAccessToken(); |
| 182 | + return { |
| 183 | + Authorization: `Bearer ${token}`, |
| 184 | + "X-Operation": context.operationName ?? "anonymous", |
| 185 | + }; |
| 186 | + }, |
| 187 | + }, |
| 188 | +}); |
| 189 | +``` |
| 190 | + |
| 191 | +## Normalized Cache |
| 192 | + |
| 193 | +Entities are automatically normalized and cached: |
| 194 | + |
| 195 | +```typescript |
| 196 | +// After fetching users, the cache contains: |
| 197 | +// User:1 -> { __typename: 'User', id: '1', name: 'Alice', ... } |
| 198 | +// User:2 -> { __typename: 'User', id: '2', name: 'Bob', ... } |
| 199 | + |
| 200 | +// Read/write directly to cache |
| 201 | +const cache = useNormalizedCache(); |
| 202 | + |
| 203 | +// Read entity |
| 204 | +const user = cache.readFragment({ __typename: "User", id: "1" }); |
| 205 | + |
| 206 | +// Write entity |
| 207 | +cache.writeFragment({ __typename: "User", id: "1" }, { name: "Updated" }); |
| 208 | + |
| 209 | +// Evict entity |
| 210 | +cache.evict({ __typename: "User", id: "1" }); |
| 211 | + |
| 212 | +// Clear all |
| 213 | +cache.clear(); |
| 214 | +``` |
| 215 | + |
| 216 | +## Development |
| 217 | + |
| 218 | +```bash |
| 219 | +# Install dependencies |
| 220 | +bun install |
| 221 | + |
| 222 | +# Build all packages |
| 223 | +bun run build |
| 224 | + |
| 225 | +# Run playground |
| 226 | +bun run dev |
| 227 | + |
| 228 | +# Type check |
| 229 | +bun run typecheck |
| 230 | + |
| 231 | +# Lint |
| 232 | +bun run lint |
| 233 | +``` |
| 234 | + |
| 235 | +## License |
| 236 | + |
| 237 | +MIT |
0 commit comments