@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 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. Create the server side router which uses Grafast to execute queries:
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
- // Wrap our entire route with the URQL provider so we can execute queries and mutations.
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
- 9. Modify the TSR server-side rendering function to use this new router:
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
- 10. Add the client side router which uses the fetch exchange to execute queries, mutations, etc.:
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
- // Wrap our entire route with the URQL provider so we can execute queries and mutations.
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
- 11. Tell TSR to use our client side router:
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
- 12. 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:
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
- 13. Now in your component, you can query with URQL as you normally would:
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 `bun run build`
299
+ 1. Run `yarn run build`
213
300
  2. `cd .output/server`
214
301
  3. `rm -rf node_modules`
215
- 4. `bun install`
216
- 5. `bun run index.mjs`
302
+ 4. `yarn install`
303
+ 5. `yarn run index.mjs`
@@ -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 args = {
41
- resolvedPreset: pgl.getResolvedPreset(),
42
- schema: await pgl.getSchema(),
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
- // Add the Graphile context to our args so Grafast can run
49
- await hookArgs(args);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carvajalconsultants/headstart",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Library to assist in integrating PostGraphile with Tanstack Start and URQL.",
5
5
  "license": "MIT",
6
6
  "author": "Miguel Carvajal <omar@carvajalonline.com>",