Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
97d411c
feat(nestjs-gateway): migrate express gateway to apollo server
TorinAsakura Jun 29, 2026
4e72360
fix(nestjs-gateway): preserve apollo runtime compatibility
TorinAsakura Jun 29, 2026
f5b142e
refactor(nestjs-gateway): split http gateway boundaries
TorinAsakura Jun 29, 2026
532d769
fix(nestjs-gateway): restore apollo mesh runtime boot
TorinAsakura Jun 30, 2026
995025f
fix(nestjs-gateway): preserve apollo health route
TorinAsakura Jun 30, 2026
61a43b3
fix(nestjs-gateway): preserve grpc error details
TorinAsakura Jun 30, 2026
e33e4b7
fix(nestjs-gateway): wrap subscription request context
TorinAsakura Jun 30, 2026
90569c7
fix(nestjs-gateway): load array type definition paths
TorinAsakura Jun 30, 2026
f1256c0
fix(nestjs-gateway): avoid internal module barrel imports
TorinAsakura Jun 30, 2026
6300293
fix(nestjs-gateway): harden grpc status detail decoding
TorinAsakura Jun 30, 2026
6726e59
fix(nestjs-gateway): normalize grpc status detail type names
TorinAsakura Jun 30, 2026
4dcd174
fix(nestjs-gateway): accept cross realm grpc detail constructors
TorinAsakura Jun 30, 2026
70f8856
fix(nestjs-gateway): resolve grpc detail constructors
TorinAsakura Jun 30, 2026
3506ef5
fix(nestjs-gateway): address apollo runtime review findings
TorinAsakura Jul 1, 2026
0ce1476
chore(nestjs-gateway): flatten module interfaces naming
TorinAsakura Jul 1, 2026
15594b9
chore(nestjs-gateway): move http interfaces to http boundary
TorinAsakura Jul 1, 2026
74be3fd
chore(nestjs-gateway): isolate graphql upload adapter
TorinAsakura Jul 1, 2026
96c6c71
chore(nestjs-gateway): use public grpc connectivity export
TorinAsakura Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,316 changes: 1,714 additions & 2,602 deletions .pnp.cjs

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions packages/nestjs-gateway/integration/test/gateway.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { MeshPubSub } from '@graphql-mesh/types'
import type { INestApplication } from '@nestjs/common'
import type { INestMicroservice } from '@nestjs/common'

import type { GatewaySourceType as GatewaySourceTypeEnum } from '../../src/index.js'
import type { GATEWAY_MESH_PUBSUB as GatewayMeshPubSubToken } from '../../src/index.js'
import type { GATEWAY_MODULE_OPTIONS as GatewayModuleOptionsToken } from '../../src/index.js'
import type { GatewayIntegrationModule as GatewayIntegrationModuleType } from '../src/index.js'

Expand All @@ -13,9 +15,8 @@ import { describe } from 'n
import { it } from 'node:test'
import { fileURLToPath } from 'node:url'

