@nestledjs/api 2.1.0 → 2.3.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.1.0",
3
+ "version": "2.3.0",
4
4
  "generators": "./generators.json",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -1,11 +1,9 @@
1
- import { Global, Module } from '@nestjs/common'
2
- import { ConfigModule } from '@<%= npmScope %>/api/config'
1
+ import { Module, Global } from '@nestjs/common'
3
2
 
4
3
  import { ApiCoreDataAccessService } from './api-core-data-access.service'
5
4
 
6
5
  @Global()
7
6
  @Module({
8
- imports: [ConfigModule],
9
7
  providers: [ApiCoreDataAccessService],
10
8
  exports: [ApiCoreDataAccessService],
11
9
  })
@@ -1,6 +1,5 @@
1
1
  import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
2
2
  import { Prisma, PrismaClient } from '@<%= npmScope %>/api/prisma'
3
- import { ConfigService } from '@<%= npmScope %>/api/config'
4
3
  import { CorePagingInput } from './dto/core-paging.input'
5
4
  import { PrismaPg } from '@prisma/adapter-pg'
6
5
 
@@ -20,8 +19,9 @@ export class ApiCoreDataAccessService
20
19
  extends PrismaClient
21
20
  implements OnModuleInit, OnModuleDestroy
22
21
  {
23
- constructor(private readonly configService: ConfigService) {
22
+ constructor() {
24
23
  const adapter = createAdapter()
24
+
25
25
  const config: Prisma.PrismaClientOptions = {
26
26
  adapter,
27
27
  log:
@@ -31,23 +31,19 @@ export class ApiCoreDataAccessService
31
31
  : [{ emit: 'event', level: 'warn' }],
32
32
  }
33
33
 
34
- // Must call super first before accessing this
35
34
  super(config)
36
35
  this.queryCount = 0
37
36
 
38
- // Check for Prisma Optimize setup using config service
39
- const optimizeEnabled = configService.prismaOptimizeEnabled
40
- const optimizeApiKey = configService.prismaOptimizeApiKey
37
+ // Only add Prisma Optimize extension in development
38
+ if (process.env['OPTIMIZE_API_KEY'] && process.env['USE_OPTIMIZE'] === 'true') {
39
+ const apiKey = process.env['OPTIMIZE_API_KEY']
40
+ if (!apiKey) {
41
+ console.warn('Not Running Prisma Optimize - No API Key Set')
42
+ }
41
43
 
42
- if (optimizeEnabled && optimizeApiKey) {
43
- console.log('🚀 Prisma Optimize enabled with API key')
44
44
  const { withOptimize } = require('@prisma/extension-optimize')
45
- const extendedClient = new PrismaClient(config).$extends(
46
- withOptimize({ apiKey: optimizeApiKey })
47
- )
45
+ const extendedClient = new PrismaClient(config).$extends(withOptimize({ apiKey }))
48
46
  Object.assign(this, extendedClient)
49
- } else if (optimizeEnabled && !optimizeApiKey) {
50
- console.warn('⚠️ OPTIMIZE_ENABLED is true but OPTIMIZE_API_KEY is not set')
51
47
  }
52
48
  }
53
49
 
@@ -1,17 +1,24 @@
1
1
  import { Logger } from '@nestjs/common'
2
- import { PubSub } from 'graphql-subscriptions'
3
2
  import { RedisPubSub } from 'graphql-redis-subscriptions'
3
+ import { PubSub } from 'graphql-subscriptions'
4
4
  import Redis from 'ioredis'
5
5
 
6
- // Support multiple Redis URL environment variables (Railway, Heroku, etc.)
6
+ // Railway provides REDIS_URL (private) and REDIS_PASSWORD separately
7
+ // REDIS_PUBLIC_URL is for external access, REDIS_URL is internal
7
8
  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
9
  const REDIS_PASSWORD = process.env['REDIS_PASSWORD'] ?? ''
10
- const secure = REDIS_URL ? /rediss:/.test(REDIS_URL) : false
11
10
 
12
- // Only attempt Redis connection if we have a valid URL
11
+ // Check if we have a valid Redis URL to connect to
13
12
  const hasValidRedisUrl = REDIS_URL && !REDIS_URL.includes('localhost') && REDIS_URL.length > 10
14
13
 
14
+ const secure = REDIS_URL ? /rediss:/.test(REDIS_URL) : false
15
+
16
+ if (hasValidRedisUrl) {
17
+ Logger.log(`🔴 Redis: Connecting to ${REDIS_URL.replace(/:[^:@]+@/, ':****@')} (password: ${REDIS_PASSWORD ? 'provided' : 'none'})`)
18
+ } else {
19
+ Logger.warn('🔴 Redis: No valid URL provided. Using in-memory PubSub (subscriptions will not work across instances).')
20
+ }
21
+
15
22
  const dateReviver = (_key: unknown, value: string | Date): Date => {
16
23
  const isISO8601Z = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/
17
24
  if (typeof value === 'string' && isISO8601Z.test(value)) {
@@ -24,26 +31,33 @@ const dateReviver = (_key: unknown, value: string | Date): Date => {
24
31
  return new Date(value)
25
32
  }
26
33
 
27
- function createPubSub(): RedisPubSub | PubSub {
34
+ function createRedisPubSub(): RedisPubSub | PubSub {
28
35
  if (!hasValidRedisUrl) {
29
- Logger.warn('No valid Redis URL provided. Using in-memory PubSub (not suitable for production with multiple instances).')
36
+ // Fall back to in-memory PubSub when Redis is not available
30
37
  return new PubSub()
31
38
  }
32
39
 
33
- Logger.verbose(`Connecting to Redis: ${REDIS_URL.replace(/:[^:@]+@/, ':****@')}`)
34
-
35
- const options: Redis.RedisOptions = {
40
+ const options = {
41
+ // Add password if provided separately (Railway sometimes does this)
36
42
  ...(REDIS_PASSWORD && { password: REDIS_PASSWORD }),
37
- ...(secure && { tls: { rejectUnauthorized: false } }),
43
+ // TLS settings for secure connections
44
+ ...(secure && {
45
+ tls: {
46
+ rejectUnauthorized: false,
47
+ },
48
+ }),
49
+ // Retry strategy to handle temporary connection issues
38
50
  retryStrategy: (times: number) => {
39
51
  if (times > 3) {
40
- Logger.warn('Redis connection failed after 3 retries. Falling back to in-memory PubSub.')
52
+ Logger.error(`🔴 Redis: Failed to connect after ${times} attempts. Giving up.`)
41
53
  return null // Stop retrying
42
54
  }
43
- return Math.min(times * 1000, 3000)
55
+ const delay = Math.min(times * 1000, 3000)
56
+ Logger.warn(`🔴 Redis: Connection attempt ${times} failed. Retrying in ${delay}ms...`)
57
+ return delay
44
58
  },
45
59
  maxRetriesPerRequest: 3,
46
- lazyConnect: true,
60
+ lazyConnect: true, // Don't connect immediately, wait for first command
47
61
  }
48
62
 
49
63
  try {
@@ -52,10 +66,16 @@ function createPubSub(): RedisPubSub | PubSub {
52
66
 
53
67
  // Handle connection errors gracefully
54
68
  publisher.on('error', (err) => {
55
- Logger.error(`Redis publisher error: ${err.message}`)
69
+ Logger.error(`🔴 Redis publisher error: ${err.message}`)
56
70
  })
57
71
  subscriber.on('error', (err) => {
58
- Logger.error(`Redis subscriber error: ${err.message}`)
72
+ Logger.error(`🔴 Redis subscriber error: ${err.message}`)
73
+ })
74
+ publisher.on('connect', () => {
75
+ Logger.log('🔴 Redis publisher connected')
76
+ })
77
+ subscriber.on('connect', () => {
78
+ Logger.log('🔴 Redis subscriber connected')
59
79
  })
60
80
 
61
81
  return new RedisPubSub({
@@ -64,9 +84,10 @@ function createPubSub(): RedisPubSub | PubSub {
64
84
  reviver: dateReviver,
65
85
  })
66
86
  } catch (error) {
67
- Logger.error(`Failed to create Redis PubSub: ${error}. Using in-memory fallback.`)
87
+ Logger.error(`🔴 Redis: Failed to create PubSub: ${error}`)
88
+ Logger.warn('🔴 Redis: Falling back to in-memory PubSub')
68
89
  return new PubSub()
69
90
  }
70
91
  }
71
92
 
72
- export const apiCorePubSub = createPubSub()
93
+ export const apiCorePubSub = createRedisPubSub()
@@ -24,6 +24,9 @@ const redisPubSubProvider = {
24
24
  GraphQLModule.forRoot<ApolloDriverConfig>({
25
25
  driver: ApolloDriver,
26
26
  autoSchemaFile: join(process.cwd(), 'api-schema.graphql'),
27
+ csrfPrevention: {
28
+ requestHeaders: ['apollo-require-preflight'],
29
+ },
27
30
  subscriptions: {
28
31
  'graphql-ws': {
29
32
  onConnect: async (context: Context<Record<string, unknown> | undefined>) => {
@@ -57,9 +60,6 @@ const redisPubSubProvider = {
57
60
  },
58
61
  },
59
62
  },
60
- csrfPrevention: {
61
- requestHeaders: ['apollo-require-preflight'],
62
- },
63
63
  context: ({ req, res, connectionParams }: { req: Partial<Request>; res: Response; connectionParams: ConnectionParameters }) => {
64
64
  if (connectionParams) {
65
65
  // Preserve existing req properties (user, organizationContext, etc.) while adding connection headers
@@ -69,10 +69,8 @@ const redisPubSubProvider = {
69
69
  },
70
70
  sortSchema: true,
71
71
  buildSchemaOptions: {
72
- dateScalarMode: 'isoDate', // Better interoperability (ISO strings, not JS Dates)
73
- numberScalarMode: 'float', // Default is fine; override to 'integer' if you hate floats
74
- scalarsMap: [], // Optional for custom scalar mappings if you use them
75
- skipCheck: true, // Skip extra metadata validation — speeds up build
72
+ dateScalarMode: 'timestamp',
73
+ numberScalarMode: 'float',
76
74
  },
77
75
  }),
78
76
  ],
@@ -1,6 +1,6 @@
1
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs' // Added readdirSync, lstatSync
2
- import { join } from 'path'
3
- import { execSync } from 'child_process'
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { execSync } from 'node:child_process'
4
4
  import { getDMMF } from '@prisma/internals'
5
5
 
6
6
  function extractQuotedStrings(input: string): string[] {
@@ -204,6 +204,10 @@ function generateModels(models: readonly any[], enums: readonly any[]): string {
204
204
  const usesDecimal = models.some(model =>
205
205
  model.fields.some((field: { type: string }) => field.type === 'Decimal'),
206
206
  )
207
+ // Check if any model field uses Json
208
+ const usesJson = models.some(model =>
209
+ model.fields.some((field: { type: string }) => field.type === 'Json'),
210
+ )
207
211
  if (usesDecimal) {
208
212
  output += `import Decimal from 'decimal.js';\n`
209
213
  output += `import { GraphQLDecimal } from 'prisma-graphql-type-decimal';\n`
@@ -211,9 +215,12 @@ function generateModels(models: readonly any[], enums: readonly any[]): string {
211
215
  if (usesBigInt) {
212
216
  output += `import { GraphQLBigInt } from 'graphql-scalars';\n`
213
217
  }
214
- // Ensure Prisma and enums are imported correctly
218
+ // Import JsonValue type if any model uses Json fields
219
+ if (usesJson) {
220
+ output += `import type { JsonValue } from '${prismaImportPath}';\n`
221
+ }
222
+ // Ensure enums are imported correctly
215
223
  const enumNames = enums.map(e => e.name)
216
- output += `import { Prisma } from '${prismaImportPath}';\n`
217
224
  if (enumNames.length > 0) {
218
225
  output += `import { ${enumNames.join(', ')} } from './enums';\n`
219
226
  }
@@ -244,7 +251,7 @@ function generateModels(models: readonly any[], enums: readonly any[]): string {
244
251
  } else if (originalType === 'DateTime') {
245
252
  tsType = 'Date'
246
253
  } else if (originalType === 'Json') {
247
- tsType = 'Prisma.JsonValue'
254
+ tsType = 'JsonValue'
248
255
  } else if (originalType === 'BigInt') {
249
256
  tsType = 'bigint' // Prisma uses bigint for BigInt
250
257
  } else if (originalType === 'Bytes') {
@@ -329,7 +336,9 @@ function generateEnums(enums: readonly any[]): string {
329
336
 
330
337
  if (enums.length > 0) {
331
338
  const enumNames = enums.map(e => e.name).join(', ')
332
- output += `export { ${enumNames} } from '${prismaImportPath}';\n\n`
339
+ // Import first to make enums available in scope, then export separately
340
+ output += `import { ${enumNames} } from '${prismaImportPath}';\n`
341
+ output += `export { ${enumNames} };\n\n`
333
342
 
334
343
  enums.forEach(enumType => {
335
344
  output += `registerEnumType(${enumType.name}, { name: '${enumType.name}' });\n\n`