@nestledjs/api 2.0.2 → 2.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestledjs/api",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "generators": "./generators.json",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -1,8 +1,9 @@
1
- import { Module } from '@nestjs/common'
1
+ import { Global, Module } from '@nestjs/common'
2
2
  import { ConfigModule } from '@<%= npmScope %>/api/config'
3
3
 
4
4
  import { ApiCoreDataAccessService } from './api-core-data-access.service'
5
5
 
6
+ @Global()
6
7
  @Module({
7
8
  imports: [ConfigModule],
8
9
  providers: [ApiCoreDataAccessService],
@@ -4,16 +4,24 @@ import { ConfigService } from '@<%= npmScope %>/api/config'
4
4
  import { CorePagingInput } from './dto/core-paging.input'
5
5
  import { PrismaPg } from '@prisma/adapter-pg'
6
6
 
7
+ function createAdapter() {
8
+ const baseUrl = process.env['DATABASE_URL'] || ''
9
+ const separator = baseUrl.includes('?') ? '&' : '?'
10
+ const usePgBouncer = process.env['PGBOUNCER_ENABLED'] === 'true'
11
+ const connectionParams = usePgBouncer
12
+ ? 'pgbouncer=true&connection_limit=5&pool_timeout=10'
13
+ : 'connection_limit=30'
14
+ const connectionString = `${baseUrl}${separator}${connectionParams}`
15
+ return new PrismaPg({ connectionString })
16
+ }
17
+
7
18
  @Injectable()
8
19
  export class ApiCoreDataAccessService
9
20
  extends PrismaClient
10
21
  implements OnModuleInit, OnModuleDestroy
11
22
  {
12
23
  constructor(private readonly configService: ConfigService) {
13
- // Create the PostgreSQL adapter with the connection string
14
- const connectionString = `${process.env['DATABASE_URL']}?connection_limit=30`
15
- const adapter = new PrismaPg({ connectionString })
16
-
24
+ const adapter = createAdapter()
17
25
  const config: Prisma.PrismaClientOptions = {
18
26
  adapter,
19
27
  log:
@@ -1,22 +1,16 @@
1
1
  import { Logger } from '@nestjs/common'
2
+ import { PubSub } from 'graphql-subscriptions'
2
3
  import { RedisPubSub } from 'graphql-redis-subscriptions'
3
4
  import Redis from 'ioredis'
4
5
 
5
- const REDIS_URL = process.env['REDIS_TLS_URL'] ?? process.env['REDIS_URL'] ?? ''
6
+ // Support multiple Redis URL environment variables (Railway, Heroku, etc.)
7
+ const REDIS_URL = process.env['REDIS_URL'] ?? process.env['REDIS_PRIVATE_URL'] ?? process.env['REDIS_TLS_URL'] ?? ''
8
+ // Some PaaS providers (like Railway) provide password separately
9
+ const REDIS_PASSWORD = process.env['REDIS_PASSWORD'] ?? ''
6
10
  const secure = REDIS_URL ? /rediss:/.test(REDIS_URL) : false
7
- Logger.verbose(`Redis URL: ${REDIS_URL}`)
8
11
 
9
- if (!REDIS_URL) {
10
- Logger.warn('No Redis URL provided. PubSub functionality may be limited.')
11
- }
12
-
13
- const options = secure
14
- ? {
15
- tls: {
16
- rejectUnauthorized: false,
17
- },
18
- }
19
- : {}
12
+ // Only attempt Redis connection if we have a valid URL
13
+ const hasValidRedisUrl = REDIS_URL && !REDIS_URL.includes('localhost') && REDIS_URL.length > 10
20
14
 
21
15
  const dateReviver = (_key: unknown, value: string | Date): Date => {
22
16
  const isISO8601Z = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/
@@ -30,8 +24,49 @@ const dateReviver = (_key: unknown, value: string | Date): Date => {
30
24
  return new Date(value)
31
25
  }
32
26
 
33
- export const apiCorePubSub = new RedisPubSub({
34
- publisher: new Redis(REDIS_URL ?? 'redis://localhost:6379', options),
35
- subscriber: new Redis(REDIS_URL ?? 'redis://localhost:6379', options),
36
- reviver: dateReviver,
37
- })
27
+ function createPubSub(): RedisPubSub | PubSub {
28
+ if (!hasValidRedisUrl) {
29
+ Logger.warn('No valid Redis URL provided. Using in-memory PubSub (not suitable for production with multiple instances).')
30
+ return new PubSub()
31
+ }
32
+
33
+ Logger.verbose(`Connecting to Redis: ${REDIS_URL.replace(/:[^:@]+@/, ':****@')}`)
34
+
35
+ const options: Redis.RedisOptions = {
36
+ ...(REDIS_PASSWORD && { password: REDIS_PASSWORD }),
37
+ ...(secure && { tls: { rejectUnauthorized: false } }),
38
+ retryStrategy: (times: number) => {
39
+ if (times > 3) {
40
+ Logger.warn('Redis connection failed after 3 retries. Falling back to in-memory PubSub.')
41
+ return null // Stop retrying
42
+ }
43
+ return Math.min(times * 1000, 3000)
44
+ },
45
+ maxRetriesPerRequest: 3,
46
+ lazyConnect: true,
47
+ }
48
+
49
+ try {
50
+ const publisher = new Redis(REDIS_URL, options)
51
+ const subscriber = new Redis(REDIS_URL, options)
52
+
53
+ // Handle connection errors gracefully
54
+ publisher.on('error', (err) => {
55
+ Logger.error(`Redis publisher error: ${err.message}`)
56
+ })
57
+ subscriber.on('error', (err) => {
58
+ Logger.error(`Redis subscriber error: ${err.message}`)
59
+ })
60
+
61
+ return new RedisPubSub({
62
+ publisher,
63
+ subscriber,
64
+ reviver: dateReviver,
65
+ })
66
+ } catch (error) {
67
+ Logger.error(`Failed to create Redis PubSub: ${error}. Using in-memory fallback.`)
68
+ return new PubSub()
69
+ }
70
+ }
71
+
72
+ export const apiCorePubSub = createPubSub()
@@ -57,9 +57,13 @@ const redisPubSubProvider = {
57
57
  },
58
58
  },
59
59
  },
60
+ csrfPrevention: {
61
+ requestHeaders: ['apollo-require-preflight'],
62
+ },
60
63
  context: ({ req, res, connectionParams }: { req: Partial<Request>; res: Response; connectionParams: ConnectionParameters }) => {
61
64
  if (connectionParams) {
62
- req = { headers: connectionParams.headers }
65
+ // Preserve existing req properties (user, organizationContext, etc.) while adding connection headers
66
+ req = { ...req, headers: connectionParams.headers }
63
67
  }
64
68
  return { req, res }
65
69
  },