import { Transport } from '@nestjs/microservices'
import { Transport } from '@nestjs/microservices'
import { Test } from '@nestjs/testing'
import { PubSub } from 'graphql-subscriptions'
import { WebSocket } from 'ws'
import { buildClientSchema } from 'graphql'
import { printSchema } from 'graphql'
Expand All @@ -29,19 +30,21 @@ const moduleDir = path.dirname(fileURLToPath(import.meta.url))
// TODO: fix gateway integration test stability and re-enable suite.
describe.skip('gateway', () => {
let GatewaySourceType: typeof GatewaySourceTypeEnum
let GATEWAY_MESH_PUBSUB: typeof GatewayMeshPubSubToken
let GATEWAY_MODULE_OPTIONS: typeof GatewayModuleOptionsToken
let GatewayIntegrationModule: typeof GatewayIntegrationModuleType

let service: INestMicroservice
let app: INestApplication
let pubsub: PubSub
let pubsub: MeshPubSub
let url: string

before(async () => {
const gatewayCore = await import('../../src/index.js')
const gatewayIntegration = await import('../src/index.js')

GatewaySourceType = gatewayCore.GatewaySourceType
GATEWAY_MESH_PUBSUB = gatewayCore.GATEWAY_MESH_PUBSUB
GATEWAY_MODULE_OPTIONS = gatewayCore.GATEWAY_MODULE_OPTIONS
GatewayIntegrationModule = gatewayIntegration.GatewayIntegrationModule

Expand Down Expand Up @@ -159,7 +162,7 @@ describe.skip('gateway', () => {
await app.listen(appPort, '0.0.0.0')
await service.listen()

pubsub = app.get(PubSub)
pubsub = app.get(GATEWAY_MESH_PUBSUB)
url = await app.getUrl()
})

Expand Down
50 changes: 30 additions & 20 deletions packages/nestjs-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,37 @@
"postpack": "rm -rf dist"
},
"dependencies": {
"@atls/grpc-error-status": "0.1.5",
"@apollo/server": "5.5.1",
"@as-integrations/express4": "1.1.2",
Comment thread
TorinAsakura marked this conversation as resolved.
"@atls/grpc-error-status": "0.1.6",
"@atls/logger": "0.0.2",
"@graphql-mesh/cache-inmemory-lru": "0.6.18",
"@graphql-mesh/config": "0.19.8",
"@graphql-mesh/cross-helpers": "0.4.7",
"@graphql-mesh/merger-stitching": "0.102.11",
"@graphql-mesh/runtime": "0.18.7",
"@graphql-mesh/store": "0.102.11",
"@graphql-mesh/transform-cache": "0.102.11",
"@graphql-mesh/transform-encapsulate": "0.102.11",
"@graphql-mesh/transform-filter-schema": "0.102.11",
"@graphql-hive/pubsub": "2.1.1",
"@graphql-mesh/cache-inmemory-lru": "0.8.38",
"@graphql-mesh/config": "0.108.45",
"@graphql-mesh/cross-helpers": "0.4.14",
"@graphql-mesh/merger-stitching": "0.105.39",
"@graphql-mesh/runtime": "0.106.39",
"@graphql-mesh/store": "0.104.39",
"@graphql-mesh/transform-cache": "0.105.38",
"@graphql-mesh/transform-encapsulate": "0.104.37",
"@graphql-mesh/transform-filter-schema": "0.104.38",
"@graphql-mesh/transform-mock": "0.15.6",
"@graphql-mesh/transform-naming-convention": "0.102.11",
"@graphql-mesh/transform-prefix": "0.102.11",
"@graphql-mesh/transform-rename": "0.102.11",
"@graphql-mesh/transform-resolvers-composition": "0.102.11",
"@graphql-mesh/transform-naming-convention": "0.105.38",
"@graphql-mesh/transform-prefix": "0.105.38",
"@graphql-mesh/transform-rename": "0.105.38",
"@graphql-mesh/transform-resolvers-composition": "0.104.37",
"@graphql-mesh/transform-snapshot": "0.14.65",
"@graphql-mesh/types": "0.102.11",
"@graphql-mesh/utils": "0.102.11",
"@graphql-mesh/types": "0.104.29",
"@graphql-mesh/utils": "0.104.37",
"@graphql-tools/utils": "10.5.5",
"@grpc/grpc-js": "1.12.2",
"@grpc/proto-loader": "0.7.13",
"apollo-server-express": "2.26.2",
"cors": "2.8.5",
"express": "4.22.2",
"globby": "14.0.2",
"graphql-compose": "9.0.11",
"graphql-scalars": "1.23.0",
"graphql-subscriptions": "2.0.0",
"graphql-upload": "17.0.0",
"graphql-ws": "5.16.0",
"lodash": "4.17.21",
"long": "5.2.3",
Expand All @@ -55,12 +59,15 @@
"@nestjs/core": "10.4.6",
"@nestjs/microservices": "10.4.6",
"@nestjs/testing": "10.4.6",
"@types/cors": "2.8.19",
"@types/express": "4.17.25",
"@types/graphql-upload": "17.0.0",
"@types/lodash": "4.17.13",
"@types/node": "22.8.5",
"@types/supertest": "6.0.2",
"@types/ws": "8.5.12",
"get-port": "7.1.0",
"graphql": "16.9.0",
"graphql": "16.11.0",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"supertest": "6.3.4",
Expand All @@ -69,10 +76,13 @@
"peerDependencies": {
"@nestjs/common": "10",
"@nestjs/core": "10",
"graphql": "16.9",
"graphql": "16.11",
"reflect-metadata": "0.2",
"rxjs": "7"
},
"engines": {
"node": ">=24"
Comment thread
TorinAsakura marked this conversation as resolved.
},
"publishConfig": {
"exports": {
"./package.json": "./package.json",
Expand Down
2 changes: 0 additions & 2 deletions packages/nestjs-gateway/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export { PubSub } from 'graphql-subscriptions'

export * from './enums/index.js'
export * from './module/index.js'
77 changes: 77 additions & 0 deletions packages/nestjs-gateway/src/mesh/errors/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ServiceError } from '@grpc/grpc-js'
import type { GraphQLFormattedError } from 'graphql'

import { ErrorStatus } from '@atls/grpc-error-status'

type ErrorExtensions = {
exception?: Record<string, unknown> | ServiceError
}

const grpcErrorMessagePattern = /^(?<code>\d+)\s+(?<status>[A-Z_]+):\s*(?<message>.*)$/

const isGrpcErrorStatus = (error: unknown): error is ServiceError => {
if (typeof error !== 'object' || error === null) {
return false
}

const candidate = error as Partial<ServiceError>

return (
Number(candidate.code) >= 0 &&
candidate.metadata !== undefined &&
candidate.details !== undefined
)
}

const formatGrpcError = (error: ServiceError): Record<string, unknown> =>
ErrorStatus.fromServiceError(error).toObject()

const formatGrpcMessageError = (error: Error): Record<string, unknown> | undefined => {
const match = grpcErrorMessagePattern.exec(error.message)

if (!match?.groups) {
return undefined
}

return {
status: match.groups.status,
code: Number(match.groups.code),
message: match.groups.message,
details: [],
}
}

export const formatError = (
error: GraphQLFormattedError & { extensions?: ErrorExtensions },
exceptionOverride?: unknown
): GraphQLFormattedError => {
const exception = isGrpcErrorStatus(exceptionOverride)
? exceptionOverride
: error.extensions?.exception

if (exception && isGrpcErrorStatus(exception)) {
return {
...error,
extensions: {
...error.extensions,
exception: formatGrpcError(exception),
},
}
}

if (exceptionOverride instanceof Error) {
const formattedException = formatGrpcMessageError(exceptionOverride)

if (formattedException) {
return {
...error,
extensions: {
...error.extensions,
exception: formattedException,
},
}
}
}

return error
}
2 changes: 2 additions & 0 deletions packages/nestjs-gateway/src/mesh/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './format.js'
export * from './unsupported.js'
84 changes: 84 additions & 0 deletions packages/nestjs-gateway/src/mesh/errors/tests/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import assert from 'node:assert/strict'
import { describe } from 'node:test'
import { it } from 'node:test'

import { BadRequest } from '@atls/grpc-error-status'
import { ErrorStatus } from '@atls/grpc-error-status'
import { status } from '@grpc/grpc-js'

import { formatError } from '../format.js'

describe('formatError', () => {
it('formats direct gRPC service errors', () => {
const serviceError = new ErrorStatus(status.INVALID_ARGUMENT, 'Test').toServiceError()

const formattedError = formatError({
message: '3 INVALID_ARGUMENT: Test',
extensions: {
exception: serviceError,
},
})

assert.deepEqual(formattedError.extensions?.exception, {
status: 'INVALID_ARGUMENT',
code: status.INVALID_ARGUMENT,
message: 'Test',
details: [],
})
})

it('preserves gRPC status details', () => {
const violation = new BadRequest.FieldViolation()
const badRequest = new BadRequest()

violation.setField('id')
violation.setDescription('id must be an email')
badRequest.addFieldViolations(violation)
const serviceError = new ErrorStatus(status.INVALID_ARGUMENT, 'Request validation failed')
.addDetail(badRequest)
.toServiceError()

const formattedError = formatError({
message: '3 INVALID_ARGUMENT: Request validation failed',
extensions: {
exception: serviceError,
},
})

assert.deepEqual(formattedError.extensions?.exception, {
status: 'INVALID_ARGUMENT',
code: status.INVALID_ARGUMENT,
message: 'Request validation failed',
details: [
{
'@type': 'type.googleapis.com/google.rpc.BadRequest',
fieldViolationsList: [
{
field: 'id',
description: 'id must be an email',
},
],
},
],
})
})

it('formats gRPC errors unwrapped as regular errors', () => {
const formattedError = formatError(
{
message: '3 INVALID_ARGUMENT: Test',
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
},
new Error('3 INVALID_ARGUMENT: Test')
)

assert.deepEqual(formattedError.extensions?.exception, {
status: 'INVALID_ARGUMENT',
code: status.INVALID_ARGUMENT,
message: 'Test',
details: [],
})
})
})
6 changes: 6 additions & 0 deletions packages/nestjs-gateway/src/mesh/errors/unsupported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class GatewayUnsupportedHttpAdapterError extends Error {
constructor(adapter: string) {
super(`Gateway HTTP adapter "${adapter}" is not supported`)
this.name = 'GatewayUnsupportedHttpAdapterError'
}
}
22 changes: 0 additions & 22 deletions packages/nestjs-gateway/src/mesh/format.error.ts

This file was deleted.

Loading
Loading