vyriy 0.4.4 → 0.4.7

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.
@@ -1,16 +1,750 @@
1
+ import packageJson from '../../../package.json' with { type: 'json' };
1
2
  import { base } from './base.js';
2
3
  export const gql = {
3
4
  files: (options) => ({
4
5
  ...base.files(options),
6
+ 'package.json': JSON.stringify({
7
+ name: options.name,
8
+ version: '0.0.0',
9
+ description: options.description,
10
+ private: true,
11
+ type: 'module',
12
+ agents: './AGENTS.md',
13
+ packageManager: packageJson.packageManager,
14
+ engines: {
15
+ node: packageJson.engines.node,
16
+ },
17
+ workspaces: [
18
+ 'packages/*',
19
+ 'workspaces/*',
20
+ ],
21
+ scripts: {
22
+ storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
23
+ check: 'run-s lint build test',
24
+ fix: "run-s 'fix:*'",
25
+ start: "run-p 'start:*'",
26
+ lint: "run-s 'lint:*'",
27
+ build: "run-s 'build:*'",
28
+ test: "run-s 'test:*'",
29
+ 'fix:prettier': 'prettier . --write',
30
+ 'fix:eslint': 'eslint . --fix',
31
+ 'start:graphql': 'sh workspaces/graphql/bin/start.sh',
32
+ 'lint:ts': 'tsc',
33
+ 'lint:prettier': 'prettier . --check',
34
+ 'lint:eslint': 'eslint .',
35
+ 'build:graphql': 'rimraf dist && sh workspaces/graphql/bin/build.sh',
36
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
37
+ 'test:jest': 'jest',
38
+ postinstall: 'husky',
39
+ },
40
+ dependencies: {
41
+ '@vyriy/typescript-config': `^${packageJson.version}`,
42
+ typescript: packageJson.peerDependencies.typescript,
43
+ '@vyriy/prettier-config': `^${packageJson.version}`,
44
+ prettier: packageJson.peerDependencies.prettier,
45
+ '@vyriy/eslint-config': `^${packageJson.version}`,
46
+ eslint: packageJson.peerDependencies.eslint,
47
+ '@vyriy/jest-config': `^${packageJson.version}`,
48
+ jest: packageJson.peerDependencies.jest,
49
+ '@vyriy/storybook-config': `^${packageJson.version}`,
50
+ storybook: packageJson.peerDependencies.storybook,
51
+ '@vyriy/path': `^${packageJson.version}`,
52
+ husky: packageJson.peerDependencies.husky,
53
+ 'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
54
+ 'cross-env': packageJson.peerDependencies['cross-env'],
55
+ rimraf: packageJson.peerDependencies.rimraf,
56
+ '@vyriy/webpack-config': `^${packageJson.version}`,
57
+ '@vyriy/handler': `^${packageJson.version}`,
58
+ '@vyriy/server': `^${packageJson.version}`,
59
+ '@vyriy/router': `^${packageJson.version}`,
60
+ tsx: packageJson.peerDependencies.tsx,
61
+ 'webpack-cli': packageJson.peerDependencies['webpack-cli'],
62
+ graphql: packageJson.peerDependencies.graphql,
63
+ '@vyriy/html': `^${packageJson.version}`,
64
+ },
65
+ }, null, 2) + '\n',
66
+ 'packages/graphql/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
67
+ import ReadMe from './README.md?raw';
68
+
69
+ <Meta title="Packages/GraphQL" />
70
+
71
+ <Markdown>{ReadMe}</Markdown>
72
+ `,
73
+ 'packages/graphql/graphiql.test.ts': `import { describe, expect, it } from '@jest/globals';
74
+
75
+ import { graphiql } from './graphiql.js';
76
+
77
+ describe('packages/graphql/graphiql.ts', () => {
78
+ it('returns the embedded GraphiQL HTML page', () => {
79
+ const page = graphiql();
80
+
81
+ expect(page).toContain('<html lang="en">');
82
+ expect(page).toContain('<title>GraphiQL</title>');
83
+ expect(page).toContain('<meta charset="UTF-8"');
84
+ expect(page).toContain('<div id="graphiql"><div class="loading">Loading');
85
+ expect(page).toContain('https://esm.sh/graphiql@5.2.2/dist/style.css');
86
+ expect(page).toContain('https://esm.sh/@graphiql/plugin-explorer@5.1.1/dist/style.css');
87
+ expect(page).toContain('<script type="importmap">');
88
+ expect(page).toContain('"graphiql": "https://esm.sh/graphiql@5.2.2?standalone');
89
+ expect(page).toContain('const fetcher = createGraphiQLFetcher({');
90
+ expect(page).toContain("url: 'http://localhost:3000'");
91
+ expect(page).toContain('root.render(React.createElement(App));');
92
+ });
93
+ });
94
+ `,
95
+ 'packages/graphql/graphiql.ts': `import { html, minify } from '@vyriy/html';
96
+
97
+ export const graphiql = () =>
98
+ minify(
99
+ html({
100
+ htmlAttributes: 'lang="en"',
101
+ title: '<title>GraphiQL</title>',
102
+ meta: '<meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />',
103
+ link: [
104
+ \`<link
105
+ rel="stylesheet"
106
+ href="https://esm.sh/graphiql@5.2.2/dist/style.css"
107
+ integrity="sha384-f6GHLfCwoa4MFYUMd3rieGOsIVAte/evKbJhMigNdzUf52U9bV2JQBMQLke0ua+2"
108
+ crossorigin="anonymous"
109
+ />\`,
110
+ \`<link
111
+ rel="stylesheet"
112
+ href="https://esm.sh/@graphiql/plugin-explorer@5.1.1/dist/style.css"
113
+ integrity="sha384-vTFGj0krVqwFXLB7kq/VHR0/j2+cCT/B63rge2mULaqnib2OX7DVLUVksTlqvMab"
114
+ crossorigin="anonymous"
115
+ />\`,
116
+ ].join(''),
117
+ style: \`<style>
118
+ body {
119
+ margin: 0;
120
+ }
121
+
122
+ #graphiql {
123
+ height: 100dvh;
124
+ }
125
+
126
+ .loading {
127
+ height: 100%;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ font-size: 4rem;
132
+ }
133
+ </style>\`,
134
+ script: [
135
+ \`<script type="importmap">
136
+ {
137
+ "imports": {
138
+ "react": "https://esm.sh/react@19.2.5",
139
+ "react/": "https://esm.sh/react@19.2.5/",
140
+ "react-dom": "https://esm.sh/react-dom@19.2.5",
141
+ "react-dom/": "https://esm.sh/react-dom@19.2.5/",
142
+ "graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
143
+ "graphiql/": "https://esm.sh/graphiql@5.2.2/",
144
+ "@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer@5.1.1?standalone&external=react,@graphiql/react,graphql",
145
+ "@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
146
+ "@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
147
+ "graphql": "https://esm.sh/graphql@16.13.2",
148
+ "@emotion/is-prop-valid": "data:text/javascript,"
149
+ },
150
+ "integrity": {
151
+ "https://esm.sh/react@19.2.5": "sha384-ZNmUQ9QQgyl95nnD/FJTBQn2ZEPTbWtMuWCXTKWNuF6Si7nC+6bvSgk5LWu+ELHn",
152
+ "https://esm.sh/react-dom@19.2.5": "sha384-qtNxBzn9gBs3CmJItMuvIVyjW3VIU0/rzGhCm9MippVU1BpR/c4VgaFYDIg/FrY2",
153
+ "https://esm.sh/graphiql@5.2.2": "sha384-MBVZMq1pmz8DwpwIWPWLk2tmS6tGiSi6WwbXvy9NhuDYASAAWd2m96xbxLqszig9",
154
+ "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql": "sha384-SzHBEbcQfhvmwqh5Vtat9k7b/kIzmdVO3KMzQiAYwcxCA9x7vZwFRUgjzN1AeV3q",
155
+ "https://esm.sh/@graphiql/plugin-explorer@5.1.1": "sha384-83REbLb9KtIhL/6J1n91SLoP0648KOKZLIDdHRx/a0E7T3ajq6PzKz+815SCfN52",
156
+ "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid": "sha384-iZsbTy9B0VcX2BOTdqMuX0uJ9Hff5GbG2QeOt4OeMp0GHza76dwQaYQYNYkZkIVq",
157
+ "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql": "sha384-ZsnupyYmzpNjF1Z/81zwi4nV352n4P7vm0JOFKiYnAwVGOf9twnEMnnxmxabMBXe",
158
+ "https://esm.sh/graphql@16.13.2": "sha384-TQg9alwG3P9fzBErDW011vKuyTnrwpBZsl3SdMAh6DwBcv9ezFOl0djGI/68VOyy"
159
+ }
160
+ }
161
+ </script>\`,
162
+ \`<script type="module">
163
+ import React from 'react';
164
+ import ReactDOM from 'react-dom/client';
165
+ import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
166
+ import { createGraphiQLFetcher } from '@graphiql/toolkit';
167
+ import { explorerPlugin } from '@graphiql/plugin-explorer';
168
+ import 'graphiql/setup-workers/esm.sh';
169
+
170
+ const fetcher = createGraphiQLFetcher({
171
+ url: 'http://localhost:3000',
172
+ });
173
+ const plugins = [HISTORY_PLUGIN, explorerPlugin()];
174
+
175
+ function App() {
176
+ return React.createElement(GraphiQL, {
177
+ fetcher,
178
+ plugins,
179
+ defaultEditorToolsVisibility: true,
180
+ });
181
+ }
182
+
183
+ const container = document.getElementById('graphiql');
184
+ const root = ReactDOM.createRoot(container);
185
+ root.render(React.createElement(App));
186
+ </script>\`,
187
+ ].join(''),
188
+ body: '<div id="graphiql"><div class="loading">Loading…</div></div>',
5
189
  }),
6
- ci: {
7
- github: {
8
- '.gitlab-ci.yml': 'code',
190
+ );
191
+ `,
192
+ 'packages/graphql/index.test.ts': `import { describe, expect, it } from '@jest/globals';
193
+
194
+ import { router } from './router.js';
195
+
196
+ describe('packages/graphql/index.ts', () => {
197
+ it('re-exports the GraphQL router', async () => {
198
+ await expect(import('./index.js')).resolves.toMatchObject({
199
+ router,
200
+ });
201
+ });
202
+ });
203
+ `,
204
+ 'packages/graphql/index.ts': "export * from './router.js';\n",
205
+ 'packages/graphql/package.json': `{
206
+ "name": "@p/graphql",
207
+ "type": "module",
208
+ "private": true
209
+ }
210
+ `,
211
+ 'packages/graphql/README.md': `# @p/graphql
212
+
213
+ Reusable GraphQL API package for the application.
214
+
215
+ ## Exports
216
+
217
+ - \`router\` - an \`@vyriy/router\` instance that serves the GraphiQL page and GraphQL HTTP requests.
218
+
219
+ ## Routes
220
+
221
+ ### \`GET /\`
222
+
223
+ Returns an embedded GraphiQL page for exploring the schema in a browser.
224
+
225
+ ### \`POST /\`
226
+
227
+ The router expects a JSON body with a GraphQL request:
228
+
229
+ \`\`\`json
230
+ {
231
+ "query": "{ hello }",
232
+ "variables": {},
233
+ "operationName": "Hello"
234
+ }
235
+ \`\`\`
236
+
237
+ Invalid JSON or an empty body returns \`400\` with \`Invalid JSON body\`. A body without \`query\` returns \`400\` with \`Missing GraphQL query\`.
238
+
239
+ ## Schema
240
+
241
+ The package schema currently supports:
242
+
243
+ - \`hello: String\`
244
+ - \`test: Test\`
245
+ - \`ping(message: String): String\`
246
+
247
+ Example mutation:
248
+
249
+ \`\`\`graphql
250
+ mutation Ping($message: String) {
251
+ ping(message: $message)
252
+ }
253
+ \`\`\`
254
+ `,
255
+ 'packages/graphql/router.test.ts': `import { describe, expect, it } from '@jest/globals';
256
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from '@vyriy/router';
257
+
258
+ import { router } from './router.js';
259
+
260
+ type GraphQLBody = {
261
+ data?: Record<string, unknown> | null;
262
+ errors?: Array<{
263
+ message: string;
264
+ }>;
265
+ };
266
+
267
+ const getPostEvent = (
268
+ body: string | null,
269
+ headers: APIGatewayProxyEvent['headers'] | null = {
270
+ authorization: 'Bearer token',
271
+ },
272
+ ): APIGatewayProxyEvent =>
273
+ ({
274
+ body,
275
+ headers,
276
+ httpMethod: 'POST',
277
+ path: '/',
278
+ pathParameters: null,
279
+ queryStringParameters: null,
280
+ }) as unknown as APIGatewayProxyEvent;
281
+
282
+ const getEvent = (path = '/'): APIGatewayProxyEvent =>
283
+ ({
284
+ body: null,
285
+ headers: {},
286
+ httpMethod: 'GET',
287
+ path,
288
+ pathParameters: null,
289
+ queryStringParameters: null,
290
+ }) as unknown as APIGatewayProxyEvent;
291
+
292
+ const parseBody = <Value>(response: APIGatewayProxyResult): Value => JSON.parse(response.body) as Value;
293
+
294
+ describe('packages/graphql/router.ts', () => {
295
+ it('serves the embedded GraphiQL page', async () => {
296
+ const response = await router.route(getEvent());
297
+
298
+ expect(response.statusCode).toBe(200);
299
+ expect(response.headers).toEqual({
300
+ 'Content-Type': 'text/html; charset=UTF-8',
301
+ });
302
+ expect(response.body).toContain('<title>GraphiQL</title>');
303
+ expect(response.body).toContain('<div id="graphiql">');
304
+ });
305
+
306
+ it('executes GraphQL requests', async () => {
307
+ const response = await router.route(
308
+ getPostEvent(
309
+ JSON.stringify({
310
+ query: 'query Hello { hello }',
311
+ operationName: 'Hello',
312
+ }),
313
+ ),
314
+ );
315
+
316
+ expect(response.statusCode).toBe(200);
317
+ expect(parseBody<GraphQLBody>(response)).toEqual({
318
+ data: {
319
+ hello: 'Hello from GraphQL',
320
+ },
321
+ });
322
+ });
323
+
324
+ it('passes variables to GraphQL execution', async () => {
325
+ const response = await router.route(
326
+ getPostEvent(
327
+ JSON.stringify({
328
+ query: 'mutation Ping($message: String) { ping(message: $message) }',
329
+ variables: {
330
+ message: 'from router',
331
+ },
332
+ }),
333
+ ),
334
+ );
335
+
336
+ expect(response.statusCode).toBe(200);
337
+ expect(parseBody<GraphQLBody>(response)).toEqual({
338
+ data: {
339
+ ping: 'pong: from router',
340
+ },
341
+ });
342
+ });
343
+
344
+ it('executes GraphQL requests without request headers', async () => {
345
+ const response = await router.route(
346
+ getPostEvent(
347
+ JSON.stringify({
348
+ query: '{ test { ok message } }',
349
+ }),
350
+ null,
351
+ ),
352
+ );
353
+
354
+ expect(response.statusCode).toBe(200);
355
+ expect(parseBody<GraphQLBody>(response)).toEqual({
356
+ data: {
357
+ test: {
358
+ ok: true,
359
+ message: 'GraphQL works',
360
+ },
361
+ },
362
+ });
363
+ });
364
+
365
+ it('rejects missing bodies', async () => {
366
+ await expect(router.route(getPostEvent(null))).resolves.toEqual({
367
+ body: JSON.stringify({
368
+ errors: [{ message: 'Invalid JSON body' }],
369
+ }),
370
+ statusCode: 400,
371
+ });
372
+ });
373
+
374
+ it('rejects invalid JSON bodies', async () => {
375
+ await expect(router.route(getPostEvent('{'))).resolves.toEqual({
376
+ body: JSON.stringify({
377
+ errors: [{ message: 'Invalid JSON body' }],
378
+ }),
379
+ statusCode: 400,
380
+ });
381
+ });
382
+
383
+ it('rejects bodies without a GraphQL query', async () => {
384
+ await expect(router.route(getPostEvent(JSON.stringify({ variables: {} })))).resolves.toEqual({
385
+ body: JSON.stringify({
386
+ errors: [{ message: 'Missing GraphQL query' }],
387
+ }),
388
+ statusCode: 400,
389
+ });
390
+ });
391
+ });
392
+ `,
393
+ 'packages/graphql/router.ts': `import { createRouter } from '@vyriy/router';
394
+
395
+ import { graphql } from 'graphql';
396
+
397
+ import { graphiql } from './graphiql.js';
398
+ import { schema } from './schema.js';
399
+
400
+ export const router = createRouter();
401
+
402
+ router.get('/', () => ({
403
+ headers: {
404
+ 'Content-Type': 'text/html; charset=UTF-8',
405
+ },
406
+ body: graphiql(),
407
+ }));
408
+
409
+ router.post('/', async (params) => {
410
+ let payload: {
411
+ query?: string;
412
+ variables?: Record<string, unknown>;
413
+ operationName?: string;
414
+ };
415
+
416
+ if (!params.body) {
417
+ return {
418
+ statusCode: 400,
419
+ body: JSON.stringify({
420
+ errors: [{ message: 'Invalid JSON body' }],
421
+ }),
422
+ };
423
+ }
424
+
425
+ try {
426
+ payload = JSON.parse(params.body) as typeof payload;
427
+ } catch {
428
+ return {
429
+ statusCode: 400,
430
+ body: JSON.stringify({
431
+ errors: [{ message: 'Invalid JSON body' }],
432
+ }),
433
+ };
434
+ }
435
+
436
+ if (!payload.query) {
437
+ return {
438
+ statusCode: 400,
439
+ body: JSON.stringify({
440
+ errors: [{ message: 'Missing GraphQL query' }],
441
+ }),
442
+ };
443
+ }
444
+
445
+ return {
446
+ body: JSON.stringify(
447
+ await graphql({
448
+ schema,
449
+ source: payload.query,
450
+ variableValues: payload.variables,
451
+ operationName: payload.operationName,
452
+ contextValue: {
453
+ event: params.event,
454
+ headers: params.headers ?? {},
455
+ },
456
+ }),
457
+ ),
458
+ };
459
+ });
460
+ `,
461
+ 'packages/graphql/schema.test.ts': `import { describe, expect, it } from '@jest/globals';
462
+ import { graphql } from 'graphql';
463
+
464
+ import { schema } from './schema.js';
465
+
466
+ describe('packages/graphql/schema.ts', () => {
467
+ it('resolves query fields', async () => {
468
+ await expect(
469
+ graphql({
470
+ schema,
471
+ source: '{ hello test { ok message } }',
472
+ }),
473
+ ).resolves.toEqual({
474
+ data: {
475
+ hello: 'Hello from GraphQL',
476
+ test: {
477
+ ok: true,
478
+ message: 'GraphQL works',
479
+ },
480
+ },
481
+ });
482
+ });
483
+
484
+ it('resolves the ping mutation', async () => {
485
+ await expect(
486
+ graphql({
487
+ schema,
488
+ source: 'mutation Ping($message: String) { ping(message: $message) }',
489
+ variableValues: {
490
+ message: 'hello',
491
+ },
492
+ }),
493
+ ).resolves.toEqual({
494
+ data: {
495
+ ping: 'pong: hello',
496
+ },
497
+ });
498
+ });
499
+ });
500
+ `,
501
+ 'packages/graphql/schema.ts': `import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLBoolean } from 'graphql';
502
+
503
+ const TestType = new GraphQLObjectType({
504
+ name: 'Test',
505
+ fields: {
506
+ ok: {
507
+ type: GraphQLBoolean,
508
+ },
509
+ message: {
510
+ type: GraphQLString,
511
+ },
512
+ },
513
+ });
514
+
515
+ const QueryType = new GraphQLObjectType({
516
+ name: 'Query',
517
+ fields: {
518
+ hello: {
519
+ type: GraphQLString,
520
+ resolve: () => 'Hello from GraphQL',
521
+ },
522
+
523
+ test: {
524
+ type: TestType,
525
+ resolve: () => ({
526
+ ok: true,
527
+ message: 'GraphQL works',
528
+ }),
529
+ },
530
+ },
531
+ });
532
+
533
+ const MutationType = new GraphQLObjectType({
534
+ name: 'Mutation',
535
+ fields: {
536
+ ping: {
537
+ type: GraphQLString,
538
+ args: {
539
+ message: {
540
+ type: GraphQLString,
9
541
  },
542
+ },
543
+ resolve: (_source, args: { message: string }) => {
544
+ return \`pong: \${args.message}\`;
545
+ },
10
546
  },
11
- deploy: {
12
- docker: {
13
- Dockerfile: 'code',
547
+ },
548
+ });
549
+
550
+ export const schema = new GraphQLSchema({
551
+ query: QueryType,
552
+ mutation: MutationType,
553
+ });
554
+ `,
555
+ 'workspaces/graphql/bin/build.sh': `#!/usr/bin/env sh
556
+
557
+ set -e
558
+
559
+ scriptdir="$PWD/workspaces/graphql";
560
+ distdir="dist/graphql";
561
+
562
+ NODE_ENV=production npx webpack --config $scriptdir/webpack.config.ts
563
+
564
+ cp $scriptdir/package.json $distdir/package.json
565
+ npm pkg delete "type" --prefix $distdir
566
+ npm pkg delete "private" --prefix $distdir
567
+ `,
568
+ 'workspaces/graphql/bin/start.sh': `#!/usr/bin/env sh
569
+
570
+ set -e
571
+
572
+ scriptdir="$PWD/workspaces/graphql";
573
+
574
+ NODE_ENV=production LOG_LEVEL=info tsx $scriptdir/index.ts
575
+ `,
576
+ 'workspaces/graphql/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
577
+ import ReadMe from './README.md?raw';
578
+
579
+ <Meta title="Workspaces/Graphql" />
580
+
581
+ <Markdown>{ReadMe}</Markdown>
582
+ `,
583
+ 'workspaces/graphql/index.test.ts': `import { describe, expect, it, jest } from '@jest/globals';
584
+ import type { APIGatewayProxyEvent } from '@vyriy/router';
585
+
586
+ const apiMock = jest.fn((handler) => ({
587
+ handler,
588
+ }));
589
+ const serverMock = jest.fn();
590
+
591
+ jest.mock('@vyriy/handler', () => ({
592
+ api: apiMock,
593
+ }));
594
+
595
+ jest.mock('@vyriy/server', () => ({
596
+ server: serverMock,
597
+ }));
598
+
599
+ describe('workspaces/graphql/index.ts', () => {
600
+ const getPostEvent = (body: string | null, path = '/'): APIGatewayProxyEvent =>
601
+ ({
602
+ body,
603
+ headers: {},
604
+ httpMethod: 'POST',
605
+ path,
606
+ pathParameters: null,
607
+ queryStringParameters: null,
608
+ }) as unknown as APIGatewayProxyEvent;
609
+
610
+ const getEvent = (path = '/'): APIGatewayProxyEvent =>
611
+ ({
612
+ body: null,
613
+ headers: {},
614
+ httpMethod: 'GET',
615
+ path,
616
+ pathParameters: null,
617
+ queryStringParameters: null,
618
+ }) as unknown as APIGatewayProxyEvent;
619
+
620
+ it('starts the server with the API router handler', async () => {
621
+ await import('./index.js');
622
+
623
+ expect(apiMock).toHaveBeenCalledTimes(1);
624
+ expect(serverMock).toHaveBeenCalledTimes(1);
625
+ expect(serverMock).toHaveBeenCalledWith(apiMock.mock.results[0]?.value);
626
+
627
+ const handler = apiMock.mock.calls[0]?.[0] as (event: APIGatewayProxyEvent) => Promise<{
628
+ body: string;
629
+ headers?: Record<string, string>;
630
+ statusCode: number;
631
+ }>;
632
+
633
+ await expect(
634
+ handler(
635
+ getPostEvent(
636
+ JSON.stringify({
637
+ query: '{ hello test { ok message } }',
638
+ }),
639
+ ),
640
+ ),
641
+ ).resolves.toEqual({
642
+ body: JSON.stringify({
643
+ data: {
644
+ hello: 'Hello from GraphQL',
645
+ test: {
646
+ ok: true,
647
+ message: 'GraphQL works',
648
+ },
14
649
  },
650
+ }),
651
+ statusCode: 200,
652
+ });
653
+
654
+ const graphiqlResponse = await handler(getEvent());
655
+
656
+ expect(graphiqlResponse).toEqual({
657
+ body: expect.stringContaining('<title>GraphiQL</title>'),
658
+ headers: {
659
+ 'Content-Type': 'text/html; charset=UTF-8',
660
+ },
661
+ statusCode: 200,
662
+ });
663
+
664
+ await expect(handler(getEvent('/healthcheck'))).resolves.toEqual({
665
+ body: JSON.stringify({
666
+ message: 'Not Found',
667
+ }),
668
+ statusCode: 404,
669
+ });
670
+ });
671
+ });
672
+ `,
673
+ 'workspaces/graphql/index.ts': `import { server } from '@vyriy/server';
674
+ import { api } from '@vyriy/handler';
675
+
676
+ import { router } from '@p/graphql';
677
+
678
+ server(api(async (event) => router.route(event)));
679
+ `,
680
+ 'workspaces/graphql/package.json': `{
681
+ "name": "@w/graphql",
682
+ "type": "module",
683
+ "private": true
684
+ }
685
+ `,
686
+ 'workspaces/graphql/README.md': `# @w/graphql
687
+
688
+ GraphQL runtime workspace for the application.
689
+
690
+ This workspace starts the HTTP server and mounts the reusable \`@p/graphql\` router through \`@vyriy/handler\` and \`@vyriy/server\`.
691
+
692
+ ## Routes
693
+
694
+ - \`GET /\` - serves the embedded GraphiQL page.
695
+ - \`POST /\` - executes GraphQL requests against the package schema.
696
+
697
+ Example request body:
698
+
699
+ \`\`\`json
700
+ {
701
+ "query": "{ hello test { ok message } }"
702
+ }
703
+ \`\`\`
704
+
705
+ ## Development
706
+
707
+ Start the local GraphQL server:
708
+
709
+ \`\`\`bash
710
+ yarn start:graphql
711
+ \`\`\`
712
+
713
+ Build the deployable bundle:
714
+
715
+ \`\`\`bash
716
+ yarn build:graphql
717
+ \`\`\`
718
+
719
+ The build output is written to \`dist/graphql\`.
720
+
721
+ ## Implementation
722
+
723
+ The workspace entrypoint is \`index.ts\`. It wraps the shared router with \`api()\` and passes it to \`server()\`:
724
+
725
+ \`\`\`ts
726
+ server(api(async (event) => router.route(event)));
727
+ \`\`\`
728
+ `,
729
+ 'workspaces/graphql/webpack.config.ts': `import { path } from '@vyriy/path';
730
+ import { ssr, external } from '@vyriy/webpack-config';
731
+
732
+ export default ssr(
733
+ '@w/graphql',
734
+ {
735
+ path: path('dist', 'graphql'),
736
+ filename: 'index.js',
737
+ library: { type: 'commonjs2' },
738
+ },
739
+ (config) => ({
740
+ ...config,
741
+ externals: [external({ allowlist: [/^@p/, /^@w/, /^@vyriy/] })],
742
+ }),
743
+ );
744
+ `,
745
+ }),
746
+ ci: {
747
+ ...base.ci,
15
748
  },
749
+ deploy: {},
16
750
  };