@stonecrop/nuxt-grafserv 0.7.4 → 0.7.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
@@ -3,21 +3,31 @@
3
3
  [![npm version][npm-version-src]][npm-version-href]
4
4
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
5
  [![License][license-src]][license-href]
6
- [![Nuxt][nuxt-src]][nuxt-href]
7
6
 
8
7
  Pluggable Grafserv GraphQL server as a Nuxt Module. Uses the Grafast execution engine for high-performance GraphQL.
9
8
 
10
- - [✨  Release Notes](/CHANGELOG.md)
11
-
12
9
  ## Features
13
10
 
14
11
  - 🚀  Grafserv Server Integration
15
- - ⚡️  Grafast Execution Engine (faster than graphql-js)
12
+ - ⚡️  Grafast Execution Engine (faster than [`graphql-js`](https://github.com/graphql/graphql-js))
16
13
  - 🔄  Schema Stitching Support
17
- - 🛠  Middleware Support
14
+ - 🔍  Graphile Preset System for Advanced Configuration
18
15
  - 📝  TypeScript Support
19
- - 🔍  GraphiQL Interface
16
+ - 🔍  GraphiQL/Ruru Interface
20
17
  - ⚡️  Hot Module Reloading
18
+ - 🎯  Separate Route Handlers for GraphQL/UI and Static Assets
19
+
20
+ ## Architecture
21
+
22
+ This module uses modern Grafserv patterns with three key components:
23
+
24
+ 1. **Preset-Based Configuration**: Leverages Graphile's preset system for extensibility and plugin support
25
+ 2. **Separate Route Handlers**: Two dedicated handlers for GraphQL operations/UI and static assets
26
+ 3. **Objects Structure**: Uses Grafast's modern `objects/interfaces/enums` schema building pattern for better type safety
27
+
28
+ The module automatically registers these handlers:
29
+ - `{url}` - Unified GraphQL operations and Ruru UI endpoint
30
+ - `/ruru-static/**` - Static assets for the Ruru IDE
21
31
 
22
32
  ## Quick Setup
23
33
 
@@ -40,48 +50,54 @@ npm install @stonecrop/nuxt-grafserv
40
50
  export default defineNuxtConfig({
41
51
  modules: ['@stonecrop/nuxt-grafserv'],
42
52
  grafserv: {
43
- schema: './server/**/*.graphql',
44
- resolvers: './server/resolvers.ts',
45
- url: '/graphql/',
53
+ schema: 'server/**/*.graphql',
54
+ resolvers: 'server/resolvers.ts',
55
+ url: '/graphql/', // Serves both GraphQL API and Ruru UI
46
56
  }
47
57
  })
48
58
  ```
49
59
 
50
60
  ## Configuration
51
61
 
52
- Here's a full example of all available options:
62
+ ### All Available Options
63
+
64
+ | Option | Type | Default | Description |
65
+ |--------|------|---------|-------------|
66
+ | `schema` | `string \| string[] \| SchemaProvider \| PostGraphileInstance` | `'server/**/*.graphql'` | Path(s) to GraphQL schema files, schema provider function, or PostGraphile instance |
67
+ | `resolvers` | `string` | `undefined` | Path to resolvers file (optional - not needed for PostGraphile) |
68
+ | `url` | `string` | `'/graphql/'` | GraphQL endpoint URL (also serves Ruru UI) |
69
+ | `graphiql` | `boolean` | `true` in dev, `false` in prod | Enable GraphiQL IDE |
70
+ | `preset` | `GraphileConfig.Preset` | `undefined` | Custom Graphile preset for advanced configuration |
71
+
72
+ ### Full Configuration Example
53
73
 
54
74
  ```ts
55
75
  export default defineNuxtConfig({
56
76
  modules: ['@stonecrop/nuxt-grafserv'],
57
77
  grafserv: {
58
- // Path to your GraphQL schema files
59
- schema: './server/**/*.graphql',
60
-
61
- // Path to your resolvers
62
- resolvers: './server/resolvers.ts',
63
-
64
- // GraphQL endpoint URL (default: /graphql/)
65
- url: '/graphql/',
66
-
67
- // Enable GraphiQL IDE (default: true in dev, false in prod)
78
+ // Schema configuration
79
+ schema: 'server/**/*.graphql',
80
+ resolvers: 'server/resolvers.ts',
81
+
82
+ // Endpoints
83
+ url: '/graphql/', // Serves both GraphQL API and Ruru UI
68
84
  graphiql: true,
69
-
70
- // Middleware functions
71
- middleware: [
72
- async (ctx, next) => {
73
- const start = Date.now()
74
- const result = await next()
75
- console.log(`Request took ${Date.now() - start}ms`)
76
- return result
85
+
86
+ // Graphile preset with grafserv options
87
+ preset: {
88
+ grafserv: {
89
+ websockets: false,
90
+ graphqlOverGET: true, // Enable GET requests for queries
91
+ maxRequestLength: 100000,
92
+ allowedRequestContentTypes: [
93
+ 'application/json',
94
+ 'application/graphql+json'
95
+ ]
96
+ },
97
+ grafast: {
98
+ explain: true, // Enable plan diagrams
77
99
  }
78
- ],
79
-
80
- // Grafserv-specific options
81
- grafserv: {
82
- websockets: false,
83
- introspection: true
84
- }
100
+ },
85
101
  }
86
102
  })
87
103
  ```
@@ -104,41 +120,405 @@ type Mutation {
104
120
  2. Create your resolvers (`server/resolvers.ts`):
105
121
 
106
122
  ```typescript
107
- export default {
123
+ import { constant, lambda, access, object, filter, type GrafastSchemaConfig } from 'grafast'
124
+
125
+ const resolvers: GrafastSchemaConfig['objects'] = {
108
126
  Query: {
109
- hello: () => 'world',
110
- ping: () => true
127
+ plans: {
128
+ hello: () => constant('world'),
129
+ ping: () => constant(true)
130
+ }
111
131
  },
112
132
  Mutation: {
113
- echo: (_: unknown, { message }: { message: string }) => message
133
+ plans: {
134
+ echo: (_source, fieldArgs) => {
135
+ const { $message } = fieldArgs
136
+ return $message
137
+ }
138
+ }
114
139
  }
115
140
  }
141
+
142
+ export default resolvers
116
143
  ```
117
144
 
118
- ## Middleware
145
+ ## Advanced Usage
146
+
147
+ ### Middleware via Grafserv Plugins
148
+
149
+ For middleware functionality (authentication, logging, etc.), use Grafserv plugins. The CLI installer creates a `server/plugins.ts` file with examples.
119
150
 
120
- Add middleware functions to process requests:
151
+ #### Inline Plugin Configuration
121
152
 
122
153
  ```ts
123
- grafserv: {
124
- middleware: [
125
- // Logging middleware
126
- async (ctx, next) => {
127
- const start = Date.now()
128
- const result = await next()
129
- console.log(`Request took ${Date.now() - start}ms`)
130
- return result
154
+ export default defineNuxtConfig({
155
+ grafserv: {
156
+ preset: {
157
+ plugins: [
158
+ {
159
+ name: 'request-logging',
160
+ version: '1.0.0',
161
+ grafserv: {
162
+ middleware: {
163
+ processGraphQLRequestBody: async (next, event) => {
164
+ const start = Date.now()
165
+ console.log('[GraphQL] Request started')
166
+
167
+ const result = await next()
168
+
169
+ console.log(`[GraphQL] Completed in ${Date.now() - start}ms`)
170
+ return result
171
+ }
172
+ }
173
+ }
174
+ }
175
+ ]
176
+ }
177
+ }
178
+ })
179
+ ```
180
+
181
+ #### Using External Plugin File
182
+
183
+ ```ts
184
+ // nuxt.config.ts
185
+ import plugins from './server/plugins'
186
+
187
+ export default defineNuxtConfig({
188
+ grafserv: {
189
+ schema: 'server/schema.graphql',
190
+ resolvers: 'server/resolvers.ts',
191
+ preset: {
192
+ plugins
193
+ }
194
+ }
195
+ })
196
+ ```
197
+
198
+ ```ts
199
+ // server/plugins.ts
200
+ import type { GraphileConfig } from 'graphile-config'
201
+
202
+ const loggingPlugin: GraphileConfig.Plugin = {
203
+ name: 'request-logging',
204
+ version: '1.0.0',
205
+ grafserv: {
206
+ middleware: {
207
+ processGraphQLRequestBody: async (next, event) => {
208
+ console.log('Processing:', event.request.url)
209
+ return next()
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ const authPlugin: GraphileConfig.Plugin = {
216
+ name: 'authentication',
217
+ version: '1.0.0',
218
+ grafserv: {
219
+ middleware: {
220
+ processGraphQLRequestBody: async (next, event) => {
221
+ const token = event.request.headers.get('authorization')
222
+ // TODO: Validate token and add user to context
223
+ return next()
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ export default [loggingPlugin, authPlugin]
230
+ ```
231
+
232
+ #### Available Middleware Hooks
233
+
234
+ - `processRequest` - Process all incoming requests
235
+ - `processGraphQLRequestBody` - Process GraphQL request bodies
236
+ - `ruruHTML` - Customize Ruru IDE HTML generation
237
+ - `onSubscribe` - Handle GraphQL subscriptions
238
+
239
+ ## Advanced Usage
240
+
241
+ ### PostGraphile Integration
242
+
243
+ This module has first-class support for PostGraphile, enabling instant GraphQL APIs from PostgreSQL databases. PostGraphile generates the complete schema and resolvers, so you don't need to define them manually.
244
+
245
+ #### Setup with PostGraphile
246
+
247
+ 1. Install PostGraphile:
248
+
249
+ ```bash
250
+ pnpm add postgraphile @graphile/crystal graphile-build-pg
251
+ ```
252
+
253
+ 2. Create a PostGraphile schema provider (`server/graphql/schema.ts`):
254
+
255
+ ```typescript
256
+ import { postgraphile } from 'postgraphile'
257
+ import type { GraphQLSchema } from 'graphql'
258
+
259
+ // Define your PostGraphile preset
260
+ const preset = {
261
+ pgServices: [
262
+ {
263
+ name: 'main',
264
+ connectionString: process.env.DATABASE_URL || 'postgres://localhost/mydb',
265
+ schemas: ['public'],
266
+ },
267
+ ],
268
+ grafserv: {
269
+ websockets: false, // Disable websockets for Nuxt compatibility
270
+ },
271
+ }
272
+
273
+ // Create PostGraphile instance
274
+ const pgl = postgraphile(preset)
275
+
276
+ // Export schema provider function
277
+ export async function createPostGraphileSchema(): Promise<GraphQLSchema> {
278
+ const { schema } = await pgl.getSchemaResult()
279
+ return schema
280
+ }
281
+
282
+ // Export the instance if you need direct access
283
+ export { pgl }
284
+ ```
285
+
286
+ 3. Configure Nuxt to use the PostGraphile schema:
287
+
288
+ ```typescript
289
+ // nuxt.config.ts
290
+ import { createPostGraphileSchema } from './server/graphql/schema'
291
+
292
+ export default defineNuxtConfig({
293
+ modules: ['@stonecrop/nuxt-grafserv'],
294
+ grafserv: {
295
+ // Use PostGraphile schema provider function
296
+ schema: createPostGraphileSchema,
297
+ // No resolvers needed - PostGraphile generates everything
298
+ url: '/graphql',
299
+ graphiql: true, // Enable Ruru IDE
300
+ }
301
+ })
302
+ ```
303
+
304
+ #### Direct PostGraphile Instance Usage
305
+
306
+ You can also pass a PostGraphile instance directly:
307
+
308
+ ```typescript
309
+ // nuxt.config.ts
310
+ import { pgl } from './server/graphql/schema'
311
+
312
+ export default defineNuxtConfig({
313
+ grafserv: {
314
+ schema: pgl, // Pass PostGraphile instance directly
315
+ url: '/graphql',
316
+ }
317
+ })
318
+ ```
319
+
320
+ #### PostGraphile with Custom Plugins
321
+
322
+ Enhance your PostGraphile setup with custom plugins:
323
+
324
+ ```typescript
325
+ // server/graphql/schema.ts
326
+ import { postgraphile } from 'postgraphile'
327
+ import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
328
+ import PgSimplifyInflectorPlugin from '@graphile-contrib/pg-simplify-inflector'
329
+
330
+ const preset = {
331
+ extends: [PostGraphileAmberPreset],
332
+ plugins: [PgSimplifyInflectorPlugin],
333
+ pgServices: [
334
+ {
335
+ name: 'main',
336
+ connectionString: process.env.DATABASE_URL,
337
+ schemas: ['public'],
131
338
  },
132
- // Authentication middleware
133
- async (ctx, next) => {
134
- const token = ctx.req.headers.get('authorization')
135
- if (!token) throw new Error('Unauthorized')
136
- return next()
339
+ ],
340
+ grafserv: {
341
+ websockets: false,
342
+ },
343
+ grafast: {
344
+ explain: process.env.NODE_ENV === 'development', // Enable plan diagrams in dev
345
+ },
346
+ schema: {
347
+ // Add custom behavior overrides
348
+ defaultBehavior: 'connection', // Enable Relay-style connections by default
349
+ },
350
+ }
351
+
352
+ const pgl = postgraphile(preset)
353
+
354
+ export async function createPostGraphileSchema() {
355
+ const { schema } = await pgl.getSchemaResult()
356
+ return schema
357
+ }
358
+ ```
359
+
360
+ #### Benefits of PostGraphile Integration
361
+
362
+ - **Zero Schema Definition**: Automatically generates GraphQL schema from PostgreSQL
363
+ - **Auto-Generated Resolvers**: All queries and mutations created from database schema
364
+ - **Real-time Subscriptions**: Live query support via PostgreSQL LISTEN/NOTIFY
365
+ - **Row-Level Security**: Leverages PostgreSQL RLS for authorization
366
+ - **High Performance**: Uses Grafast execution engine with intelligent batching
367
+ - **Type Safety**: Full TypeScript support for generated schema
368
+
369
+ ### Custom Graphile Preset
370
+
371
+ Leverage the full power of the Graphile ecosystem with custom presets:
372
+
373
+ ```ts
374
+ import { PostGraphileAmberPreset } from 'postgraphile/presets/amber'
375
+
376
+ export default defineNuxtConfig({
377
+ grafserv: {
378
+ preset: {
379
+ extends: [PostGraphileAmberPreset],
380
+ grafast: {
381
+ explain: true, // Enable plan diagrams for debugging
382
+ context: {
383
+ // Custom context available in all resolvers
384
+ apiVersion: '1.0'
385
+ }
386
+ },
387
+ grafserv: {
388
+ graphqlOverGET: true,
389
+ maxRequestLength: 200000
390
+ }
137
391
  }
138
- ]
392
+ }
393
+ })
394
+ ```
395
+
396
+ ### Accessing Grafserv Instance
397
+
398
+ For advanced use cases, you can access the grafserv instance directly:
399
+
400
+ ```ts
401
+ // server/api/custom.ts
402
+ import { getGrafservInstance } from '@stonecrop/nuxt-grafserv/runtime/handler'
403
+ import { useRuntimeConfig } from '#imports'
404
+
405
+ export default defineEventHandler(async (event) => {
406
+ const config = useRuntimeConfig()
407
+ const serv = await getGrafservInstance(config.grafserv)
408
+
409
+ // Access grafserv methods or configuration
410
+ const preset = serv.getPreset()
411
+
412
+ return { preset }
413
+ })
414
+ ```
415
+
416
+ ### Schema Building with Objects Structure
417
+
418
+ The module uses Grafast's modern objects structure for better type safety and performance. Leverage Grafast's standard steps for common operations:
419
+
420
+ ```typescript
421
+ import { constant, lambda, access, object, filter, type GrafastSchemaConfig } from 'grafast'
422
+
423
+ const resolvers: GrafastSchemaConfig['objects'] = {
424
+ Query: {
425
+ plans: {
426
+ // Static values use constant()
427
+ hello: () => constant('world'),
428
+
429
+ // Arguments accessed via fieldArgs with $ prefix
430
+ user: (_source, fieldArgs) => {
431
+ const { $id } = fieldArgs
432
+ return lambda($id, (id) => getUserById(id))
433
+ },
434
+
435
+ // Use filter() for list filtering
436
+ userOrders: (_source, fieldArgs) => {
437
+ const { $userId } = fieldArgs
438
+ const $allOrders = constant(getAllOrders())
439
+ return filter($allOrders, $order =>
440
+ lambda([access($order, 'userId'), $userId],
441
+ ([orderUserId, userId]) => orderUserId === userId
442
+ )
443
+ )
444
+ }
445
+ }
446
+ },
447
+
448
+ Mutation: {
449
+ plans: {
450
+ // Use object() to compose objects from steps
451
+ createUser: (_source, fieldArgs) => {
452
+ const { $name, $email } = fieldArgs
453
+ const $id = constant(generateId())
454
+ const $now = constant(new Date().toISOString())
455
+
456
+ const $user = object({
457
+ id: $id,
458
+ name: $name,
459
+ email: $email,
460
+ role: constant('user'),
461
+ createdAt: $now,
462
+ updatedAt: $now
463
+ })
464
+
465
+ return lambda($user, user => {
466
+ saveUser(user)
467
+ return user
468
+ })
469
+ }
470
+ }
471
+ },
472
+
473
+ // Field resolvers for types
474
+ User: {
475
+ plans: {
476
+ fullName: ($user) => {
477
+ return lambda($user, (user) => {
478
+ const typed = user as { firstName: string; lastName: string }
479
+ return `${typed.firstName} ${typed.lastName}`
480
+ })
481
+ }
482
+ }
483
+ },
484
+
485
+ // Related type resolvers
486
+ Order: {
487
+ plans: {
488
+ // Use access() to extract properties before lookups
489
+ user: ($order) => {
490
+ const $userId = access($order, 'userId')
491
+ return lambda($userId, userId => getUserById(userId as string) ?? null)
492
+ }
493
+ }
494
+ }
139
495
  }
496
+
497
+ export default resolvers
140
498
  ```
141
499
 
500
+ **Key Concepts:**
501
+ - All resolvers return **steps** (constant, lambda, access, object, filter, etc.), not plain values
502
+ - Arguments are accessed via `fieldArgs.$argumentName` (note the `$` prefix)
503
+ - Use `constant()` for static values
504
+ - Use `lambda()` to transform step values at execution time
505
+ - Use `access()` to extract object properties (more efficient than lambda for simple property access)
506
+ - Use `object()` to compose objects from multiple steps
507
+ - Use `filter()` for list filtering operations
508
+ - Source objects (`$source`, `$user`, `$order`, etc.) are also steps that need `lambda()` or `access()` to work with their properties
509
+
510
+ **Standard Steps Reference:**
511
+ - `constant()` - Create a step from a static value
512
+ - `lambda()` - Transform step values with execution-time callbacks
513
+ - `access()` - Extract properties from object steps
514
+ - `object()` - Compose objects from multiple steps
515
+ - `filter()` - Filter list steps based on conditions
516
+ - `list()` - Create lists from step tuples
517
+ - `first()`/`last()` - Get first/last elements from lists
518
+ - `loadOne()`/`loadMany()` - Batch data loading (DataLoader-style)
519
+
520
+ For the complete list, see [Grafast Standard Steps](https://grafast.org/grafast/standard-steps)
521
+
142
522
  ## Development
143
523
 
144
524
  ```bash
@@ -162,10 +542,6 @@ pnpm run test
162
542
  pnpm run test:watch
163
543
  ```
164
544
 
165
- ## License
166
-
167
- [MIT License](./LICENSE)
168
-
169
545
  <!-- Badges -->
170
546
  [npm-version-src]: https://img.shields.io/npm/v/@stonecrop/nuxt-grafserv/latest.svg?style=flat&colorA=020420&colorB=00DC82
171
547
  [npm-version-href]: https://npmjs.com/package/@stonecrop/nuxt-grafserv
@@ -175,6 +551,3 @@ pnpm run test:watch
175
551
 
176
552
  [license-src]: https://img.shields.io/npm/l/@stonecrop/nuxt-grafserv.svg?style=flat&colorA=020420&colorB=00DC82
177
553
  [license-href]: https://npmjs.com/package/@stonecrop/nuxt-grafserv
178
-
179
- [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
180
- [nuxt-href]: https://nuxt.com
package/dist/module.d.mts CHANGED
@@ -1,61 +1,39 @@
1
1
  import { NuxtModule } from '@nuxt/schema';
2
+ import { PromiseOrDirect } from 'grafast';
2
3
  import { GraphQLSchema } from 'graphql';
3
4
  import { GraphileConfig } from 'graphile-config';
4
5
 
5
- /**
6
- * Context provided to GraphQL resolvers
7
- */
8
- type GrafastContext = {
9
- req: Request;
10
- params: Record<string, string>;
11
- [key: string]: unknown;
12
- };
13
- /**
14
- * Middleware function type for request processing
15
- */
16
- type MiddlewareFunction = (context: GrafastContext, next: () => Promise<GrafastContext>) => Promise<GrafastContext>;
17
6
  /**
18
7
  * Schema provider function - returns a GraphQL schema
19
8
  */
20
9
  type SchemaProvider = () => GraphQLSchema | Promise<GraphQLSchema>;
10
+ /**
11
+ * PostGraphile instance interface (minimal type for compatibility)
12
+ */
13
+ interface PostGraphileInstance {
14
+ getSchema(): PromiseOrDirect<GraphQLSchema>;
15
+ getSchemaResult(): PromiseOrDirect<{
16
+ schema: GraphQLSchema;
17
+ resolvedPreset: GraphileConfig.ResolvedPreset;
18
+ }>;
19
+ }
21
20
  /**
22
21
  * Configuration for the Grafast module
23
22
  */
24
23
  interface ModuleOptions {
25
- /** Path to schema file(s) or a schema provider function */
26
- schema?: string | string[] | SchemaProvider;
27
- /** Path to resolvers file (for .graphql schema files) */
24
+ /** Path to schema file(s), a schema provider function, or a PostGraphile instance */
25
+ schema?: string | string[] | SchemaProvider | PostGraphileInstance;
26
+ /** Path to resolvers file (optional, only needed for .graphql schema files without PostGraphile) */
28
27
  resolvers?: string;
29
28
  /** GraphQL endpoint URL (default: '/graphql/') */
30
29
  url?: string;
31
30
  /** Whether to enable GraphiQL IDE (default: true in dev, false in prod) */
32
31
  graphiql?: boolean;
33
- /**
34
- * Path to middleware file that exports an array of middleware functions.
35
- * This is the recommended approach as it preserves imports/dependencies.
36
- * Example: './server/middleware.ts'
37
- */
38
- middlewarePath?: string;
39
- /**
40
- * Middleware functions to process requests (inline).
41
- * Note: Inline middleware cannot reference external modules.
42
- * For middleware with dependencies, use middlewarePath instead.
43
- * @deprecated Use middlewarePath for middleware with external dependencies
44
- */
45
- middleware?: MiddlewareFunction[];
46
32
  /** Custom Graphile preset to extend (for advanced grafast configuration) */
47
33
  preset?: GraphileConfig.Preset;
48
- /** Additional Graphile plugins */
49
- plugins?: GraphileConfig.Plugin[];
50
- /** Grafserv options */
51
- grafserv?: {
52
- /** Whether to enable the GraphQL websocket endpoint */
53
- websockets?: boolean;
54
- /** Whether to enable introspection (default: true in dev) */
55
- introspection?: boolean;
56
- };
57
34
  }
58
35
 
59
36
  declare const module$1: NuxtModule<ModuleOptions>;
60
37
 
61
38
  export { module$1 as default };
39
+ export type { ModuleOptions, SchemaProvider };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "nuxt-grafserv",
2
+ "name": "@stonecrop/nuxt-grafserv",
3
3
  "configKey": "grafserv",
4
- "version": "0.7.4",
4
+ "version": "0.7.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,63 +1,91 @@
1
1
  import { join } from 'node:path';
2
+ import { createRequire } from 'node:module';
2
3
  import { useLogger, defineNuxtModule, createResolver } from '@nuxt/kit';
3
4
 
4
- const logger = useLogger("nuxt-grafserv");
5
+ const logger = useLogger("@stonecrop/nuxt-grafserv");
5
6
  const module$1 = defineNuxtModule({
6
7
  meta: {
7
- name: "nuxt-grafserv",
8
+ name: "@stonecrop/nuxt-grafserv",
8
9
  configKey: "grafserv"
9
10
  },
10
11
  defaults: (_nuxt) => ({
11
- schema: "./server/**/*.graphql",
12
- resolvers: "./server/resolvers.ts",
12
+ schema: "server/**/*.graphql",
13
+ resolvers: void 0,
14
+ // Optional - not needed for PostGraphile setups
13
15
  url: "/graphql/",
14
16
  graphiql: void 0,
15
17
  // Will default based on dev mode
16
- middleware: [],
17
18
  plugins: [],
18
- grafserv: {
19
- websockets: false,
20
- introspection: void 0
21
- // Will default based on dev mode
19
+ preset: {
20
+ grafserv: {
21
+ websockets: false
22
+ }
22
23
  }
23
24
  }),
24
25
  setup(options, nuxt) {
25
26
  const { resolve } = createResolver(import.meta.url);
27
+ const require = createRequire(import.meta.url);
26
28
  nuxt.hook("nitro:config", (config) => {
27
- const resolverPath = options.resolvers ? join(nuxt.options.srcDir, options.resolvers) : void 0;
29
+ config.alias = config.alias || {};
30
+ config.alias["#grafserv-server"] = join(nuxt.options.rootDir, "server");
31
+ const resolveForSchema = (path) => {
32
+ if (path.startsWith("/")) {
33
+ return path;
34
+ }
35
+ return join(nuxt.options.rootDir, path);
36
+ };
37
+ const resolveForVirtualModule = (path) => {
38
+ if (path.startsWith("/")) {
39
+ return path;
40
+ }
41
+ return join(nuxt.options.rootDir, path);
42
+ };
43
+ logger.info(`Nitro srcDir: ${config.srcDir}`);
44
+ logger.info(`Nuxt srcDir: ${nuxt.options.srcDir}`);
45
+ logger.info(`Nuxt rootDir: ${nuxt.options.rootDir}`);
46
+ logger.info(`Grafserv server alias: ${config.alias["#grafserv-server"]}`);
47
+ const resolverPath = options.resolvers ? resolveForVirtualModule(options.resolvers) : void 0;
48
+ if (resolverPath) {
49
+ logger.info(`Resolved resolver path: ${resolverPath}`);
50
+ } else {
51
+ logger.info("No resolvers configured (normal for PostGraphile setups)");
52
+ }
53
+ let runtimeSchema;
54
+ if (typeof options.schema === "function" || options.schema && typeof options.schema === "object" && "getSchema" in options.schema) {
55
+ runtimeSchema = options.schema;
56
+ logger.info("Using schema provider function or PostGraphile instance");
57
+ } else if (typeof options.schema === "string") {
58
+ runtimeSchema = resolveForSchema(options.schema);
59
+ logger.info(`Resolved schema path: ${runtimeSchema}`);
60
+ } else if (Array.isArray(options.schema)) {
61
+ runtimeSchema = options.schema.map((s) => resolveForSchema(s));
62
+ logger.info(`Resolved schema paths: ${runtimeSchema.join(", ")}`);
63
+ } else {
64
+ runtimeSchema = options.schema;
65
+ }
28
66
  config.runtimeConfig = config.runtimeConfig || {};
29
67
  config.runtimeConfig.grafserv = {
30
68
  ...options,
31
- // Resolve schema paths
32
- schema: typeof options.schema === "string" ? join(nuxt.options.srcDir, options.schema) : Array.isArray(options.schema) ? options.schema.map((s) => join(nuxt.options.srcDir, s)) : options.schema
33
- // function passed through
69
+ // Pass resolved resolver path for direct import
70
+ resolversPath: resolverPath,
71
+ // Pass schema (either resolved paths or function/instance)
72
+ schema: runtimeSchema
34
73
  };
35
74
  config.virtual = config.virtual || {};
36
75
  if (resolverPath) {
37
76
  config.virtual["#internal/grafserv/resolvers"] = `export { default } from '${resolverPath}'`;
38
77
  }
39
- let middlewareCode;
40
- if (options.middlewarePath) {
41
- const middlewarePath = join(nuxt.options.srcDir, options.middlewarePath);
42
- middlewareCode = `export { default } from '${middlewarePath}'`;
43
- } else if (options.middleware?.length) {
44
- logger.warn("Inline middleware is deprecated. Use middlewarePath for middleware with external dependencies.");
45
- middlewareCode = `export default [${options.middleware.map((fn) => fn.toString()).join(",")}]`;
46
- } else {
47
- middlewareCode = "export default []";
48
- }
49
- config.virtual["#internal/grafserv/middleware"] = middlewareCode;
50
78
  config.externals = config.externals || {};
51
- config.externals.inline = config.externals.inline || [];
52
- config.externals.inline.push(
79
+ config.externals.external = config.externals.external || [];
80
+ config.externals.external.push(
53
81
  "grafast",
54
- "grafserv",
55
- "grafserv/h3/v1",
56
- "graphile-config",
57
82
  "@graphql-tools/schema",
58
83
  "@graphql-tools/load",
59
84
  "@graphql-tools/graphql-file-loader"
60
85
  );
86
+ const grafastPath = require.resolve("grafast");
87
+ config.alias = config.alias || {};
88
+ config.alias["grafast"] = grafastPath;
61
89
  });
62
90
  nuxt.hook("nitro:config", (config) => {
63
91
  config.handlers = config.handlers || [];
@@ -66,18 +94,18 @@ const module$1 = defineNuxtModule({
66
94
  handler: resolve("./runtime/handler")
67
95
  });
68
96
  config.handlers.push({
69
- route: "/graphql/cache",
70
- handler: resolve("./runtime/cache")
97
+ route: "/ruru-static/**",
98
+ handler: resolve("./runtime/ruru-static")
71
99
  });
72
100
  config.handlers.push({
73
- route: "/ruru-static/**",
74
- handler: resolve("./runtime/handler")
101
+ route: "/graphql/cache",
102
+ handler: resolve("./runtime/cache")
75
103
  });
76
104
  });
77
105
  if (options.url) {
78
106
  nuxt.hook("devtools:customTabs", (tabs) => {
79
107
  tabs.push({
80
- name: "nuxt-grafserv",
108
+ name: "@stonecrop/nuxt-grafserv",
81
109
  title: "GraphQL (Grafserv)",
82
110
  icon: "simple-icons:graphql",
83
111
  view: {
@@ -93,7 +121,6 @@ const module$1 = defineNuxtModule({
93
121
  const isSchemaFile = path.endsWith(".graphql");
94
122
  const isResolverFile = options.resolvers && path.includes(options.resolvers.replace("./", ""));
95
123
  if (isSchemaFile || isResolverFile) {
96
- logger.info(`${path} changed`);
97
124
  if (!cacheClearing) {
98
125
  cacheClearing = true;
99
126
  try {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * GraphQL operations handler
3
+ * Handles POST requests with GraphQL queries, mutations, and subscriptions
4
+ */
5
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Buffer<ArrayBufferLike> | import("grafserv").JSONValue | undefined>>;
6
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { getGrafservInstance } from "./handler.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig();
6
+ const options = config.grafserv;
7
+ const serv = await getGrafservInstance(options);
8
+ return serv.handleGraphQLEvent(event);
9
+ });
@@ -1,9 +1,17 @@
1
+ import { grafserv } from 'grafserv/h3/v1';
2
+ import type { ModuleOptions } from '../types.js';
3
+ /**
4
+ * Get or create the grafserv instance
5
+ * Exported for use by separate handler files
6
+ */
7
+ export declare function getGrafservInstance(options: ModuleOptions): Promise<ReturnType<typeof grafserv>>;
1
8
  /**
2
9
  * Clear the cached instances (useful for development hot reload)
3
10
  */
4
11
  export declare function clearGrafservCache(): Promise<void>;
5
12
  /**
6
- * Main H3 event handler for GraphQL requests
13
+ * Main H3 event handler for GraphQL requests and Ruru UI
14
+ * Routes between GraphQL operations and GraphiQL UI based on request type
7
15
  */
8
16
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Buffer<ArrayBufferLike> | import("grafserv").JSONValue | undefined>>;
9
17
  export default _default;
@@ -1,22 +1,9 @@
1
- import { grafserv } from "grafserv/h3/v1";
2
- import { makeGrafastSchema } from "grafast";
3
1
  import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
4
2
  import { loadTypedefs } from "@graphql-tools/load";
3
+ import { grafserv } from "grafserv/h3/v1";
4
+ import { makeGrafastSchema } from "grafast";
5
5
  import { defineEventHandler } from "h3";
6
6
  import { useRuntimeConfig } from "nitropack/runtime";
7
- let middlewareFunctions = null;
8
- async function getMiddleware() {
9
- if (middlewareFunctions !== null) {
10
- return middlewareFunctions;
11
- }
12
- try {
13
- const mod = await import("#internal/grafserv/middleware");
14
- middlewareFunctions = mod.default || [];
15
- } catch {
16
- middlewareFunctions = [];
17
- }
18
- return middlewareFunctions;
19
- }
20
7
  let grafservInstance = null;
21
8
  let cachedSchema = null;
22
9
  async function loadTypeDefsFromFiles(schemaPath) {
@@ -26,58 +13,80 @@ async function loadTypeDefsFromFiles(schemaPath) {
26
13
  });
27
14
  return sources.map((source) => source.document).filter(Boolean);
28
15
  }
16
+ function isPostGraphileInstance(value) {
17
+ return value !== null && typeof value === "object" && "getSchema" in value && typeof value.getSchema === "function";
18
+ }
29
19
  async function getSchema(options) {
30
20
  if (cachedSchema) {
31
21
  return cachedSchema;
32
22
  }
33
23
  let schema;
34
- if (typeof options.schema === "function") {
24
+ if (isPostGraphileInstance(options.schema)) {
25
+ console.debug("[@stonecrop/nuxt-grafserv] Using PostGraphile instance for schema");
26
+ try {
27
+ if ("getSchemaResult" in options.schema && typeof options.schema.getSchemaResult === "function") {
28
+ const result = await options.schema.getSchemaResult();
29
+ schema = result.schema;
30
+ } else {
31
+ schema = await options.schema.getSchema();
32
+ }
33
+ console.debug("[@stonecrop/nuxt-grafserv] PostGraphile schema loaded successfully");
34
+ } catch (error) {
35
+ console.error("[@stonecrop/nuxt-grafserv] Error loading PostGraphile schema:", error);
36
+ throw error;
37
+ }
38
+ } else if (typeof options.schema === "function") {
35
39
  schema = await options.schema();
36
40
  } else if (options.schema) {
37
41
  const typeDefDocs = await loadTypeDefsFromFiles(options.schema);
38
- let plans = {};
42
+ const objects = {};
39
43
  if (options.resolvers) {
40
44
  try {
41
45
  const resolverModule = await import("#internal/grafserv/resolvers");
42
46
  const resolvers = resolverModule.default || resolverModule;
43
- console.log("[@stonecrop/nuxt-grafserv] Resolvers loaded:", Object.keys(resolvers));
47
+ console.debug("[@stonecrop/nuxt-grafserv] Resolvers loaded:", Object.keys(resolvers));
44
48
  for (const typeName of Object.keys(resolvers)) {
45
- plans[typeName] = {};
46
49
  const typeResolvers = resolvers[typeName];
47
- for (const fieldName of Object.keys(typeResolvers)) {
48
- const resolver = typeResolvers[fieldName];
49
- if (typeof resolver === "function") {
50
- plans[typeName][fieldName] = { resolve: resolver };
51
- } else {
52
- plans[typeName][fieldName] = resolver;
53
- }
50
+ if (typeResolvers && typeof typeResolvers === "object" && "plans" in typeResolvers) {
51
+ objects[typeName] = typeResolvers;
52
+ } else {
53
+ objects[typeName] = { plans: typeResolvers };
54
54
  }
55
55
  }
56
56
  } catch (e) {
57
- console.warn("[@stonecrop/nuxt-grafserv] Could not load resolvers:", e);
57
+ console.error("[@stonecrop/nuxt-grafserv] Error loading resolvers:", e);
58
+ console.warn("[@stonecrop/nuxt-grafserv] Continuing without resolvers - this is normal for PostGraphile setups");
58
59
  }
60
+ } else {
61
+ console.debug(
62
+ "[@stonecrop/nuxt-grafserv] No resolvers specified - using schema-only mode (normal for PostGraphile)"
63
+ );
64
+ }
65
+ try {
66
+ schema = makeGrafastSchema({
67
+ typeDefs: typeDefDocs,
68
+ objects
69
+ });
70
+ console.debug("[@stonecrop/nuxt-grafserv] Grafast schema created successfully");
71
+ } catch (error) {
72
+ console.error("[@stonecrop/nuxt-grafserv] Error creating Grafast schema:", error);
73
+ throw error;
59
74
  }
60
- schema = makeGrafastSchema({
61
- typeDefs: typeDefDocs,
62
- plans
63
- });
64
75
  } else {
65
- throw new Error("[@stonecrop/nuxt-grafserv] No schema provided. Configure schema path or provider function.");
76
+ throw new Error(
77
+ "[@stonecrop/nuxt-grafserv] No schema provided. Configure schema path, provider function, or PostGraphile instance."
78
+ );
66
79
  }
67
80
  cachedSchema = schema;
68
81
  return schema;
69
82
  }
70
- async function getGrafservInstance(options) {
83
+ export async function getGrafservInstance(options) {
71
84
  if (grafservInstance) {
85
+ console.log("[@stonecrop/nuxt-grafserv] Returning cached grafserv instance");
72
86
  return grafservInstance;
73
87
  }
74
88
  const schema = await getSchema(options);
75
- const isDev = process.env.NODE_ENV === "development";
76
- grafservInstance = grafserv({
77
- schema,
78
- graphiql: options.graphiql ?? isDev,
79
- websockets: options.grafserv?.websockets ?? false
80
- });
89
+ grafservInstance = grafserv({ schema, preset: options.preset });
81
90
  console.log("[@stonecrop/nuxt-grafserv] Grafserv instance created");
82
91
  return grafservInstance;
83
92
  }
@@ -86,46 +95,16 @@ export async function clearGrafservCache() {
86
95
  cachedSchema = null;
87
96
  console.log("[@stonecrop/nuxt-grafserv] Cache cleared");
88
97
  }
89
- async function applyMiddleware(context, middleware) {
90
- if (!middleware || middleware.length === 0) {
91
- return context;
92
- }
93
- const applyNext = async (index) => {
94
- if (index >= middleware.length) {
95
- return context;
96
- }
97
- return middleware[index](context, () => applyNext(index + 1));
98
- };
99
- return applyNext(0);
100
- }
101
98
  export default defineEventHandler(async (event) => {
102
99
  const config = useRuntimeConfig();
103
100
  const options = config.grafserv;
104
101
  try {
105
- if (event.node.req.url?.includes("/__grafserv_cache_clear") && process.env.NODE_ENV === "development") {
106
- await clearGrafservCache();
107
- return { success: true, message: "Cache cleared" };
108
- }
109
- const { req } = event.node;
110
- const context = {
111
- req: new Request(new URL(req.url || "/", `http://${req.headers.host || "localhost"}`), {
112
- method: req.method,
113
- headers: req.headers
114
- }),
115
- params: event.context.params || {}
116
- };
117
- const middleware = await getMiddleware();
118
- await applyMiddleware(context, middleware);
119
102
  const serv = await getGrafservInstance(options);
120
- const url = event.node.req.url || "";
121
- const method = event.node.req.method || "GET";
122
- if (url.includes("/ruru-static/")) {
123
- return serv.handleGraphiqlStaticEvent(event);
124
- }
125
- if (method === "GET" && url.match(/\/graphql\/?(\?.*)?$/)) {
126
- return serv.handleGraphiqlEvent(event);
103
+ const graphqlResult = await serv.handleGraphQLEvent(event);
104
+ if (graphqlResult !== null) {
105
+ return graphqlResult;
127
106
  }
128
- return serv.handleGraphQLEvent(event);
107
+ return serv.handleGraphiqlEvent(event);
129
108
  } catch (error) {
130
109
  console.error("[@stonecrop/nuxt-grafserv] Error in GraphQL handler:", error);
131
110
  throw error;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ruru/GraphiQL static assets handler
3
+ * Serves CSS, JavaScript, and other static files for the GraphQL IDE
4
+ */
5
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Buffer<ArrayBufferLike> | import("grafserv").JSONValue | undefined>>;
6
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { getGrafservInstance } from "./handler.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig();
6
+ const options = config.grafserv;
7
+ const serv = await getGrafservInstance(options);
8
+ return serv.handleGraphiqlStaticEvent(event);
9
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ruru/GraphiQL UI handler
3
+ * Serves the GraphQL IDE interface for exploring and testing the API
4
+ */
5
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Buffer<ArrayBufferLike> | import("grafserv").JSONValue | undefined>>;
6
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { getGrafservInstance } from "./handler.js";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig();
6
+ const options = config.grafserv;
7
+ const serv = await getGrafservInstance(options);
8
+ return serv.handleGraphiqlEvent(event);
9
+ });
package/dist/types.d.mts CHANGED
@@ -1,7 +1,3 @@
1
- import type { NuxtModule } from '@nuxt/schema'
2
-
3
- import type { default as Module } from './module.mjs'
4
-
5
- export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
-
7
1
  export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions, type SchemaProvider } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt-grafserv",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Pluggable Grafserv GraphQL server as Nuxt Module",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,16 +19,16 @@
19
19
  "exports": {
20
20
  ".": {
21
21
  "import": "./dist/module.mjs",
22
- "types": "./dist/types.d.mts"
22
+ "types": "./dist/module.d.mts"
23
23
  },
24
24
  "./runtime/*": "./dist/runtime/*"
25
25
  },
26
26
  "main": "./dist/module.mjs",
27
- "types": "./dist/types.d.mts",
27
+ "types": "./dist/module.d.mts",
28
28
  "typesVersions": {
29
29
  "*": {
30
30
  ".": [
31
- "./dist/types.d.mts"
31
+ "./dist/module.d.mts"
32
32
  ]
33
33
  }
34
34
  },
@@ -40,14 +40,14 @@
40
40
  "@graphql-tools/load": "^8.1.8",
41
41
  "@graphql-tools/stitch": "^10.1.8",
42
42
  "@graphql-tools/wrap": "^11.1.4",
43
- "grafast": "^1.0.0-rc.3",
44
- "grafserv": "^1.0.0-rc.3",
45
- "graphile-config": "^1.0.0-rc.2",
43
+ "grafast": "^1.0.0-rc.4",
44
+ "grafserv": "^1.0.0-rc.4",
45
+ "graphile-config": "^1.0.0-rc.3",
46
46
  "graphql": "^16.12.0",
47
- "@stonecrop/graphql-middleware": "0.7.4"
47
+ "@stonecrop/graphql-middleware": "0.7.6"
48
48
  },
49
49
  "devDependencies": {
50
- "@casl/ability": "^6.7.5",
50
+ "@casl/ability": "^6.8.0",
51
51
  "@eslint/js": "^9.39.2",
52
52
  "@nuxt/devtools": "^3.1.1",
53
53
  "@nuxt/eslint-config": "^1.12.1",
@@ -57,22 +57,23 @@
57
57
  "@nuxt/test-utils": "^3.23.0",
58
58
  "@vitest/coverage-istanbul": "^4.0.17",
59
59
  "@types/node": "^22.19.5",
60
- "h3": "^1.15.4",
61
- "nitropack": "^2.13.0",
60
+ "h3": "^1.15.5",
61
+ "nitropack": "^2.13.1",
62
62
  "changelogen": "^0.6.2",
63
63
  "eslint": "^9.39.2",
64
64
  "jsdom": "^27.4.0",
65
65
  "nuxt": "^4.2.2",
66
+ "postgraphile": "^5.0.0-rc.4",
66
67
  "typescript": "^5.9.3",
67
68
  "vite": "^7.3.1",
68
69
  "vitest": "^4.0.17",
69
70
  "vue-tsc": "^3.2.2",
70
- "@stonecrop/casl-middleware": "0.7.4",
71
- "@stonecrop/rockfoil": "0.7.4"
71
+ "@stonecrop/casl-middleware": "0.7.6",
72
+ "@stonecrop/rockfoil": "0.7.6"
72
73
  },
73
74
  "peerDependencies": {
74
- "@stonecrop/casl-middleware": "0.7.4",
75
- "@stonecrop/rockfoil": "0.7.4"
75
+ "@stonecrop/casl-middleware": "0.7.6",
76
+ "@stonecrop/rockfoil": "0.7.6"
76
77
  },
77
78
  "peerDependenciesMeta": {
78
79
  "@stonecrop/casl-middleware": {
@@ -93,6 +94,8 @@
93
94
  "lint": "eslint .",
94
95
  "test": "vitest run",
95
96
  "test:ci": "vitest run --run",
97
+ "test:coverage": "vitest run --coverage",
98
+ "test:e2e": "vitest run test/e2e",
96
99
  "test:watch": "vitest watch",
97
100
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
98
101
  }