@carvajalconsultants/headstart 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -28
- package/grafastExchange.ts +61 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,15 +16,15 @@ This library is made up of:
|
|
|
16
16
|
Specifically, the `graphile.config.ts` and `pgl.ts`. If you need WebSocket support, make sure to add a plugin in `graphile.config.ts` as per: https://postgraphile.org/postgraphile/next/subscriptions
|
|
17
17
|
|
|
18
18
|
3. Install Headtart & URQL:
|
|
19
|
-
```
|
|
20
|
-
yarn add @carvajalconsultants/headstart urql @urql/exchange-graphcache
|
|
19
|
+
```bash
|
|
20
|
+
yarn add @carvajalconsultants/headstart urql @urql/exchange-graphcache @urql/exchange-auth
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
4. Enable WebSockets in `app.config.ts` if you need it:
|
|
24
24
|
|
|
25
|
-
```
|
|
25
|
+
```typescript
|
|
26
26
|
// app.config.ts
|
|
27
|
-
import { defineConfig } from "@tanstack/start/config";
|
|
27
|
+
import { defineConfig } from "@tanstack/react-start/config";
|
|
28
28
|
|
|
29
29
|
export default defineConfig({
|
|
30
30
|
server: {
|
|
@@ -37,7 +37,7 @@ export default defineConfig({
|
|
|
37
37
|
|
|
38
38
|
5. Make sure to add the `/api` endpoint to the grafserv configuration so that Ruru GraphQL client works correctly:
|
|
39
39
|
|
|
40
|
-
```
|
|
40
|
+
```typescript
|
|
41
41
|
// graphile.config.ts
|
|
42
42
|
const preset: GraphileConfig.Preset = {
|
|
43
43
|
...
|
|
@@ -52,9 +52,9 @@ const preset: GraphileConfig.Preset = {
|
|
|
52
52
|
|
|
53
53
|
6. Add the api.ts file so that it calls our GraphQL handler. This will receive all GraphQL requests at the /api endpoint.
|
|
54
54
|
|
|
55
|
-
```
|
|
55
|
+
```typescript
|
|
56
56
|
// app/api.ts
|
|
57
|
-
import { defaultAPIFileRouteHandler } from "@tanstack/start/api";
|
|
57
|
+
import { defaultAPIFileRouteHandler } from "@tanstack/react-start/api";
|
|
58
58
|
import { createStartAPIHandler } from "@carvajalconsultants/headstart/server";
|
|
59
59
|
import { pgl } from "../pgl";
|
|
60
60
|
|
|
@@ -63,7 +63,7 @@ export default createStartAPIHandler(pgl, defaultAPIFileRouteHandler);
|
|
|
63
63
|
|
|
64
64
|
7. Now we need to configure URQL for client and server rendering, first we start with the server. Create this provider:
|
|
65
65
|
|
|
66
|
-
```
|
|
66
|
+
```typescript
|
|
67
67
|
// app/graphql/serverProvider.tsx
|
|
68
68
|
import { Client } from "urql";
|
|
69
69
|
import { grafastExchange } from "@carvajalconsultants/headstart/server";
|
|
@@ -81,9 +81,90 @@ export const client = new Client({
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
8.
|
|
84
|
+
8. Now the client side for URQL:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// app/graphql/clientProvider.tsx
|
|
88
|
+
import { ssr } from "@carvajalconsultants/headstart/client";
|
|
89
|
+
import { authExchange } from "@urql/exchange-auth";
|
|
90
|
+
import { cacheExchange } from "@urql/exchange-graphcache";
|
|
91
|
+
//import { relayPagination } from "@urql/exchange-graphcache/extras";
|
|
92
|
+
import { Client, fetchExchange } from "urql";
|
|
85
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Creates an authentication exchange for handling secure GraphQL operations.
|
|
96
|
+
* This exchange ensures that all GraphQL requests are properly authenticated
|
|
97
|
+
* and handles authentication failures gracefully.
|
|
98
|
+
*
|
|
99
|
+
* @returns {Object} Authentication configuration object
|
|
100
|
+
* @returns {Function} .addAuthToOperation - Prepares operations with auth context
|
|
101
|
+
* @returns {Function} .didAuthError - Detects authentication failures after server request
|
|
102
|
+
* @returns {Function} .refreshAuth - Handles auth token refresh
|
|
103
|
+
*/
|
|
104
|
+
const auth = authExchange(async () => {
|
|
105
|
+
//TODO Implement authentication checking for the client s ide
|
|
106
|
+
await new Promise((resolve) => {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
console.log("IMPLEMENT CLIENT AUTH CHECK");
|
|
109
|
+
resolve();
|
|
110
|
+
}, 2000);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
/**
|
|
115
|
+
* Processes each GraphQL operation to include authentication context.
|
|
116
|
+
* Currently configured as a pass-through as tokens are in cookies.
|
|
117
|
+
*
|
|
118
|
+
* @param {Operation} operation - The GraphQL operation to authenticate
|
|
119
|
+
* @returns {Operation} The operation with authentication context
|
|
120
|
+
*/
|
|
121
|
+
addAuthToOperation: (operation) => operation,
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Identifies when an operation has failed due to authentication issues.
|
|
125
|
+
* Used to trigger authentication refresh flows when needed.
|
|
126
|
+
*
|
|
127
|
+
* @param {Error} error - The GraphQL error response
|
|
128
|
+
* @returns {boolean} True if the error was caused by authentication failure
|
|
129
|
+
*/
|
|
130
|
+
didAuthError: (error) => error.graphQLErrors.some((e) => e.extensions?.code === "FORBIDDEN"),
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handles refreshing authentication when it becomes invalid.
|
|
134
|
+
* Currently implemented as a no-op as token refresh is handled by getSession().
|
|
135
|
+
*/
|
|
136
|
+
refreshAuth: async () => {
|
|
137
|
+
/* No-op, this is done in getSession() */
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Configured GraphQL client for the application.
|
|
144
|
+
* Provides a centralized way to make authenticated GraphQL requests with
|
|
145
|
+
* proper caching and server-side rendering support.
|
|
146
|
+
*/
|
|
147
|
+
export const client = new Client({
|
|
148
|
+
url: "http://localhost:3000/api/graphql",
|
|
149
|
+
exchanges: [
|
|
150
|
+
cacheExchange({
|
|
151
|
+
resolvers: {
|
|
152
|
+
Query: {
|
|
153
|
+
// Implements relay-style pagination for fills pending match
|
|
154
|
+
//queryName: relayPagination(),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
auth,
|
|
159
|
+
ssr,
|
|
160
|
+
fetchExchange,
|
|
161
|
+
],
|
|
162
|
+
});
|
|
86
163
|
```
|
|
164
|
+
|
|
165
|
+
9. Create the server side router which uses Grafast to execute queries:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
87
168
|
// app/serverRouter.tsx
|
|
88
169
|
import { ssr } from "@carvajalconsultants/headstart/client";
|
|
89
170
|
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
@@ -91,6 +172,8 @@ import { Provider } from "urql";
|
|
|
91
172
|
import { client } from "./graphql/serverProvider";
|
|
92
173
|
import { routeTree } from "./routeTree.gen";
|
|
93
174
|
|
|
175
|
+
import type { ReactNode } from "react";
|
|
176
|
+
|
|
94
177
|
export function createRouter() {
|
|
95
178
|
const router = createTanStackRouter({
|
|
96
179
|
routeTree,
|
|
@@ -101,20 +184,21 @@ export function createRouter() {
|
|
|
101
184
|
// Send data to client so URQL can be hydrated.
|
|
102
185
|
dehydrate: () => ({ initialData: ssr.extractData() }),
|
|
103
186
|
|
|
104
|
-
|
|
105
|
-
Wrap: ({ children }) => <Provider value={client}>{children}</Provider>,
|
|
187
|
+
// Wrap our entire route with the URQL provider so we can execute queries and mutations.
|
|
188
|
+
Wrap: ({ children }: { children: ReactNode }) => <Provider value={client}>{children}</Provider>,
|
|
106
189
|
});
|
|
107
190
|
|
|
108
191
|
return router;
|
|
109
192
|
}
|
|
110
193
|
```
|
|
111
194
|
|
|
112
|
-
|
|
195
|
+
10. Modify the TSR server-side rendering function to use this new router:
|
|
113
196
|
|
|
114
|
-
```
|
|
197
|
+
```typescript
|
|
198
|
+
/* eslint-disable */
|
|
115
199
|
// app/ssr.tsx
|
|
116
200
|
/// <reference types="vinxi/types/server" />
|
|
117
|
-
import { getRouterManifest } from "@tanstack/start/router-manifest";
|
|
201
|
+
import { getRouterManifest } from "@tanstack/react-start/router-manifest";
|
|
118
202
|
import {
|
|
119
203
|
createStartHandler,
|
|
120
204
|
defaultStreamHandler,
|
|
@@ -128,9 +212,9 @@ export default createStartHandler({
|
|
|
128
212
|
})(defaultStreamHandler);
|
|
129
213
|
```
|
|
130
214
|
|
|
131
|
-
|
|
215
|
+
11. Add the client side router which uses the fetch exchange to execute queries, mutations, etc.:
|
|
132
216
|
|
|
133
|
-
```
|
|
217
|
+
```typescript
|
|
134
218
|
// app/clientRouter.tsx
|
|
135
219
|
import { ssr } from "@carvajalconsultants/headstart/client";
|
|
136
220
|
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
@@ -138,6 +222,9 @@ import { Provider } from "urql";
|
|
|
138
222
|
import { client } from "./graphql/clientProvider";
|
|
139
223
|
import { routeTree } from "./routeTree.gen";
|
|
140
224
|
|
|
225
|
+
import type { ReactNode } from "react";
|
|
226
|
+
import type { SSRData } from "urql";
|
|
227
|
+
|
|
141
228
|
export function createRouter() {
|
|
142
229
|
const router = createTanStackRouter({
|
|
143
230
|
routeTree,
|
|
@@ -146,20 +233,20 @@ export function createRouter() {
|
|
|
146
233
|
},
|
|
147
234
|
hydrate: (dehydrated) => {
|
|
148
235
|
// Hydrate URQL with data passed by TSR, this is generated by dehydrate function in server router.
|
|
149
|
-
ssr.restoreData(dehydrated.initialData);
|
|
236
|
+
ssr.restoreData(dehydrated.initialData as SSRData);
|
|
150
237
|
},
|
|
151
238
|
|
|
152
|
-
|
|
153
|
-
Wrap: ({ children }) => <Provider value={client}>{children}</Provider>,
|
|
239
|
+
// Wrap our entire route with the URQL provider so we can execute queries and mutations.
|
|
240
|
+
Wrap: ({ children }: { children: ReactNode }) => <Provider value={client}>{children}</Provider>,
|
|
154
241
|
});
|
|
155
242
|
|
|
156
243
|
return router;
|
|
157
244
|
}
|
|
158
245
|
```
|
|
159
246
|
|
|
160
|
-
|
|
247
|
+
12. Tell TSR to use our client side router:
|
|
161
248
|
|
|
162
|
-
```
|
|
249
|
+
```typescript
|
|
163
250
|
// app/client.tsx
|
|
164
251
|
/// <reference types="vinxi/types/client" />
|
|
165
252
|
import { StartClient } from "@tanstack/start";
|
|
@@ -171,9 +258,9 @@ const router = createRouter();
|
|
|
171
258
|
hydrateRoot(document.getElementById("root")!, <StartClient router={router} />);
|
|
172
259
|
```
|
|
173
260
|
|
|
174
|
-
|
|
261
|
+
13. Last but not least, you're ready to start using URQL on your components and pages. First we create the route using the loader option so we can pre-load data:
|
|
175
262
|
|
|
176
|
-
```
|
|
263
|
+
```typescript
|
|
177
264
|
export const Route = createFileRoute("/")({
|
|
178
265
|
...
|
|
179
266
|
validateSearch: zodSearchValidator(paramSchema),
|
|
@@ -188,9 +275,9 @@ export const Route = createFileRoute("/")({
|
|
|
188
275
|
});
|
|
189
276
|
```
|
|
190
277
|
|
|
191
|
-
|
|
278
|
+
14. Now in your component, you can query with URQL as you normally would:
|
|
192
279
|
|
|
193
|
-
```
|
|
280
|
+
```typescript
|
|
194
281
|
const Home = () => {
|
|
195
282
|
const { page } = Route.useSearch();
|
|
196
283
|
|
|
@@ -209,8 +296,8 @@ const Home = () => {
|
|
|
209
296
|
|
|
210
297
|
## Deployment
|
|
211
298
|
|
|
212
|
-
1. Run `
|
|
299
|
+
1. Run `yarn run build`
|
|
213
300
|
2. `cd .output/server`
|
|
214
301
|
3. `rm -rf node_modules`
|
|
215
|
-
4. `
|
|
216
|
-
5. `
|
|
302
|
+
4. `yarn install`
|
|
303
|
+
5. `yarn run index.mjs`
|
package/grafastExchange.ts
CHANGED
|
@@ -6,9 +6,18 @@ import {
|
|
|
6
6
|
type OperationResult,
|
|
7
7
|
} from "urql";
|
|
8
8
|
import { filter, fromPromise, mergeMap, pipe } from "wonka";
|
|
9
|
+
import type { App, H3Event } from "h3";
|
|
10
|
+
import {
|
|
11
|
+
getQuery,
|
|
12
|
+
getRequestHeaders,
|
|
13
|
+
getRequestProtocol,
|
|
14
|
+
readRawBody,
|
|
15
|
+
} from "h3";
|
|
9
16
|
|
|
10
17
|
import type { Maybe } from "grafast";
|
|
11
18
|
import type { PostGraphileInstance } from "postgraphile";
|
|
19
|
+
import { getEvent, getWebRequest } from "vinxi/http";
|
|
20
|
+
import { GrafservBodyBuffer, normalizeRequest, processHeaders, RequestDigest } from "postgraphile/grafserv";
|
|
12
21
|
|
|
13
22
|
export const grafastExchange = (pgl: PostGraphileInstance): Exchange => {
|
|
14
23
|
return () => (ops$) => {
|
|
@@ -16,13 +25,49 @@ export const grafastExchange = (pgl: PostGraphileInstance): Exchange => {
|
|
|
16
25
|
ops$,
|
|
17
26
|
|
|
18
27
|
// Skip anything that's not a query since that's all we can run
|
|
19
|
-
filter((operation) => operation.kind === "query"),
|
|
28
|
+
filter((operation) => operation.kind === "query" || operation.kind === "mutation"),
|
|
20
29
|
|
|
21
30
|
mergeMap((operation) => fromPromise(runGrafastQuery(pgl, operation))),
|
|
22
31
|
);
|
|
23
32
|
};
|
|
24
33
|
};
|
|
25
34
|
|
|
35
|
+
/* This is a direct copy from: https://github.com/graphile/crystal/blob/main/grafast/grafserv/src/servers/h3/v1/index.ts#L24 */
|
|
36
|
+
function getDigest(event: H3Event): RequestDigest {
|
|
37
|
+
const req = event.node.req;
|
|
38
|
+
const res = event.node.res;
|
|
39
|
+
return {
|
|
40
|
+
httpVersionMajor: req.httpVersionMajor,
|
|
41
|
+
httpVersionMinor: req.httpVersionMinor,
|
|
42
|
+
isSecure: getRequestProtocol(event) === "https",
|
|
43
|
+
method: event.method,
|
|
44
|
+
path: event.path,
|
|
45
|
+
headers: processHeaders(getRequestHeaders(event)),
|
|
46
|
+
getQueryParams() {
|
|
47
|
+
return getQuery(event) as Record<string, string | string[]>;
|
|
48
|
+
},
|
|
49
|
+
async getBody() {
|
|
50
|
+
const buffer = await readRawBody(event, false);
|
|
51
|
+
if (!buffer) {
|
|
52
|
+
throw new Error("Failed to retrieve body from h3");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
type: "buffer",
|
|
56
|
+
buffer,
|
|
57
|
+
} as GrafservBodyBuffer;
|
|
58
|
+
},
|
|
59
|
+
requestContext: {
|
|
60
|
+
h3v1: {
|
|
61
|
+
event,
|
|
62
|
+
},
|
|
63
|
+
node: {
|
|
64
|
+
req,
|
|
65
|
+
res,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
26
71
|
/**
|
|
27
72
|
* Run the URQL query with Grafast, without making an HTTP request to ourselves (which is unnecessary overhead).
|
|
28
73
|
*
|
|
@@ -37,16 +82,25 @@ const runGrafastQuery = async (
|
|
|
37
82
|
try {
|
|
38
83
|
const { variables: variableValues, query: document } = operation;
|
|
39
84
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
const { schema, resolvedPreset } = await pgl.getSchemaResult();
|
|
86
|
+
|
|
87
|
+
// Request/H3 info so we can run with it
|
|
88
|
+
const event = await getEvent();
|
|
89
|
+
const http = normalizeRequest(getDigest(event));
|
|
90
|
+
|
|
91
|
+
// Add the Graphile context to our args so Grafast can run
|
|
92
|
+
const args = await hookArgs({
|
|
93
|
+
schema,
|
|
43
94
|
document,
|
|
44
95
|
variableValues: variableValues as Maybe<{
|
|
45
96
|
readonly [variable: string]: unknown;
|
|
46
97
|
}>,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
resolvedPreset,
|
|
99
|
+
requestContext: {
|
|
100
|
+
...http.requestContext,
|
|
101
|
+
http,
|
|
102
|
+
}
|
|
103
|
+
});
|
|
50
104
|
|
|
51
105
|
// Run the query with Grafast, to get the result from PostgreSQL
|
|
52
106
|
const result = await execute(args);
|
package/package.json
CHANGED