@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 +1 -1
- package/src/core/files/data-access/src/lib/api-core-data-access.module.ts__tmpl__ +2 -1
- package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +12 -4
- package/src/core/files/data-access/src/lib/api-core-pub-sub.ts__tmpl__ +53 -18
- package/src/core/files/feature/src/lib/api-core-feature.module.ts__tmpl__ +5 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
},
|