@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 +1 -1
- package/src/core/files/data-access/src/lib/api-core-data-access.module.ts__tmpl__ +1 -3
- package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +9 -13
- package/src/core/files/data-access/src/lib/api-core-pub-sub.ts__tmpl__ +39 -18
- package/src/core/files/feature/src/lib/api-core-feature.module.ts__tmpl__ +5 -7
- package/src/core/files/models/src/lib/generate-models.ts__tmpl__ +16 -7
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
34
|
+
function createRedisPubSub(): RedisPubSub | PubSub {
|
|
28
35
|
if (!hasValidRedisUrl) {
|
|
29
|
-
|
|
36
|
+
// Fall back to in-memory PubSub when Redis is not available
|
|
30
37
|
return new PubSub()
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
|
|
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
|
-
|
|
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.
|
|
52
|
+
Logger.error(`🔴 Redis: Failed to connect after ${times} attempts. Giving up.`)
|
|
41
53
|
return null // Stop retrying
|
|
42
54
|
}
|
|
43
|
-
|
|
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(
|
|
69
|
+
Logger.error(`🔴 Redis publisher error: ${err.message}`)
|
|
56
70
|
})
|
|
57
71
|
subscriber.on('error', (err) => {
|
|
58
|
-
Logger.error(
|
|
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(
|
|
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 =
|
|
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: '
|
|
73
|
-
numberScalarMode: 'float',
|
|
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'
|
|
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
|
-
//
|
|
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 = '
|
|
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
|
-
|
|
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`
|