updates
This commit is contained in:
parent
faa4aa48eb
commit
414bc04782
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
## License
|
||||
(The MIT License)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -6,6 +6,10 @@ services:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- "./data/mongo:/data"
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
# api:
|
||||
# build: .
|
||||
# volumes:
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
@ -40,9 +39,12 @@
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.2",
|
||||
"graphql": "15.0.0",
|
||||
"graphql-redis-subscriptions": "^2.2.1",
|
||||
"graphql-subscriptions": "^1.1.0",
|
||||
"graphql-tools": "^5.0.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"inquirer": "^7.1.0",
|
||||
"ioredis": "^4.17.1",
|
||||
"migrate-mongoose": "^4.0.0",
|
||||
"mongoose": "^5.9.11",
|
||||
"nestjs-console": "^3.0.2",
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { HttpModule, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { GraphQLFederationModule } from '@nestjs/graphql';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { MongooseModuleOptions } from '@nestjs/mongoose/dist/interfaces/mongoose-options.interface';
|
||||
import crypto from 'crypto';
|
||||
import { Request } from 'express-serve-static-core';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { ConsoleModule } from 'nestjs-console';
|
||||
import { LoggerModule, Params as LoggerModuleParams } from 'nestjs-pino/dist';
|
||||
import { join } from 'path';
|
||||
import { ContextCache } from './resolver/context.cache';
|
||||
import { schema } from './schema';
|
||||
|
||||
export const LoggerConfig: LoggerModuleParams = {
|
||||
@ -60,14 +61,14 @@ export const imports = [
|
||||
})
|
||||
}),
|
||||
LoggerModule.forRoot(LoggerConfig),
|
||||
GraphQLFederationModule.forRoot({
|
||||
GraphQLModule.forRoot({
|
||||
debug: process.env.NODE_ENV !== 'production',
|
||||
definitions: {
|
||||
outputAs: 'class',
|
||||
},
|
||||
introspection: true,
|
||||
playground: true,
|
||||
// installSubscriptionHandlers: true,
|
||||
installSubscriptionHandlers: true,
|
||||
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
|
||||
// to allow guards on resolver props https://github.com/nestjs/graphql/issues/295
|
||||
fieldResolverEnhancers: [
|
||||
@ -77,11 +78,22 @@ export const imports = [
|
||||
resolverValidationOptions: {
|
||||
|
||||
},
|
||||
context: ({ req }) => {
|
||||
return {
|
||||
cache: new ContextCache(),
|
||||
req,
|
||||
context: ({ req, connection }) => {
|
||||
if (!req && connection) {
|
||||
const headers: IncomingHttpHeaders = {}
|
||||
|
||||
Object.keys(connection.context).forEach(key => {
|
||||
headers[key.toLowerCase()] = connection.context[key]
|
||||
})
|
||||
|
||||
return {
|
||||
req: {
|
||||
headers
|
||||
} as Request
|
||||
}
|
||||
}
|
||||
|
||||
return { req }
|
||||
},
|
||||
}),
|
||||
MongooseModule.forRootAsync({
|
||||
|
||||
@ -3,10 +3,10 @@ export const fieldTypes = [
|
||||
'textfield',
|
||||
'date',
|
||||
'email',
|
||||
'legal',
|
||||
// 'legal',
|
||||
'textarea',
|
||||
'link',
|
||||
'statement',
|
||||
// 'statement',
|
||||
'dropdown',
|
||||
'rating',
|
||||
'radio',
|
||||
|
||||
13
src/dto/form/abstract.notification.input.ts
Normal file
13
src/dto/form/abstract.notification.input.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('NotificationInput', { isAbstract: true })
|
||||
export class AbstractNotificationInput {
|
||||
@Field({ nullable: true })
|
||||
readonly subject?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly htmlTemplate?: string
|
||||
|
||||
@Field()
|
||||
readonly enabled: boolean
|
||||
}
|
||||
19
src/dto/form/button.input.ts
Normal file
19
src/dto/form/button.input.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('ButtonInput')
|
||||
export class ButtonInput {
|
||||
@Field({ nullable: true })
|
||||
readonly url?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly action?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly text?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly bgColor?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly color?: string
|
||||
}
|
||||
@ -9,7 +9,7 @@ export class ButtonModel {
|
||||
readonly action?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly text: string
|
||||
readonly text?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly bgColor?: string
|
||||
|
||||
19
src/dto/form/colors.input.ts
Normal file
19
src/dto/form/colors.input.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('ColorsInput')
|
||||
export class ColorsInput {
|
||||
@Field()
|
||||
readonly backgroundColor: string
|
||||
|
||||
@Field()
|
||||
readonly questionColor: string
|
||||
|
||||
@Field()
|
||||
readonly answerColor: string
|
||||
|
||||
@Field()
|
||||
readonly buttonColor: string
|
||||
|
||||
@Field()
|
||||
readonly buttonTextColor: string
|
||||
}
|
||||
11
src/dto/form/design.input.ts
Normal file
11
src/dto/form/design.input.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { ColorsInput } from './colors.input';
|
||||
|
||||
@InputType('DesignInput')
|
||||
export class DesignInput {
|
||||
@Field()
|
||||
readonly colors: ColorsInput
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly font?: string
|
||||
}
|
||||
23
src/dto/form/form.create.input.ts
Normal file
23
src/dto/form/form.create.input.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
import { FormFieldInput } from './form.field.input';
|
||||
|
||||
@InputType('FormCreateInput')
|
||||
export class FormCreateInput {
|
||||
@Field(() => ID, { nullable: true })
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
readonly title: string
|
||||
|
||||
@Field()
|
||||
readonly language: string
|
||||
|
||||
@Field()
|
||||
readonly showFooter: boolean
|
||||
|
||||
@Field()
|
||||
readonly isLive: boolean
|
||||
|
||||
@Field(() => [FormFieldInput], { nullable: true })
|
||||
readonly fields: FormFieldInput[]
|
||||
}
|
||||
22
src/dto/form/form.field.input.ts
Normal file
22
src/dto/form/form.field.input.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('FormFieldInput')
|
||||
export class FormFieldInput {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
readonly title: string
|
||||
|
||||
@Field()
|
||||
readonly type: string
|
||||
|
||||
@Field()
|
||||
readonly description: string
|
||||
|
||||
@Field()
|
||||
readonly required: boolean
|
||||
|
||||
@Field()
|
||||
readonly value: string
|
||||
}
|
||||
32
src/dto/form/form.field.model.ts
Normal file
32
src/dto/form/form.field.model.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { FormFieldDocument } from '../../schema/form.field.schema';
|
||||
|
||||
@ObjectType('FormField')
|
||||
export class FormFieldModel {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
readonly title: string
|
||||
|
||||
@Field()
|
||||
readonly type: string
|
||||
|
||||
@Field()
|
||||
readonly description: string
|
||||
|
||||
@Field()
|
||||
readonly required: boolean
|
||||
|
||||
@Field()
|
||||
readonly value: string
|
||||
|
||||
constructor(document: FormFieldDocument) {
|
||||
this.id = document.id
|
||||
this.title = document.title
|
||||
this.type = document.type
|
||||
this.description = document.description
|
||||
this.required = document.required
|
||||
this.value = document.value
|
||||
}
|
||||
}
|
||||
@ -21,12 +21,12 @@ export class FormModel {
|
||||
@Field()
|
||||
readonly showFooter: boolean
|
||||
|
||||
constructor(partial: Partial<FormDocument>) {
|
||||
this.id = partial.id
|
||||
this.title = partial.title
|
||||
this.created = partial.created
|
||||
this.lastModified = partial.lastModified
|
||||
this.language = partial.language
|
||||
this.showFooter = partial.showFooter
|
||||
constructor(form: FormDocument) {
|
||||
this.id = form.id
|
||||
this.title = form.title
|
||||
this.created = form.created
|
||||
this.lastModified = form.lastModified
|
||||
this.language = form.language
|
||||
this.showFooter = form.showFooter
|
||||
}
|
||||
}
|
||||
|
||||
42
src/dto/form/form.update.input.ts
Normal file
42
src/dto/form/form.update.input.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
import { DesignInput } from './design.input';
|
||||
import { FormFieldInput } from './form.field.input';
|
||||
import { PageInput } from './page.input';
|
||||
import { RespondentNotificationsInput } from './respondent.notifications.input';
|
||||
import { SelfNotificationsInput } from './self.notifications.input';
|
||||
|
||||
@InputType('FormUpdateInput')
|
||||
export class FormUpdateInput {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly title: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly language: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly showFooter: boolean
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly isLive: boolean
|
||||
|
||||
@Field(() => [FormFieldInput], { nullable: true })
|
||||
readonly fields: FormFieldInput[]
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly design: DesignInput
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly startPage: PageInput
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly endPage: PageInput
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly selfNotifications: SelfNotificationsInput
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly respondentNotifications: RespondentNotificationsInput
|
||||
}
|
||||
20
src/dto/form/page.input.ts
Normal file
20
src/dto/form/page.input.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { ButtonInput } from './button.input';
|
||||
|
||||
@InputType('PageInput')
|
||||
export class PageInput {
|
||||
@Field()
|
||||
readonly show: boolean
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly title?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly paragraph?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly buttonText?: string
|
||||
|
||||
@Field(() => [ButtonInput])
|
||||
readonly buttons: ButtonInput[]
|
||||
}
|
||||
@ -2,8 +2,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { FormPage } from '../../schema/form.schema';
|
||||
import { ButtonModel } from './button.model';
|
||||
|
||||
@ObjectType('FormPage')
|
||||
export class FormPageModel {
|
||||
@ObjectType('Page')
|
||||
export class PageModel {
|
||||
@Field()
|
||||
readonly show: boolean
|
||||
|
||||
25
src/dto/form/pager.form.model.ts
Normal file
25
src/dto/form/pager.form.model.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { GraphQLInt } from 'graphql';
|
||||
import { FormModel } from './form.model';
|
||||
|
||||
@ObjectType('PagerForm')
|
||||
export class PagerFormModel {
|
||||
@Field(() => [FormModel])
|
||||
entries: FormModel[]
|
||||
|
||||
@Field(() => GraphQLInt)
|
||||
total: number
|
||||
|
||||
@Field(() => GraphQLInt)
|
||||
limit: number
|
||||
|
||||
@Field(() => GraphQLInt)
|
||||
start: number
|
||||
|
||||
constructor(entries: FormModel[], total: number, limit: number, start: number) {
|
||||
this.entries = entries
|
||||
this.total = total
|
||||
this.limit = limit
|
||||
this.start = start
|
||||
}
|
||||
}
|
||||
11
src/dto/form/respondent.notifications.input.ts
Normal file
11
src/dto/form/respondent.notifications.input.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { AbstractNotificationInput } from './abstract.notification.input';
|
||||
|
||||
@InputType()
|
||||
export class RespondentNotificationsInput extends AbstractNotificationInput {
|
||||
@Field({ nullable: true })
|
||||
readonly toField?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly fromEmail?: string
|
||||
}
|
||||
11
src/dto/form/self.notifications.input.ts
Normal file
11
src/dto/form/self.notifications.input.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { AbstractNotificationInput } from './abstract.notification.input';
|
||||
|
||||
@InputType()
|
||||
export class SelfNotificationsInput extends AbstractNotificationInput {
|
||||
@Field({ nullable: true })
|
||||
readonly fromField?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly toEmail?: string
|
||||
}
|
||||
@ -1,8 +1,15 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { UserModel } from './user.model';
|
||||
|
||||
@ObjectType('OwnUser')
|
||||
export class OwnUserModel extends UserModel {
|
||||
@Field(() => [String])
|
||||
readonly roles: string[]
|
||||
|
||||
constructor(user: UserDocument) {
|
||||
super(user)
|
||||
|
||||
this.roles = user.roles
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ export class UserModel {
|
||||
@Field()
|
||||
readonly lastName?: string
|
||||
|
||||
constructor(user: Partial<UserDocument>) {
|
||||
constructor(user: UserDocument) {
|
||||
this.id = user.id
|
||||
this.username = user.username
|
||||
this.email = user.email
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ContextCache } from '../resolver/context.cache';
|
||||
|
||||
@Injectable()
|
||||
export class GqlAuthGuard extends AuthGuard('jwt') {
|
||||
getRequest(context: ExecutionContext) {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
if (!ctx.getContext().cache) {
|
||||
ctx.getContext().cache = {
|
||||
// add(type, id, object) =>
|
||||
}
|
||||
ctx.getContext().cache = new ContextCache()
|
||||
}
|
||||
return ctx.getContext().req;
|
||||
}
|
||||
|
||||
@ -19,5 +19,5 @@ import { AppModule } from './app.module';
|
||||
app.enableCors({origin: '*'})
|
||||
app.getHttpAdapter().options('*', cors())
|
||||
|
||||
await app.listen(process.env.PORT || 3000);
|
||||
await app.listen(process.env.PORT || 4100);
|
||||
})()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Args, Mutation } from '@nestjs/graphql';
|
||||
import { PinoLogger } from 'nestjs-pino/dist';
|
||||
import { AuthJwtModel } from '../../dto/auth/auth.jwt.model';
|
||||
import { UserCreateInput } from '../../dto/user/user.create.input';
|
||||
import { AuthService } from '../../service/auth/auth.service';
|
||||
@ -10,6 +11,7 @@ export class AuthRegisterResolver {
|
||||
constructor(
|
||||
private readonly createUser: UserCreateService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly logger: PinoLogger,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -17,6 +19,10 @@ export class AuthRegisterResolver {
|
||||
async authRegister(
|
||||
@Args({ name: 'user' }) data: UserCreateInput,
|
||||
): Promise<AuthJwtModel> {
|
||||
this.logger.info({
|
||||
email: data.email,
|
||||
username: data.username,
|
||||
}, 'try to register new user')
|
||||
const user = await this.createUser.create(data)
|
||||
|
||||
return this.auth.login(user)
|
||||
|
||||
31
src/resolver/form/form.create.mutation.ts
Normal file
31
src/resolver/form/form.create.mutation.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Args, Context, Mutation } from '@nestjs/graphql';
|
||||
import { Roles } from '../../decorator/roles.decorator';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { FormCreateInput } from '../../dto/form/form.create.input';
|
||||
import { FormModel } from '../../dto/form/form.model';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { FormCreateService } from '../../service/form/form.create.service';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Injectable()
|
||||
export class FormCreateMutation {
|
||||
constructor(
|
||||
private readonly createService: FormCreateService
|
||||
) {
|
||||
}
|
||||
|
||||
@Mutation(() => FormModel)
|
||||
@Roles('admin')
|
||||
async createForm(
|
||||
@User() user: UserDocument,
|
||||
@Args({ name: 'form', type: () => FormCreateInput }) input: FormCreateInput,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<FormModel> {
|
||||
const form = await this.createService.create(user, input)
|
||||
|
||||
cache.addForm(form)
|
||||
|
||||
return new FormModel(form)
|
||||
}
|
||||
}
|
||||
34
src/resolver/form/form.delete.mutation.ts
Normal file
34
src/resolver/form/form.delete.mutation.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Args, ID, Mutation } from '@nestjs/graphql';
|
||||
import { Roles } from '../../decorator/roles.decorator';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { FormModel } from '../../dto/form/form.model';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { FormDeleteService } from '../../service/form/form.delete.service';
|
||||
import { FormService } from '../../service/form/form.service';
|
||||
|
||||
@Injectable()
|
||||
export class FormDeleteMutation {
|
||||
constructor(
|
||||
private readonly deleteService: FormDeleteService,
|
||||
private readonly formService: FormService,
|
||||
) {
|
||||
}
|
||||
|
||||
@Mutation(() => FormModel)
|
||||
@Roles('admin')
|
||||
async deleteForm(
|
||||
@User() user: UserDocument,
|
||||
@Args({ name: 'id', type: () => ID}) id: string,
|
||||
) {
|
||||
const form = await this.formService.findById(id)
|
||||
|
||||
if (!form.isLive && !await this.formService.isAdmin(form, user)) {
|
||||
throw new Error('invalid form')
|
||||
}
|
||||
|
||||
await this.deleteService.delete(id)
|
||||
|
||||
return new FormModel(form)
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,9 @@ import { Args, Context, ID, Parent, Query, ResolveField, Resolver } from '@nestj
|
||||
import { Roles } from '../../decorator/roles.decorator';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { DesignModel } from '../../dto/form/design.model';
|
||||
import { FormFieldModel } from '../../dto/form/form.field.model';
|
||||
import { FormModel } from '../../dto/form/form.model';
|
||||
import { FormPageModel } from '../../dto/form/form.page.model';
|
||||
import { PageModel } from '../../dto/form/page.model';
|
||||
import { RespondentNotificationsModel } from '../../dto/form/respondent.notifications.model';
|
||||
import { SelfNotificationsModel } from '../../dto/form/self.notifications.model';
|
||||
import { UserModel } from '../../dto/user/user.model';
|
||||
@ -35,6 +36,17 @@ export class FormResolver {
|
||||
return new FormModel(form)
|
||||
}
|
||||
|
||||
@ResolveField('fields', () => [FormFieldModel])
|
||||
async getFields(
|
||||
@User() user: UserDocument,
|
||||
@Parent() parent: FormModel,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<FormFieldModel[]> {
|
||||
const form = await cache.getForm(parent.id)
|
||||
|
||||
return form.fields.map(field => new FormFieldModel(field))
|
||||
}
|
||||
|
||||
@ResolveField('isLive', () => Boolean)
|
||||
@Roles('admin')
|
||||
async getRoles(
|
||||
@ -67,13 +79,13 @@ export class FormResolver {
|
||||
return new SelfNotificationsModel(form.selfNotifications)
|
||||
}
|
||||
|
||||
@ResolveField('respondentNotifications', () => SelfNotificationsModel)
|
||||
@ResolveField('respondentNotifications', () => RespondentNotificationsModel)
|
||||
@Roles('admin')
|
||||
async getRespondentNotifications(
|
||||
@User() user: UserDocument,
|
||||
@Parent() parent: FormModel,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<SelfNotificationsModel> {
|
||||
): Promise<RespondentNotificationsModel> {
|
||||
const form = await cache.getForm(parent.id)
|
||||
|
||||
if (!await this.formService.isAdmin(form, user)) {
|
||||
@ -94,24 +106,24 @@ export class FormResolver {
|
||||
return new DesignModel(form.design)
|
||||
}
|
||||
|
||||
@ResolveField('startPage', () => FormPageModel)
|
||||
@ResolveField('startPage', () => PageModel)
|
||||
async getStartPage(
|
||||
@Parent() parent: FormModel,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<FormPageModel> {
|
||||
): Promise<PageModel> {
|
||||
const form = await cache.getForm(parent.id)
|
||||
|
||||
return new FormPageModel(form.startPage)
|
||||
return new PageModel(form.startPage)
|
||||
}
|
||||
|
||||
@ResolveField('endPage', () => FormPageModel)
|
||||
@ResolveField('endPage', () => PageModel)
|
||||
async getEndPage(
|
||||
@Parent() parent: FormModel,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<FormPageModel> {
|
||||
): Promise<PageModel> {
|
||||
const form = await cache.getForm(parent.id)
|
||||
|
||||
return new FormPageModel(form.endPage)
|
||||
return new PageModel(form.endPage)
|
||||
}
|
||||
|
||||
@ResolveField('admin', () => UserModel)
|
||||
|
||||
35
src/resolver/form/form.search.resolver.ts
Normal file
35
src/resolver/form/form.search.resolver.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Args, Context, Query, Resolver } from '@nestjs/graphql';
|
||||
import { GraphQLInt } from 'graphql';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { FormModel } from '../../dto/form/form.model';
|
||||
import { PagerFormModel } from '../../dto/form/pager.form.model';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { FormService } from '../../service/form/form.service';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Resolver(() => PagerFormModel)
|
||||
export class FormSearchResolver {
|
||||
constructor(
|
||||
private readonly formService: FormService,
|
||||
) {
|
||||
}
|
||||
|
||||
@Query(() => PagerFormModel)
|
||||
async listForms(
|
||||
@User() user: UserDocument,
|
||||
@Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start,
|
||||
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit,
|
||||
@Context('cache') cache: ContextCache,
|
||||
) {
|
||||
const [forms, total] = await this.formService.find(user, start, limit)
|
||||
|
||||
forms.forEach(form => cache.addForm(form))
|
||||
|
||||
return new PagerFormModel(
|
||||
forms.map(form => new FormModel(form)),
|
||||
total,
|
||||
limit,
|
||||
start,
|
||||
)
|
||||
}
|
||||
}
|
||||
39
src/resolver/form/form.update.mutation.ts
Normal file
39
src/resolver/form/form.update.mutation.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Args, Context, Mutation } from '@nestjs/graphql';
|
||||
import { Roles } from '../../decorator/roles.decorator';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { FormModel } from '../../dto/form/form.model';
|
||||
import { FormUpdateInput } from '../../dto/form/form.update.input';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { FormService } from '../../service/form/form.service';
|
||||
import { FormUpdateService } from '../../service/form/form.update.service';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Injectable()
|
||||
export class FormUpdateMutation {
|
||||
constructor(
|
||||
private readonly updateService: FormUpdateService,
|
||||
private readonly formService: FormService,
|
||||
) {
|
||||
}
|
||||
|
||||
@Mutation(() => FormModel)
|
||||
@Roles('admin')
|
||||
async updateForm(
|
||||
@User() user: UserDocument,
|
||||
@Args({ name: 'form', type: () => FormUpdateInput }) input: FormUpdateInput,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<FormModel> {
|
||||
let form = await this.formService.findById(input.id)
|
||||
|
||||
if (!form.isLive && !await this.formService.isAdmin(form, user)) {
|
||||
throw new Error('invalid form')
|
||||
}
|
||||
|
||||
form = await this.updateService.update(form, input)
|
||||
|
||||
cache.addForm(form)
|
||||
|
||||
return new FormModel(form)
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
import { FieldResolver } from './field.resolver';
|
||||
import { FormCreateResolver } from './form.create.resolver';
|
||||
import { FormCreateMutation } from './form.create.mutation';
|
||||
import { FormDeleteMutation } from './form.delete.mutation';
|
||||
import { FormResolver } from './form.resolver';
|
||||
import { FormSearchResolver } from './form.search.resolver';
|
||||
import { FormUpdateMutation } from './form.update.mutation';
|
||||
|
||||
export const formResolvers = [
|
||||
FormResolver,
|
||||
FormCreateResolver,
|
||||
FieldResolver,
|
||||
FormSearchResolver,
|
||||
FormCreateMutation,
|
||||
FormDeleteMutation,
|
||||
FormUpdateMutation,
|
||||
]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Schema, SchemaDefinition } from 'mongoose';
|
||||
import { FieldSchemaName } from '../field.schema';
|
||||
import { FormFieldSchemaName } from '../form.field.schema';
|
||||
|
||||
export const LogicJump: SchemaDefinition = {
|
||||
expressionString: {
|
||||
@ -21,14 +21,14 @@ export const LogicJump: SchemaDefinition = {
|
||||
},
|
||||
fieldA: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: FieldSchemaName
|
||||
ref: FormFieldSchemaName
|
||||
},
|
||||
valueB: {
|
||||
type: String,
|
||||
},
|
||||
jumpTo: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: FieldSchemaName
|
||||
ref: FormFieldSchemaName
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
|
||||
@ -24,9 +24,4 @@ export const RatingField: SchemaDefinition = {
|
||||
'Trash',
|
||||
],
|
||||
},
|
||||
validShapes: {
|
||||
type: [{
|
||||
type: String,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,17 +4,21 @@ import { FieldOption } from './embedded/field.option';
|
||||
import { LogicJump } from './embedded/logic.jump';
|
||||
import { RatingField } from './embedded/rating.field';
|
||||
|
||||
export const FieldSchemaName = 'FormField'
|
||||
export const FormFieldSchemaName = 'FormField'
|
||||
|
||||
export interface FieldDocument extends Document {
|
||||
isSubmission: boolean
|
||||
export interface FormFieldDocument extends Document {
|
||||
title: string
|
||||
description: string
|
||||
logicJump: any
|
||||
rating: any
|
||||
options: any
|
||||
required: boolean
|
||||
disabled: boolean
|
||||
type: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export const FieldSchema = new Schema({
|
||||
isSubmission: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
export const FormFieldSchema = new Schema({
|
||||
title: {
|
||||
type: String,
|
||||
trim: true,
|
||||
@ -27,9 +31,11 @@ export const FieldSchema = new Schema({
|
||||
type: LogicJump,
|
||||
},
|
||||
ratingOptions: {
|
||||
alias: 'rating',
|
||||
type: RatingField,
|
||||
},
|
||||
fieldOptions: {
|
||||
alias: 'options',
|
||||
type: [FieldOption],
|
||||
},
|
||||
required: {
|
||||
@ -40,19 +46,20 @@ export const FieldSchema = new Schema({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
deletePreserved: { // TODO remove
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
validFieldTypes: { // TODO remove
|
||||
type: [String],
|
||||
},
|
||||
fieldType: {
|
||||
alias: 'type',
|
||||
type: String,
|
||||
enum: fieldTypes,
|
||||
},
|
||||
fieldValue: {
|
||||
alias: 'value',
|
||||
type: Schema.Types.Mixed,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
export const FormFieldDefinition = {
|
||||
name: FormFieldSchemaName,
|
||||
schema: FormFieldSchema,
|
||||
}
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import exp from 'constants';
|
||||
import { Document, Schema } from 'mongoose';
|
||||
import { matchType } from '../config/fields';
|
||||
import { defaultLanguage, languages } from '../config/languages';
|
||||
import { rolesType } from '../config/roles';
|
||||
import { ButtonDocument, ButtonSchema } from './button.schema';
|
||||
import { FieldDocument, FieldSchema } from './field.schema';
|
||||
import { VisitorDataDocument, VisitorDataSchema } from './visitor.data.schema';
|
||||
import { FormFieldDocument, FormFieldSchema } from './form.field.schema';
|
||||
import { UserDocument, UserSchemaName } from './user.schema';
|
||||
import { VisitorDataDocument, VisitorDataSchema } from './visitor.data.schema';
|
||||
|
||||
export const FormSchemaName = 'Form'
|
||||
|
||||
@ -58,7 +56,7 @@ export interface FormDocument extends Document {
|
||||
readonly visitors: [VisitorDataDocument]
|
||||
}
|
||||
|
||||
readonly fields: [FieldDocument]
|
||||
readonly fields: [FormFieldDocument]
|
||||
|
||||
readonly admin: UserDocument
|
||||
|
||||
@ -103,9 +101,10 @@ export const FormSchema = new Schema({
|
||||
type: [VisitorDataSchema],
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
alias: 'form_fields',
|
||||
type: [FieldSchema],
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
form_fields: {
|
||||
alias: 'fields',
|
||||
type: [FormFieldSchema],
|
||||
default: [],
|
||||
},
|
||||
admin: {
|
||||
@ -113,18 +112,18 @@ export const FormSchema = new Schema({
|
||||
ref: UserSchemaName,
|
||||
},
|
||||
startPage: {
|
||||
show: {
|
||||
alias: 'showStart',
|
||||
showStart: {
|
||||
alias: 'startPage.show',
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
alias: 'introTitle',
|
||||
introTitle: {
|
||||
alias: 'startPage.title',
|
||||
type: String,
|
||||
default: 'Welcome to Form',
|
||||
},
|
||||
paragraph: {
|
||||
alias: 'introParagraph',
|
||||
introParagraph: {
|
||||
alias: 'startPage.paragraph',
|
||||
type: String,
|
||||
default: 'Start',
|
||||
},
|
||||
@ -133,8 +132,8 @@ export const FormSchema = new Schema({
|
||||
},
|
||||
},
|
||||
endPage: {
|
||||
show: {
|
||||
alias: 'showEnd',
|
||||
showEnd: {
|
||||
alias: 'endPage.show',
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@ -157,8 +156,8 @@ export const FormSchema = new Schema({
|
||||
fromField: {
|
||||
type: String,
|
||||
},
|
||||
toEmail: {
|
||||
alias: 'toEmails',
|
||||
toEmails: {
|
||||
alias: 'selfNotifications.toEmail',
|
||||
type: String,
|
||||
},
|
||||
subject: {
|
||||
@ -176,8 +175,8 @@ export const FormSchema = new Schema({
|
||||
toField: {
|
||||
type: String,
|
||||
},
|
||||
fromEmail: {
|
||||
alias: 'fromEmails',
|
||||
fromEmails: {
|
||||
alias: 'respondentNotifications.fromEmail',
|
||||
type: String,
|
||||
match: matchType.email,
|
||||
},
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { FormFieldDefinition } from './form.field.schema';
|
||||
import { FormDefinition } from './form.schema';
|
||||
import { FormSubmissionDefinition, FormSubmissionSchema } from './form.submission.schema';
|
||||
import { SubmissionDefinition } from './submission.schema';
|
||||
import { UserDefinition } from './user.schema';
|
||||
|
||||
export const schema = [
|
||||
FormDefinition,
|
||||
FormSubmissionDefinition,
|
||||
FormFieldDefinition,
|
||||
SubmissionDefinition,
|
||||
UserDefinition,
|
||||
]
|
||||
|
||||
26
src/schema/submission.field.schema.ts
Normal file
26
src/schema/submission.field.schema.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Document, Schema } from 'mongoose';
|
||||
import { fieldTypes } from '../config/fields';
|
||||
import { FormFieldDocument, FormFieldSchemaName } from './form.field.schema';
|
||||
|
||||
export const SubmissionFieldSchemaName = 'SubmissionField'
|
||||
|
||||
export interface SubmissionFieldDocument extends Document {
|
||||
field: FormFieldDocument
|
||||
fieldType: string
|
||||
fieldValue: any
|
||||
}
|
||||
|
||||
export const SubmissionFormFieldSchema = new Schema({
|
||||
field: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: FormFieldSchemaName
|
||||
},
|
||||
fieldType: {
|
||||
type: String,
|
||||
enum: fieldTypes,
|
||||
},
|
||||
fieldValue: {
|
||||
type: Schema.Types.Mixed,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
@ -1,16 +1,17 @@
|
||||
import { Document, Schema } from 'mongoose';
|
||||
import { FieldSchema } from './field.schema';
|
||||
import { FormSchemaName } from './form.schema';
|
||||
import { SubmissionFieldDocument, SubmissionFieldSchemaName } from './submission.field.schema';
|
||||
|
||||
export const FormSubmissionSchemaName = 'FormSubmission'
|
||||
export const SubmissionSchemaName = 'FormSubmission'
|
||||
|
||||
export interface FormSubmissionDocument extends Document {
|
||||
export interface SubmissionDocument extends Document {
|
||||
fields: SubmissionFieldDocument[]
|
||||
}
|
||||
|
||||
export const FormSubmissionSchema = new Schema({
|
||||
export const SubmissionSchema = new Schema({
|
||||
fields: {
|
||||
alias: 'form_fields',
|
||||
type: [FieldSchema],
|
||||
type: [SubmissionFieldSchemaName],
|
||||
default: [],
|
||||
},
|
||||
form: {
|
||||
@ -50,8 +51,8 @@ export const FormSubmissionSchema = new Schema({
|
||||
}
|
||||
})
|
||||
|
||||
export const FormSubmissionDefinition = {
|
||||
name: FormSubmissionSchemaName,
|
||||
schema: FormSubmissionSchema,
|
||||
export const SubmissionDefinition = {
|
||||
name: SubmissionSchemaName,
|
||||
schema: SubmissionSchema,
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Document, Schema } from 'mongoose';
|
||||
import { defaultLanguage, languages } from '../config/languages';
|
||||
import { FieldDocument, FieldSchemaName } from './field.schema';
|
||||
import { FormFieldDocument, FormFieldSchemaName } from './form.field.schema';
|
||||
|
||||
export interface VisitorDataDocument extends Document {
|
||||
readonly introParagraph?: string
|
||||
readonly referrer?: string
|
||||
readonly filledOutFields: [FieldDocument]
|
||||
readonly filledOutFields: [FormFieldDocument]
|
||||
readonly timeElapsed: number
|
||||
readonly isSubmitted: boolean
|
||||
readonly language: string
|
||||
@ -24,7 +24,7 @@ export const VisitorDataSchema = new Schema({
|
||||
filledOutFields: {
|
||||
type: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: FieldSchemaName,
|
||||
ref: FormFieldSchemaName,
|
||||
}],
|
||||
},
|
||||
timeElapsed: {
|
||||
|
||||
@ -16,6 +16,8 @@ export class AuthService {
|
||||
async validateUser(username: string, password: string): Promise<UserDocument> {
|
||||
console.log('check user??', username)
|
||||
|
||||
// TODO only allow login for verified users!
|
||||
|
||||
const user = await this.userService.findByUsername(username);
|
||||
if (user && await this.passwordService.verify(password, user.passwordHash, user.salt)) {
|
||||
return user;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from "mongoose";
|
||||
import { Model } from 'mongoose';
|
||||
import { FormCreateInput } from '../../dto/form/form.create.input';
|
||||
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
|
||||
@Injectable()
|
||||
export class FormCreateService {
|
||||
@ -10,4 +12,7 @@ export class FormCreateService {
|
||||
) {
|
||||
}
|
||||
|
||||
async create(admin: UserDocument, input: FormCreateInput): Promise<FormDocument> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
17
src/service/form/form.delete.service.ts
Normal file
17
src/service/form/form.delete.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||
|
||||
@Injectable()
|
||||
export class FormDeleteService {
|
||||
constructor(
|
||||
@InjectModel(FormSchemaName) private formModel: Model<FormDocument>,
|
||||
) {
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
// TODO
|
||||
throw new Error('form.delete not yet implemented')
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { Model, Types } from 'mongoose';
|
||||
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
@ -20,6 +19,19 @@ export class FormService {
|
||||
return Types.ObjectId(form.admin.id).equals(Types.ObjectId(user.id))
|
||||
}
|
||||
|
||||
async find(user: UserDocument, start: number, limit: number, sort: any = {}): Promise<[FormDocument[], number]> {
|
||||
const qb = this.formModel.find()
|
||||
|
||||
// TODO apply restrictions based on user!
|
||||
|
||||
return [
|
||||
await qb.sort(sort)
|
||||
.skip(start)
|
||||
.limit(limit),
|
||||
await qb.count()
|
||||
]
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<FormDocument> {
|
||||
const form = await this.formModel.findById(id);
|
||||
|
||||
|
||||
94
src/service/form/form.update.service.ts
Normal file
94
src/service/form/form.update.service.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { FormUpdateInput } from '../../dto/form/form.update.input';
|
||||
import { FormFieldDocument, FormFieldSchemaName } from '../../schema/form.field.schema';
|
||||
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||
|
||||
@Injectable()
|
||||
export class FormUpdateService {
|
||||
constructor(
|
||||
@InjectModel(FormSchemaName) private formModel: Model<FormDocument>,
|
||||
@InjectModel(FormFieldSchemaName) private formFieldModel: Model<FormFieldDocument>,
|
||||
) {
|
||||
}
|
||||
|
||||
async update(form: FormDocument, input: FormUpdateInput): Promise<FormDocument> {
|
||||
if (input.language !== undefined) {
|
||||
form.set('language', input.language)
|
||||
}
|
||||
|
||||
if (input.title !== undefined) {
|
||||
form.set('title', input.title)
|
||||
}
|
||||
|
||||
if (input.showFooter !== undefined) {
|
||||
form.set('showFooter', input.showFooter)
|
||||
}
|
||||
|
||||
const fieldMapping = {}
|
||||
|
||||
if (input.fields !== undefined) {
|
||||
const nextFields = await Promise.all(input.fields.map(async (nextField) => {
|
||||
let field = form.fields.find(field => field.id.toString() === nextField.id)
|
||||
|
||||
if (!field) {
|
||||
field = await this.formFieldModel.create({
|
||||
type: nextField.type,
|
||||
})
|
||||
}
|
||||
|
||||
// ability for other fields to apply mapping
|
||||
fieldMapping[nextField.id] = field.id.toString()
|
||||
field.set('title', nextField.title)
|
||||
field.set('description', nextField.description)
|
||||
field.set('required', nextField.required)
|
||||
field.set('value', nextField.value)
|
||||
|
||||
return field
|
||||
}))
|
||||
|
||||
console.log('field mapping', fieldMapping)
|
||||
|
||||
form.set('fields', nextFields)
|
||||
}
|
||||
|
||||
const extractField = (id) => {
|
||||
if (id && fieldMapping[id]) {
|
||||
return fieldMapping[id]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (input.design !== undefined) {
|
||||
form.set('design', input.design)
|
||||
}
|
||||
|
||||
if (input.selfNotifications !== undefined) {
|
||||
form.set('selfNotifications', {
|
||||
...input.selfNotifications,
|
||||
fromField: extractField(input.selfNotifications.fromField)
|
||||
})
|
||||
}
|
||||
|
||||
if (input.respondentNotifications !== undefined) {
|
||||
form.set('respondentNotifications', {
|
||||
...input.respondentNotifications,
|
||||
toField: extractField(input.respondentNotifications.toField)
|
||||
})
|
||||
}
|
||||
|
||||
if (input.startPage !== undefined) {
|
||||
form.set('startPage', input.startPage)
|
||||
}
|
||||
|
||||
if (input.endPage !== undefined) {
|
||||
form.set('endPage', input.endPage)
|
||||
}
|
||||
|
||||
await form.save()
|
||||
|
||||
return form
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
import { FormCreateService } from './form.create.service';
|
||||
import { FormDeleteService } from './form.delete.service';
|
||||
import { FormService } from './form.service';
|
||||
import { FormUpdateService } from './form.update.service';
|
||||
|
||||
export const formServices = [
|
||||
FormService,
|
||||
FormCreateService,
|
||||
FormUpdateService,
|
||||
FormDeleteService,
|
||||
]
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { RedisPubSub } from 'graphql-redis-subscriptions';
|
||||
import { PubSub, PubSubEngine } from 'graphql-subscriptions';
|
||||
import Redis from 'ioredis';
|
||||
import { PinoLogger } from 'nestjs-pino/dist';
|
||||
import { authServices } from './auth';
|
||||
import { formServices } from './form';
|
||||
import { MailService } from './mail.service';
|
||||
@ -8,4 +13,27 @@ export const services = [
|
||||
...formServices,
|
||||
...authServices,
|
||||
MailService,
|
||||
{
|
||||
provide: 'PUB_SUB',
|
||||
inject: [ConfigService, PinoLogger],
|
||||
useFactory: (configService: ConfigService, logger: PinoLogger): PubSubEngine => {
|
||||
const host = configService.get<string>('REDIS_HOST', null)
|
||||
const port = configService.get<number>('REDIS_PORT', 6379)
|
||||
|
||||
if (host === null) {
|
||||
logger.warn('without redis graphql subscriptions will be unreliable in load balanced environments')
|
||||
return new PubSub()
|
||||
}
|
||||
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
}
|
||||
|
||||
return new RedisPubSub({
|
||||
publisher: new Redis(options),
|
||||
subscriber: new Redis(options),
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
59
yarn.lock
59
yarn.lock
@ -2979,6 +2979,11 @@ cls-hooked@^4.2.2:
|
||||
emitter-listener "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
cluster-key-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
|
||||
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
@ -3458,7 +3463,7 @@ delegates@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
denque@^1.4.1:
|
||||
denque@^1.1.0, denque@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
|
||||
integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
|
||||
@ -4763,7 +4768,16 @@ graphql-extensions@^0.12.0:
|
||||
apollo-server-env "^2.4.3"
|
||||
apollo-server-types "^0.4.0"
|
||||
|
||||
graphql-subscriptions@^1.0.0:
|
||||
graphql-redis-subscriptions@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-2.2.1.tgz#377be5670ff344aa78cf147a9852e686a65e4b21"
|
||||
integrity sha512-Rk0hapKUZuZpJIv3rG5rmd1SX3f+9k1k5AXoh8bxbM3Vkdzh28WM7kvJOqq1pJuO3gQ4OAoqzciNT0MMHRylXQ==
|
||||
dependencies:
|
||||
iterall "^1.3.0"
|
||||
optionalDependencies:
|
||||
ioredis "^4.6.3"
|
||||
|
||||
graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11"
|
||||
integrity sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA==
|
||||
@ -5240,6 +5254,21 @@ invert-kv@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
|
||||
|
||||
ioredis@^4.17.1, ioredis@^4.6.3:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.1.tgz#06ef3d3b2cb96b7e6bc90a7b8839a33e743843ad"
|
||||
integrity sha512-kfxkN/YO1dnyaoAGyNdH3my4A1eoGDy4QOfqn6o86fo4dTboxyxYVW0S0v/d3MkwCWlvSWhlwq6IJMY9BlWs6w==
|
||||
dependencies:
|
||||
cluster-key-slot "^1.1.0"
|
||||
debug "^4.1.1"
|
||||
denque "^1.1.0"
|
||||
lodash.defaults "^4.2.0"
|
||||
lodash.flatten "^4.4.0"
|
||||
redis-commands "1.5.0"
|
||||
redis-errors "^1.2.0"
|
||||
redis-parser "^3.0.0"
|
||||
standard-as-callback "^2.0.1"
|
||||
|
||||
ip-regex@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
||||
@ -6339,7 +6368,7 @@ lodash.bind@^4.1.4:
|
||||
resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
|
||||
integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
|
||||
|
||||
lodash.defaults@^4.0.1:
|
||||
lodash.defaults@^4.0.1, lodash.defaults@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
|
||||
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
|
||||
@ -6349,7 +6378,7 @@ lodash.filter@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
|
||||
integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=
|
||||
|
||||
lodash.flatten@^4.2.0:
|
||||
lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
|
||||
@ -8410,6 +8439,23 @@ rechoir@^0.6.2:
|
||||
dependencies:
|
||||
resolve "^1.1.6"
|
||||
|
||||
redis-commands@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785"
|
||||
integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==
|
||||
|
||||
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
|
||||
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
|
||||
|
||||
redis-parser@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
|
||||
integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
|
||||
dependencies:
|
||||
redis-errors "^1.0.0"
|
||||
|
||||
reflect-metadata@^0.1.13:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||
@ -9132,6 +9178,11 @@ stack-utils@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
|
||||
integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
|
||||
|
||||
standard-as-callback@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126"
|
||||
integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==
|
||||
|
||||
static-extend@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user