@nestledjs/api 0.0.1
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/CHANGELOG.md +5 -0
- package/README.md +11 -0
- package/eslint.config.cjs +28 -0
- package/generators.json +69 -0
- package/package.json +21 -0
- package/project.json +47 -0
- package/src/account/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/account/files/data-access/src/lib/api-account-data-access.module.ts__tmpl__ +10 -0
- package/src/account/files/data-access/src/lib/api-account-data-access.service.ts__tmpl__ +152 -0
- package/src/account/files/data-access/src/lib/dto/account-create-email.input.ts__tmpl__ +9 -0
- package/src/account/files/data-access/src/lib/dto/account-update-password.input.ts__tmpl__ +16 -0
- package/src/account/files/data-access/src/lib/dto/account-update-profile.input.ts__tmpl__ +25 -0
- package/src/account/files/feature/src/index.ts__tmpl__ +1 -0
- package/src/account/files/feature/src/lib/api-account-feature.module.ts__tmpl__ +9 -0
- package/src/account/files/feature/src/lib/api-account-feature.resolver.ts__tmpl__ +83 -0
- package/src/account/generator.spec.ts +71 -0
- package/src/account/generator.ts +20 -0
- package/src/account/schema.d.ts +3 -0
- package/src/account/schema.json +13 -0
- package/src/app/files/src/app.config.ts__tmpl__ +66 -0
- package/src/app/files/src/app.module.ts__tmpl__ +43 -0
- package/src/app/files/src/applogger.middleware.ts__tmpl__ +21 -0
- package/src/app/files/src/main.ts__tmpl__ +33 -0
- package/src/app/files/webpack.config.js__tmpl__ +54 -0
- package/src/app/generator.spec.ts +112 -0
- package/src/app/generator.ts +105 -0
- package/src/app/schema.d.ts +1 -0
- package/src/app/schema.json +9 -0
- package/src/config/files/src/index.ts__tmpl__ +3 -0
- package/src/config/files/src/lib/config.service.ts__tmpl__ +51 -0
- package/src/config/files/src/lib/configuration.ts__tmpl__ +32 -0
- package/src/config/files/src/lib/validation.ts__tmpl__ +21 -0
- package/src/config/generator.spec.ts +47 -0
- package/src/config/generator.ts +16 -0
- package/src/config/schema.d.ts +3 -0
- package/src/config/schema.json +13 -0
- package/src/core/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/core/files/data-access/src/lib/api-core-data-access.module.ts__tmpl__ +9 -0
- package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +97 -0
- package/src/core/files/data-access/src/lib/api-core-pub-sub.ts__tmpl__ +37 -0
- package/src/core/files/data-access/src/lib/dto/core-paging.input.ts__tmpl__ +26 -0
- package/src/core/files/data-access/src/lib/dto/multi-select-input.ts__tmpl__ +7 -0
- package/src/core/files/data-access/src/lib/models/core-paging.ts__tmpl__ +19 -0
- package/src/core/files/feature/src/index.ts__tmpl__ +2 -0
- package/src/core/files/feature/src/lib/api-core-feature.controller.ts__tmpl__ +12 -0
- package/src/core/files/feature/src/lib/api-core-feature.module.ts__tmpl__ +86 -0
- package/src/core/files/feature/src/lib/api-core-feature.resolver.ts__tmpl__ +12 -0
- package/src/core/files/feature/src/lib/api-core-feature.service.ts__tmpl__ +55 -0
- package/src/core/files/feature/src/lib/config/configuration.ts__tmpl__ +32 -0
- package/src/core/files/feature/src/lib/config/validation.ts__tmpl__ +25 -0
- package/src/core/files/feature/src/lib/plugins/complexity.plugin.ts__tmpl__ +51 -0
- package/src/core/files/feature/src/lib/plugins/logging.plugin.ts__tmpl__ +17 -0
- package/src/core/files/models/src/index.ts__tmpl__ +1 -0
- package/src/core/files/models/src/lib/generate-models.ts__tmpl__ +294 -0
- package/src/core/files/models/src/lib/models/core-paging.model.ts__tmpl__ +25 -0
- package/src/core/generator.spec.ts +85 -0
- package/src/core/generator.ts +35 -0
- package/src/core/schema.d.ts +3 -0
- package/src/core/schema.json +13 -0
- package/src/custom/generator.spec.ts +75 -0
- package/src/custom/generator.ts +239 -0
- package/src/custom/schema.json +21 -0
- package/src/custom/schema.ts +5 -0
- package/src/extended/generator.spec.ts +95 -0
- package/src/extended/generator.ts +161 -0
- package/src/extended/index.ts +1 -0
- package/src/extended/schema.json +12 -0
- package/src/extended/schema.ts +3 -0
- package/src/generate-crud/files/data-access/src/index.ts__tmpl__ +3 -0
- package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.module.ts__tmpl__ +11 -0
- package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.service.ts__tmpl__ +72 -0
- package/src/generate-crud/files/data-access/src/lib/dto/index.ts__tmpl__ +224 -0
- package/src/generate-crud/files/feature/.gitkeep +0 -0
- package/src/generate-crud/generator.spec.ts +84 -0
- package/src/generate-crud/generator.ts +354 -0
- package/src/generate-crud/schema.json +32 -0
- package/src/generate-crud/schema.ts +8 -0
- package/src/index.ts +13 -0
- package/src/plugin/generator.spec.ts +18 -0
- package/src/plugin/generator.ts +74 -0
- package/src/plugin/schema.json +14 -0
- package/src/plugin/schema.ts +4 -0
- package/src/prisma/files/src/index.ts__tmpl__ +1 -0
- package/src/prisma/files/src/lib/.gitkeep +1 -0
- package/src/prisma/files/src/lib/schemas/schema.prisma__tmpl__ +402 -0
- package/src/prisma/files/src/lib/seed/seed-data/iso-3166-countries.ts__tmpl__ +3239 -0
- package/src/prisma/files/src/lib/seed/seed-data/seed-users.ts__tmpl__ +32 -0
- package/src/prisma/files/src/lib/seed/seed.ts__tmpl__ +64 -0
- package/src/prisma/generator.spec.ts +60 -0
- package/src/prisma/generator.ts +61 -0
- package/src/prisma/schema.d.ts +3 -0
- package/src/prisma/schema.json +13 -0
- package/src/setup/generator.md +49 -0
- package/src/setup/generator.spec.ts +18 -0
- package/src/setup/generator.ts +106 -0
- package/src/setup/schema.json +8 -0
- package/src/smtp-mailer/files/data-access/src/index.ts__tmpl__ +2 -0
- package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.module.ts__tmpl__ +10 -0
- package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.service.ts__tmpl__ +61 -0
- package/src/smtp-mailer/generator.spec.ts +41 -0
- package/src/smtp-mailer/generator.ts +14 -0
- package/src/smtp-mailer/schema.d.ts +0 -0
- package/src/smtp-mailer/schema.json +7 -0
- package/src/user/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/user/files/data-access/src/lib/api-user-data-access.module.ts__tmpl__ +10 -0
- package/src/user/files/data-access/src/lib/api-user-data-access.service.ts__tmpl__ +119 -0
- package/src/user/files/data-access/src/lib/dto/admin-create-user.input.ts__tmpl__ +20 -0
- package/src/user/files/data-access/src/lib/dto/admin-update-user.input.ts__tmpl__ +29 -0
- package/src/user/files/feature/src/index.ts__tmpl__ +1 -0
- package/src/user/files/feature/src/lib/api-user-feature-admin.resolver.ts__tmpl__ +57 -0
- package/src/user/files/feature/src/lib/api-user-feature.module.ts__tmpl__ +10 -0
- package/src/user/files/feature/src/lib/api-user-feature.resolver.ts__tmpl__ +17 -0
- package/src/user/generator.spec.ts +41 -0
- package/src/user/generator.ts +15 -0
- package/src/user/schema.d.ts +0 -0
- package/src/user/schema.json +7 -0
- package/src/utils/files/src/index.ts__tmpl__ +3 -0
- package/src/utils/files/src/lib/decorators/ctx-user.decorator.ts__tmpl__ +6 -0
- package/src/utils/files/src/lib/guards/gql-auth-admin.guard.ts__tmpl__ +39 -0
- package/src/utils/files/src/lib/guards/gql-auth.guard.ts__tmpl__ +11 -0
- package/src/utils/generator.ts +14 -0
- package/src/utils/schema.json +8 -0
- package/src/workspace-setup/generator.md +39 -0
- package/src/workspace-setup/generator.spec.ts +82 -0
- package/src/workspace-setup/generator.ts +49 -0
- package/src/workspace-setup/lib/helpers.ts +142 -0
- package/src/workspace-setup/schema.d.ts +3 -0
- package/src/workspace-setup/schema.json +7 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +23 -0
- package/tsconfig.spec.json +22 -0
- package/vite.config.mts +37 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
|
|
2
|
+
import { Prisma, PrismaClient } from '@<%= npmScope %>/api/prisma'
|
|
3
|
+
import { CorePagingInput } from './dto/core-paging.input'
|
|
4
|
+
import { withOptimize } from '@prisma/extension-optimize'
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class ApiCoreDataAccessService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
8
|
+
constructor() {
|
|
9
|
+
const config: Prisma.PrismaClientOptions = {
|
|
10
|
+
datasources: {
|
|
11
|
+
db: { url: `${process.env['DATABASE_URL']}?connection_limit=30` },
|
|
12
|
+
},
|
|
13
|
+
log:
|
|
14
|
+
process.env['LOG_PRISMA_QUERIES'] === 'true' || process.env['COUNT_PRISMA_QUERIES'] === 'true'
|
|
15
|
+
? [{ emit: 'event', level: 'query' }]
|
|
16
|
+
: [{ emit: 'event', level: 'warn' }],
|
|
17
|
+
}
|
|
18
|
+
super(config)
|
|
19
|
+
this.queryCount = 0
|
|
20
|
+
|
|
21
|
+
// Only add Prisma Optimize extension in development
|
|
22
|
+
if (process.env['OPTIMIZE_API_KEY'] && process.env['USE_OPTIMIZE'] === 'true') {
|
|
23
|
+
const apiKey = process.env['OPTIMIZE_API_KEY']
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
console.warn('Not Running Prisma Optimize - No API Key Set')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const extendedClient = new PrismaClient(config).$extends(withOptimize({ apiKey }))
|
|
29
|
+
Object.assign(this, extendedClient)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public queryCount: number
|
|
34
|
+
|
|
35
|
+
public async onModuleDestroy(): Promise<void> {
|
|
36
|
+
await this.$disconnect()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async onModuleInit(): Promise<void> {
|
|
40
|
+
await this.$connect()
|
|
41
|
+
|
|
42
|
+
if (process.env['LOG_PRISMA_QUERIES'] == 'true') {
|
|
43
|
+
this.$on('query' as never, async (e: Prisma.QueryEvent) => {
|
|
44
|
+
console.log(`QUERY: ${e.query} \n\nPARAMS: ${e.params}\n\n\n`)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (process.env['COUNT_PRISMA_QUERIES'] == 'true') {
|
|
49
|
+
this.$on('query' as never, async () => {
|
|
50
|
+
this.queryCount++
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
filter<T extends Record<string, unknown>>(input: CorePagingInput = {}): {
|
|
56
|
+
skip: number
|
|
57
|
+
take: number
|
|
58
|
+
where?: T
|
|
59
|
+
orderBy: { [key: string]: 'asc' | 'desc' }
|
|
60
|
+
} {
|
|
61
|
+
const {
|
|
62
|
+
search = '',
|
|
63
|
+
searchFields = [],
|
|
64
|
+
take = 20,
|
|
65
|
+
skip = 0,
|
|
66
|
+
orderBy = 'id',
|
|
67
|
+
orderDirection = 'asc',
|
|
68
|
+
filters = {},
|
|
69
|
+
} = input
|
|
70
|
+
|
|
71
|
+
const trimmedSearch = search.trim()
|
|
72
|
+
const andConditions: unknown[] = []
|
|
73
|
+
|
|
74
|
+
if (Object.keys(filters).length > 0) {
|
|
75
|
+
andConditions.push(filters)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (trimmedSearch && searchFields.length > 0) {
|
|
79
|
+
const terms = trimmedSearch.includes(' ') ? trimmedSearch.split(' ') : [trimmedSearch].filter(Boolean)
|
|
80
|
+
const searchFilters = terms.map((term) => ({
|
|
81
|
+
OR: searchFields.map((field) => ({
|
|
82
|
+
[field]: { contains: term, mode: Prisma.QueryMode.insensitive },
|
|
83
|
+
})),
|
|
84
|
+
}))
|
|
85
|
+
andConditions.push(...searchFilters)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const where = andConditions.length > 0 ? { AND: andConditions } : undefined
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
skip,
|
|
92
|
+
take,
|
|
93
|
+
where: where as unknown as T, // assert type safety for the generic
|
|
94
|
+
orderBy: { [orderBy]: orderDirection },
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common'
|
|
2
|
+
import { RedisPubSub } from 'graphql-redis-subscriptions'
|
|
3
|
+
import Redis from 'ioredis'
|
|
4
|
+
|
|
5
|
+
const REDIS_URL = process.env['REDIS_TLS_URL'] ?? process.env['REDIS_URL'] ?? ''
|
|
6
|
+
const secure = REDIS_URL ? /rediss:/.test(REDIS_URL) : false
|
|
7
|
+
Logger.verbose(`Redis URL: ${REDIS_URL}`)
|
|
8
|
+
|
|
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
|
+
: {}
|
|
20
|
+
|
|
21
|
+
const dateReviver = (_key: unknown, value: string | Date): Date => {
|
|
22
|
+
const isISO8601Z = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/
|
|
23
|
+
if (typeof value === 'string' && isISO8601Z.test(value)) {
|
|
24
|
+
const tempDateNumber = Date.parse(value)
|
|
25
|
+
if (!isNaN(tempDateNumber)) {
|
|
26
|
+
return new Date(tempDateNumber)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== 'string') return value
|
|
30
|
+
return new Date(value)
|
|
31
|
+
}
|
|
32
|
+
|
|
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
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Field, InputType } from '@nestjs/graphql'
|
|
2
|
+
import { GraphQLJSONObject } from 'graphql-type-json'
|
|
3
|
+
|
|
4
|
+
@InputType()
|
|
5
|
+
export class CorePagingInput {
|
|
6
|
+
@Field({ nullable: true, defaultValue: 20 })
|
|
7
|
+
take?: number
|
|
8
|
+
|
|
9
|
+
@Field({ nullable: true, defaultValue: 0 })
|
|
10
|
+
skip?: number
|
|
11
|
+
|
|
12
|
+
@Field({ nullable: true })
|
|
13
|
+
search?: string
|
|
14
|
+
|
|
15
|
+
@Field(() => [String], { nullable: true })
|
|
16
|
+
searchFields?: string[]
|
|
17
|
+
|
|
18
|
+
@Field({ nullable: true })
|
|
19
|
+
orderDirection?: 'asc' | 'desc'
|
|
20
|
+
|
|
21
|
+
@Field({ nullable: true })
|
|
22
|
+
orderBy?: string
|
|
23
|
+
|
|
24
|
+
@Field(() => GraphQLJSONObject, { nullable: true })
|
|
25
|
+
filters?: Record<string, unknown>
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Field, ObjectType } from '@nestjs/graphql'
|
|
2
|
+
|
|
3
|
+
@ObjectType()
|
|
4
|
+
export class CorePaging {
|
|
5
|
+
@Field({ nullable: true })
|
|
6
|
+
total?: number
|
|
7
|
+
|
|
8
|
+
@Field({ nullable: true })
|
|
9
|
+
count?: number
|
|
10
|
+
|
|
11
|
+
@Field({ nullable: true })
|
|
12
|
+
take?: number
|
|
13
|
+
|
|
14
|
+
@Field({ nullable: true })
|
|
15
|
+
page?: number
|
|
16
|
+
|
|
17
|
+
@Field({ nullable: true })
|
|
18
|
+
skip?: number
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common'
|
|
2
|
+
import { ApiCoreFeatureService } from './api-core-feature.service'
|
|
3
|
+
|
|
4
|
+
@Controller()
|
|
5
|
+
export class ApiCoreFeatureController {
|
|
6
|
+
constructor(private readonly service: ApiCoreFeatureService) {}
|
|
7
|
+
|
|
8
|
+
@Get('uptime')
|
|
9
|
+
uptime() {
|
|
10
|
+
return this.service.uptime()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'
|
|
2
|
+
import { Module } from '@nestjs/common'
|
|
3
|
+
import { ConfigModule } from '@nestjs/config'
|
|
4
|
+
import { GraphQLModule } from '@nestjs/graphql'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { Request, Response } from 'express'
|
|
7
|
+
import { apiCorePubSub } from '@<%= npmScope %>/api/core/data-access'
|
|
8
|
+
import { configuration, validationSchema } from '@<%= npmScope %>/api/config'
|
|
9
|
+
import { Context } from 'graphql-ws'
|
|
10
|
+
import { ApiCoreFeatureController } from './api-core-feature.controller'
|
|
11
|
+
import { ApiCoreFeatureResolver } from './api-core-feature.resolver'
|
|
12
|
+
import { ApiCoreFeatureService } from './api-core-feature.service'
|
|
13
|
+
import { ComplexityPlugin } from './plugins/complexity.plugin'
|
|
14
|
+
|
|
15
|
+
interface ConnectionParameters {
|
|
16
|
+
headers?: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const redisPubSubProvider = {
|
|
20
|
+
provide: 'REDIS_PUB_SUB',
|
|
21
|
+
useValue: apiCorePubSub,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Module({
|
|
25
|
+
imports: [
|
|
26
|
+
ConfigModule.forRoot({
|
|
27
|
+
isGlobal: true,
|
|
28
|
+
load: [configuration],
|
|
29
|
+
validationSchema,
|
|
30
|
+
}),
|
|
31
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
32
|
+
driver: ApolloDriver,
|
|
33
|
+
autoSchemaFile: join(process.cwd(), 'api-schema.graphql'),
|
|
34
|
+
subscriptions: {
|
|
35
|
+
'graphql-ws': {
|
|
36
|
+
onConnect: async (context: Context<Record<string, unknown> | undefined>) => {
|
|
37
|
+
const { extra } = context
|
|
38
|
+
if (extra && typeof extra === 'object' && 'request' in extra && extra.request && typeof extra.request === 'object' && 'rawHeaders' in extra.request) {
|
|
39
|
+
const rawHeaders = extra.request.rawHeaders as string[] | undefined
|
|
40
|
+
let token = ''
|
|
41
|
+
if (rawHeaders) {
|
|
42
|
+
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
43
|
+
if (rawHeaders[i].toLowerCase() === 'cookie') {
|
|
44
|
+
const cookies = rawHeaders[i + 1].split(';')
|
|
45
|
+
for (const cookie of cookies) {
|
|
46
|
+
const [name, value] = cookie.trim().split('=')
|
|
47
|
+
if (name === '__session') {
|
|
48
|
+
token = value
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (token === '') {
|
|
58
|
+
throw new Error('Authentication token is missing')
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error('Authentication token is missing')
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
context: ({ req, res, connectionParams }: { req: Partial<Request>; res: Response; connectionParams: ConnectionParameters }) => {
|
|
68
|
+
if (connectionParams) {
|
|
69
|
+
req = { headers: connectionParams.headers }
|
|
70
|
+
}
|
|
71
|
+
return { req, res }
|
|
72
|
+
},
|
|
73
|
+
sortSchema: true,
|
|
74
|
+
buildSchemaOptions: {
|
|
75
|
+
dateScalarMode: 'isoDate', // Better interoperability (ISO strings, not JS Dates)
|
|
76
|
+
numberScalarMode: 'float', // Default is fine; override to 'integer' if you hate floats
|
|
77
|
+
scalarsMap: [], // Optional for custom scalar mappings if you use them
|
|
78
|
+
skipCheck: true, // Skip extra metadata validation — speeds up build
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
controllers: [ApiCoreFeatureController],
|
|
83
|
+
providers: [ApiCoreFeatureResolver, ApiCoreFeatureService, ComplexityPlugin, redisPubSubProvider],
|
|
84
|
+
exports: [ApiCoreFeatureService, ComplexityPlugin, 'REDIS_PUB_SUB'],
|
|
85
|
+
})
|
|
86
|
+
export class ApiCoreFeatureModule {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Float, Query, Resolver } from '@nestjs/graphql'
|
|
2
|
+
import { ApiCoreFeatureService } from './api-core-feature.service'
|
|
3
|
+
|
|
4
|
+
@Resolver()
|
|
5
|
+
export class ApiCoreFeatureResolver {
|
|
6
|
+
constructor(private readonly service: ApiCoreFeatureService) {}
|
|
7
|
+
|
|
8
|
+
@Query(() => Float, { nullable: true })
|
|
9
|
+
uptime() {
|
|
10
|
+
return this.service.uptime()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common'
|
|
2
|
+
import { ConfigService } from '@nestjs/config'
|
|
3
|
+
import { CookieOptions } from 'express'
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class ApiCoreFeatureService {
|
|
7
|
+
constructor(public readonly config: ConfigService) {}
|
|
8
|
+
|
|
9
|
+
uptime(): number {
|
|
10
|
+
return process.uptime()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get apiUrl(): string {
|
|
14
|
+
return this.config.getOrThrow<string>('apiUrl')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get apiCorsOrigins(): string[] {
|
|
18
|
+
return this.config.get<string[]>('api.cors.origin') ?? []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get cookie(): { name: string; options: CookieOptions } {
|
|
22
|
+
return this.config.getOrThrow<{ name: string; options: CookieOptions }>('api.cookie')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get appEmail(): string {
|
|
26
|
+
return this.config.getOrThrow<string>('app.email')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get appSupportEmail(): string {
|
|
30
|
+
return this.config.getOrThrow<string>('app.supportEmail')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get appAdminEmails(): string {
|
|
34
|
+
return this.config.getOrThrow<string>('app.adminEmails')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get appName(): string {
|
|
38
|
+
return this.config.getOrThrow<string>('app.name')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get siteUrl(): string {
|
|
42
|
+
return this.config.getOrThrow<string>('siteUrl')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get mailerConfig() {
|
|
46
|
+
return {
|
|
47
|
+
host: this.config.getOrThrow<string>('smtp.host'),
|
|
48
|
+
port: this.config.getOrThrow<string>('smtp.port'),
|
|
49
|
+
auth: {
|
|
50
|
+
user: this.config.getOrThrow<string>('smtp.user'),
|
|
51
|
+
pass: this.config.getOrThrow<string>('smtp.pass'),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const configuration = () => ({
|
|
2
|
+
prefix: 'api',
|
|
3
|
+
environment: process.env['NODE_ENV'],
|
|
4
|
+
host: process.env['HOST'],
|
|
5
|
+
port: parseInt(process.env['PORT'] ?? '3000', 10),
|
|
6
|
+
apiUrl: process.env['API_URL'],
|
|
7
|
+
api: {
|
|
8
|
+
cookie: {
|
|
9
|
+
name: process.env['API_COOKIE_NAME'],
|
|
10
|
+
options: {
|
|
11
|
+
domain: process.env['API_COOKIE_DOMAIN'],
|
|
12
|
+
httpOnly: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
cors: {
|
|
16
|
+
origin: [process.env['WEB_URL']],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
siteUrl: process.env['SITE_URL'] || process.env['API_URL']?.replace('/api', ''),
|
|
20
|
+
app: {
|
|
21
|
+
email: process.env['APP_EMAIL'],
|
|
22
|
+
supportEmail: process.env['APP_SUPPORT_EMAIL'],
|
|
23
|
+
adminEmails: process.env['APP_ADMIN_EMAILS'],
|
|
24
|
+
name: process.env['APP_NAME'],
|
|
25
|
+
},
|
|
26
|
+
smtp: {
|
|
27
|
+
host: process.env['SMTP_HOST'],
|
|
28
|
+
port: process.env['SMTP_PORT'],
|
|
29
|
+
user: process.env['SMTP_USER'],
|
|
30
|
+
pass: process.env['SMTP_PASS'],
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as Joi from 'joi'
|
|
2
|
+
|
|
3
|
+
export const validationSchema = Joi.object({
|
|
4
|
+
NODE_ENV: Joi.string().valid('development', 'production', 'test'),
|
|
5
|
+
HOST: Joi.string().alphanum().default('localhost'),
|
|
6
|
+
PORT: Joi.number().default(3000),
|
|
7
|
+
WEB_PORT: Joi.number().default(4200),
|
|
8
|
+
WEB_URL: Joi.string().default(
|
|
9
|
+
`http://${process.env['HOST'] || 'localhost'}:${process.env['WEB_PORT']}`
|
|
10
|
+
),
|
|
11
|
+
API_COOKIE_DOMAIN: Joi.string().default('localhost'),
|
|
12
|
+
API_COOKIE_NAME: Joi.string().default('__session'),
|
|
13
|
+
API_URL: Joi.string().default(
|
|
14
|
+
`http://${process.env['HOST'] || 'localhost'}:${process.env['PORT']}/api`
|
|
15
|
+
),
|
|
16
|
+
APP_NAME: Joi.string().required(),
|
|
17
|
+
APP_EMAIL: Joi.string().email().required(),
|
|
18
|
+
APP_SUPPORT_EMAIL: Joi.string().email().required(),
|
|
19
|
+
APP_ADMIN_EMAILS: Joi.string().required(),
|
|
20
|
+
SITE_URL: Joi.string().uri().required(),
|
|
21
|
+
SMTP_HOST: Joi.string().required(),
|
|
22
|
+
SMTP_PORT: Joi.string().required(),
|
|
23
|
+
SMTP_USER: Joi.string().required(),
|
|
24
|
+
SMTP_PASS: Joi.string().required(),
|
|
25
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { GraphQLSchemaHost } from '@nestjs/graphql'
|
|
2
|
+
import { Plugin } from '@nestjs/apollo'
|
|
3
|
+
import { ApolloServerPlugin, GraphQLRequestListener } from '@apollo/server'
|
|
4
|
+
import { GraphQLError } from 'graphql'
|
|
5
|
+
import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from 'graphql-query-complexity'
|
|
6
|
+
import { Logger } from '@nestjs/common'
|
|
7
|
+
|
|
8
|
+
@Plugin()
|
|
9
|
+
export class ComplexityPlugin implements ApolloServerPlugin {
|
|
10
|
+
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
|
|
11
|
+
|
|
12
|
+
async requestDidStart(): Promise<void | GraphQLRequestListener<any>> {
|
|
13
|
+
const maxComplexity = process.env['QUERY_COMPLEXITY_LIMIT'] ? parseInt(process.env['QUERY_COMPLEXITY_LIMIT']) : 15000
|
|
14
|
+
const logQueryComplexity = process.env['LOG_QUERY_COMPLEXITY'] ? process.env['LOG_QUERY_COMPLEXITY'] === 'true' : false
|
|
15
|
+
const logQueryComplexityThreshold = process.env['LOG_QUERY_COMPLEXITY_THRESHOLD']
|
|
16
|
+
? parseInt(process.env['LOG_QUERY_COMPLEXITY_THRESHOLD'])
|
|
17
|
+
: 5000
|
|
18
|
+
const queryComplexityVerboseErrors = process.env['QUERY_COMPLEXITY_VERBOSE_ERRORS']
|
|
19
|
+
? process.env['QUERY_COMPLEXITY_VERBOSE_ERRORS'] === 'true'
|
|
20
|
+
: false
|
|
21
|
+
|
|
22
|
+
const { schema } = this.gqlSchemaHost
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
didResolveOperation: async ({ request, document }): Promise<void> => {
|
|
26
|
+
const complexity = getComplexity({
|
|
27
|
+
schema,
|
|
28
|
+
operationName: request.operationName,
|
|
29
|
+
query: document,
|
|
30
|
+
variables: request.variables,
|
|
31
|
+
estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (logQueryComplexity && complexity > logQueryComplexityThreshold) {
|
|
35
|
+
Logger.log(`Query Complexity for "${request.operationName}": ${complexity}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (complexity > maxComplexity) {
|
|
39
|
+
let message = `Query "${request.operationName}" is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}.`
|
|
40
|
+
if (queryComplexityVerboseErrors) {
|
|
41
|
+
message = `${message}\n\nVariables:\n${JSON.stringify(request.variables)}\n\nFull Query:\n${request.query}`
|
|
42
|
+
}
|
|
43
|
+
Logger.error(message)
|
|
44
|
+
throw new GraphQLError(message)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Promise.resolve()
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from '@nestjs/apollo'
|
|
2
|
+
import { ApolloServerPlugin, GraphQLRequestListener } from '@apollo/server'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This is a general logging plugin that is a placeholder for future plugin use.
|
|
6
|
+
*/
|
|
7
|
+
@Plugin()
|
|
8
|
+
export class LoggingPlugin implements ApolloServerPlugin {
|
|
9
|
+
async requestDidStart(): Promise<GraphQLRequestListener<any>> {
|
|
10
|
+
console.log('Request started')
|
|
11
|
+
return {
|
|
12
|
+
async willSendResponse(): Promise<void> {
|
|
13
|
+
console.log('Will send response')
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/models'
|