This commit is contained in:
Michael Schramm 2020-05-29 17:09:19 +02:00
parent faa4aa48eb
commit 414bc04782
47 changed files with 799 additions and 102 deletions

21
LICENSE.md Normal file
View 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.

View File

@ -6,6 +6,10 @@ services:
- "27017:27017" - "27017:27017"
volumes: volumes:
- "./data/mongo:/data" - "./data/mongo:/data"
redis:
image: redis
ports:
- "6379:6379"
# api: # api:
# build: . # build: .
# volumes: # volumes:

View File

@ -3,8 +3,7 @@
"version": "0.3.0", "version": "0.3.0",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "license": "MIT",
"license": "UNLICENSED",
"scripts": { "scripts": {
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "nest build", "build": "nest build",
@ -40,9 +39,12 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"graphql": "15.0.0", "graphql": "15.0.0",
"graphql-redis-subscriptions": "^2.2.1",
"graphql-subscriptions": "^1.1.0",
"graphql-tools": "^5.0.0", "graphql-tools": "^5.0.0",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"inquirer": "^7.1.0", "inquirer": "^7.1.0",
"ioredis": "^4.17.1",
"migrate-mongoose": "^4.0.0", "migrate-mongoose": "^4.0.0",
"mongoose": "^5.9.11", "mongoose": "^5.9.11",
"nestjs-console": "^3.0.2", "nestjs-console": "^3.0.2",

View File

@ -1,15 +1,16 @@
import { MailerModule } from '@nestjs-modules/mailer'; import { MailerModule } from '@nestjs-modules/mailer';
import { HttpModule, RequestMethod } from '@nestjs/common'; import { HttpModule, RequestMethod } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLFederationModule } from '@nestjs/graphql'; import { GraphQLModule } from '@nestjs/graphql';
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose'; import { MongooseModule } from '@nestjs/mongoose';
import { MongooseModuleOptions } from '@nestjs/mongoose/dist/interfaces/mongoose-options.interface'; import { MongooseModuleOptions } from '@nestjs/mongoose/dist/interfaces/mongoose-options.interface';
import crypto from 'crypto'; import crypto from 'crypto';
import { Request } from 'express-serve-static-core';
import { IncomingHttpHeaders } from 'http';
import { ConsoleModule } from 'nestjs-console'; import { ConsoleModule } from 'nestjs-console';
import { LoggerModule, Params as LoggerModuleParams } from 'nestjs-pino/dist'; import { LoggerModule, Params as LoggerModuleParams } from 'nestjs-pino/dist';
import { join } from 'path'; import { join } from 'path';
import { ContextCache } from './resolver/context.cache';
import { schema } from './schema'; import { schema } from './schema';
export const LoggerConfig: LoggerModuleParams = { export const LoggerConfig: LoggerModuleParams = {
@ -60,14 +61,14 @@ export const imports = [
}) })
}), }),
LoggerModule.forRoot(LoggerConfig), LoggerModule.forRoot(LoggerConfig),
GraphQLFederationModule.forRoot({ GraphQLModule.forRoot({
debug: process.env.NODE_ENV !== 'production', debug: process.env.NODE_ENV !== 'production',
definitions: { definitions: {
outputAs: 'class', outputAs: 'class',
}, },
introspection: true, introspection: true,
playground: true, playground: true,
// installSubscriptionHandlers: true, installSubscriptionHandlers: true,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
// to allow guards on resolver props https://github.com/nestjs/graphql/issues/295 // to allow guards on resolver props https://github.com/nestjs/graphql/issues/295
fieldResolverEnhancers: [ fieldResolverEnhancers: [
@ -77,11 +78,22 @@ export const imports = [
resolverValidationOptions: { resolverValidationOptions: {
}, },
context: ({ req }) => { context: ({ req, connection }) => {
return { if (!req && connection) {
cache: new ContextCache(), const headers: IncomingHttpHeaders = {}
req,
Object.keys(connection.context).forEach(key => {
headers[key.toLowerCase()] = connection.context[key]
})
return {
req: {
headers
} as Request
}
} }
return { req }
}, },
}), }),
MongooseModule.forRootAsync({ MongooseModule.forRootAsync({

View File

@ -3,10 +3,10 @@ export const fieldTypes = [
'textfield', 'textfield',
'date', 'date',
'email', 'email',
'legal', // 'legal',
'textarea', 'textarea',
'link', 'link',
'statement', // 'statement',
'dropdown', 'dropdown',
'rating', 'rating',
'radio', 'radio',

View 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
}

View 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
}

View File

@ -9,7 +9,7 @@ export class ButtonModel {
readonly action?: string readonly action?: string
@Field({ nullable: true }) @Field({ nullable: true })
readonly text: string readonly text?: string
@Field({ nullable: true }) @Field({ nullable: true })
readonly bgColor?: string readonly bgColor?: string

View 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
}

View 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
}

View 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[]
}

View 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
}

View 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
}
}

View File

@ -21,12 +21,12 @@ export class FormModel {
@Field() @Field()
readonly showFooter: boolean readonly showFooter: boolean
constructor(partial: Partial<FormDocument>) { constructor(form: FormDocument) {
this.id = partial.id this.id = form.id
this.title = partial.title this.title = form.title
this.created = partial.created this.created = form.created
this.lastModified = partial.lastModified this.lastModified = form.lastModified
this.language = partial.language this.language = form.language
this.showFooter = partial.showFooter this.showFooter = form.showFooter
} }
} }

View 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
}

View 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[]
}

View File

@ -2,8 +2,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { FormPage } from '../../schema/form.schema'; import { FormPage } from '../../schema/form.schema';
import { ButtonModel } from './button.model'; import { ButtonModel } from './button.model';
@ObjectType('FormPage') @ObjectType('Page')
export class FormPageModel { export class PageModel {
@Field() @Field()
readonly show: boolean readonly show: boolean

View 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
}
}

View 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
}

View 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
}

View File

@ -1,8 +1,15 @@
import { Field, ObjectType } from '@nestjs/graphql'; import { Field, ObjectType } from '@nestjs/graphql';
import { UserDocument } from '../../schema/user.schema';
import { UserModel } from './user.model'; import { UserModel } from './user.model';
@ObjectType('OwnUser') @ObjectType('OwnUser')
export class OwnUserModel extends UserModel { export class OwnUserModel extends UserModel {
@Field(() => [String]) @Field(() => [String])
readonly roles: string[] readonly roles: string[]
constructor(user: UserDocument) {
super(user)
this.roles = user.roles
}
} }

View File

@ -21,7 +21,7 @@ export class UserModel {
@Field() @Field()
readonly lastName?: string readonly lastName?: string
constructor(user: Partial<UserDocument>) { constructor(user: UserDocument) {
this.id = user.id this.id = user.id
this.username = user.username this.username = user.username
this.email = user.email this.email = user.email

View File

@ -1,15 +1,14 @@
import { ExecutionContext, Injectable } from '@nestjs/common'; import { ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql'; import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { ContextCache } from '../resolver/context.cache';
@Injectable() @Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') { export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) { getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context); const ctx = GqlExecutionContext.create(context);
if (!ctx.getContext().cache) { if (!ctx.getContext().cache) {
ctx.getContext().cache = { ctx.getContext().cache = new ContextCache()
// add(type, id, object) =>
}
} }
return ctx.getContext().req; return ctx.getContext().req;
} }

View File

@ -19,5 +19,5 @@ import { AppModule } from './app.module';
app.enableCors({origin: '*'}) app.enableCors({origin: '*'})
app.getHttpAdapter().options('*', cors()) app.getHttpAdapter().options('*', cors())
await app.listen(process.env.PORT || 3000); await app.listen(process.env.PORT || 4100);
})() })()

View File

@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql'; import { Args, Mutation } from '@nestjs/graphql';
import { PinoLogger } from 'nestjs-pino/dist';
import { AuthJwtModel } from '../../dto/auth/auth.jwt.model'; import { AuthJwtModel } from '../../dto/auth/auth.jwt.model';
import { UserCreateInput } from '../../dto/user/user.create.input'; import { UserCreateInput } from '../../dto/user/user.create.input';
import { AuthService } from '../../service/auth/auth.service'; import { AuthService } from '../../service/auth/auth.service';
@ -10,6 +11,7 @@ export class AuthRegisterResolver {
constructor( constructor(
private readonly createUser: UserCreateService, private readonly createUser: UserCreateService,
private readonly auth: AuthService, private readonly auth: AuthService,
private readonly logger: PinoLogger,
) { ) {
} }
@ -17,6 +19,10 @@ export class AuthRegisterResolver {
async authRegister( async authRegister(
@Args({ name: 'user' }) data: UserCreateInput, @Args({ name: 'user' }) data: UserCreateInput,
): Promise<AuthJwtModel> { ): Promise<AuthJwtModel> {
this.logger.info({
email: data.email,
username: data.username,
}, 'try to register new user')
const user = await this.createUser.create(data) const user = await this.createUser.create(data)
return this.auth.login(user) return this.auth.login(user)

View 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)
}
}

View 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)
}
}

View File

@ -2,8 +2,9 @@ import { Args, Context, ID, Parent, Query, ResolveField, Resolver } from '@nestj
import { Roles } from '../../decorator/roles.decorator'; import { Roles } from '../../decorator/roles.decorator';
import { User } from '../../decorator/user.decorator'; import { User } from '../../decorator/user.decorator';
import { DesignModel } from '../../dto/form/design.model'; import { DesignModel } from '../../dto/form/design.model';
import { FormFieldModel } from '../../dto/form/form.field.model';
import { FormModel } from '../../dto/form/form.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 { RespondentNotificationsModel } from '../../dto/form/respondent.notifications.model';
import { SelfNotificationsModel } from '../../dto/form/self.notifications.model'; import { SelfNotificationsModel } from '../../dto/form/self.notifications.model';
import { UserModel } from '../../dto/user/user.model'; import { UserModel } from '../../dto/user/user.model';
@ -35,6 +36,17 @@ export class FormResolver {
return new FormModel(form) 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) @ResolveField('isLive', () => Boolean)
@Roles('admin') @Roles('admin')
async getRoles( async getRoles(
@ -67,13 +79,13 @@ export class FormResolver {
return new SelfNotificationsModel(form.selfNotifications) return new SelfNotificationsModel(form.selfNotifications)
} }
@ResolveField('respondentNotifications', () => SelfNotificationsModel) @ResolveField('respondentNotifications', () => RespondentNotificationsModel)
@Roles('admin') @Roles('admin')
async getRespondentNotifications( async getRespondentNotifications(
@User() user: UserDocument, @User() user: UserDocument,
@Parent() parent: FormModel, @Parent() parent: FormModel,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<SelfNotificationsModel> { ): Promise<RespondentNotificationsModel> {
const form = await cache.getForm(parent.id) const form = await cache.getForm(parent.id)
if (!await this.formService.isAdmin(form, user)) { if (!await this.formService.isAdmin(form, user)) {
@ -94,24 +106,24 @@ export class FormResolver {
return new DesignModel(form.design) return new DesignModel(form.design)
} }
@ResolveField('startPage', () => FormPageModel) @ResolveField('startPage', () => PageModel)
async getStartPage( async getStartPage(
@Parent() parent: FormModel, @Parent() parent: FormModel,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<FormPageModel> { ): Promise<PageModel> {
const form = await cache.getForm(parent.id) 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( async getEndPage(
@Parent() parent: FormModel, @Parent() parent: FormModel,
@Context('cache') cache: ContextCache, @Context('cache') cache: ContextCache,
): Promise<FormPageModel> { ): Promise<PageModel> {
const form = await cache.getForm(parent.id) const form = await cache.getForm(parent.id)
return new FormPageModel(form.endPage) return new PageModel(form.endPage)
} }
@ResolveField('admin', () => UserModel) @ResolveField('admin', () => UserModel)

View 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,
)
}
}

View 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)
}
}

View File

@ -1,9 +1,13 @@
import { FieldResolver } from './field.resolver'; import { FormCreateMutation } from './form.create.mutation';
import { FormCreateResolver } from './form.create.resolver'; import { FormDeleteMutation } from './form.delete.mutation';
import { FormResolver } from './form.resolver'; import { FormResolver } from './form.resolver';
import { FormSearchResolver } from './form.search.resolver';
import { FormUpdateMutation } from './form.update.mutation';
export const formResolvers = [ export const formResolvers = [
FormResolver, FormResolver,
FormCreateResolver, FormSearchResolver,
FieldResolver, FormCreateMutation,
FormDeleteMutation,
FormUpdateMutation,
] ]

View File

@ -1,5 +1,5 @@
import { Schema, SchemaDefinition } from 'mongoose'; import { Schema, SchemaDefinition } from 'mongoose';
import { FieldSchemaName } from '../field.schema'; import { FormFieldSchemaName } from '../form.field.schema';
export const LogicJump: SchemaDefinition = { export const LogicJump: SchemaDefinition = {
expressionString: { expressionString: {
@ -21,14 +21,14 @@ export const LogicJump: SchemaDefinition = {
}, },
fieldA: { fieldA: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: FieldSchemaName ref: FormFieldSchemaName
}, },
valueB: { valueB: {
type: String, type: String,
}, },
jumpTo: { jumpTo: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: FieldSchemaName ref: FormFieldSchemaName
}, },
enabled: { enabled: {
type: Boolean, type: Boolean,

View File

@ -24,9 +24,4 @@ export const RatingField: SchemaDefinition = {
'Trash', 'Trash',
], ],
}, },
validShapes: {
type: [{
type: String,
}]
}
} }

View File

@ -4,17 +4,21 @@ import { FieldOption } from './embedded/field.option';
import { LogicJump } from './embedded/logic.jump'; import { LogicJump } from './embedded/logic.jump';
import { RatingField } from './embedded/rating.field'; import { RatingField } from './embedded/rating.field';
export const FieldSchemaName = 'FormField' export const FormFieldSchemaName = 'FormField'
export interface FieldDocument extends Document { export interface FormFieldDocument extends Document {
isSubmission: boolean title: string
description: string
logicJump: any
rating: any
options: any
required: boolean
disabled: boolean
type: string
value: any
} }
export const FieldSchema = new Schema({ export const FormFieldSchema = new Schema({
isSubmission: {
type: Boolean,
default: false,
},
title: { title: {
type: String, type: String,
trim: true, trim: true,
@ -27,9 +31,11 @@ export const FieldSchema = new Schema({
type: LogicJump, type: LogicJump,
}, },
ratingOptions: { ratingOptions: {
alias: 'rating',
type: RatingField, type: RatingField,
}, },
fieldOptions: { fieldOptions: {
alias: 'options',
type: [FieldOption], type: [FieldOption],
}, },
required: { required: {
@ -40,19 +46,20 @@ export const FieldSchema = new Schema({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
deletePreserved: { // TODO remove
type: Boolean,
default: false,
},
validFieldTypes: { // TODO remove
type: [String],
},
fieldType: { fieldType: {
alias: 'type',
type: String, type: String,
enum: fieldTypes, enum: fieldTypes,
}, },
fieldValue: { fieldValue: {
alias: 'value',
type: Schema.Types.Mixed, type: Schema.Types.Mixed,
default: '', default: '',
}, },
}) })
export const FormFieldDefinition = {
name: FormFieldSchemaName,
schema: FormFieldSchema,
}

View File

@ -1,12 +1,10 @@
import exp from 'constants';
import { Document, Schema } from 'mongoose'; import { Document, Schema } from 'mongoose';
import { matchType } from '../config/fields'; import { matchType } from '../config/fields';
import { defaultLanguage, languages } from '../config/languages'; import { defaultLanguage, languages } from '../config/languages';
import { rolesType } from '../config/roles';
import { ButtonDocument, ButtonSchema } from './button.schema'; import { ButtonDocument, ButtonSchema } from './button.schema';
import { FieldDocument, FieldSchema } from './field.schema'; import { FormFieldDocument, FormFieldSchema } from './form.field.schema';
import { VisitorDataDocument, VisitorDataSchema } from './visitor.data.schema';
import { UserDocument, UserSchemaName } from './user.schema'; import { UserDocument, UserSchemaName } from './user.schema';
import { VisitorDataDocument, VisitorDataSchema } from './visitor.data.schema';
export const FormSchemaName = 'Form' export const FormSchemaName = 'Form'
@ -58,7 +56,7 @@ export interface FormDocument extends Document {
readonly visitors: [VisitorDataDocument] readonly visitors: [VisitorDataDocument]
} }
readonly fields: [FieldDocument] readonly fields: [FormFieldDocument]
readonly admin: UserDocument readonly admin: UserDocument
@ -103,9 +101,10 @@ export const FormSchema = new Schema({
type: [VisitorDataSchema], type: [VisitorDataSchema],
}, },
}, },
fields: { // eslint-disable-next-line @typescript-eslint/camelcase
alias: 'form_fields', form_fields: {
type: [FieldSchema], alias: 'fields',
type: [FormFieldSchema],
default: [], default: [],
}, },
admin: { admin: {
@ -113,18 +112,18 @@ export const FormSchema = new Schema({
ref: UserSchemaName, ref: UserSchemaName,
}, },
startPage: { startPage: {
show: { showStart: {
alias: 'showStart', alias: 'startPage.show',
type: Boolean, type: Boolean,
default: false default: false
}, },
title: { introTitle: {
alias: 'introTitle', alias: 'startPage.title',
type: String, type: String,
default: 'Welcome to Form', default: 'Welcome to Form',
}, },
paragraph: { introParagraph: {
alias: 'introParagraph', alias: 'startPage.paragraph',
type: String, type: String,
default: 'Start', default: 'Start',
}, },
@ -133,8 +132,8 @@ export const FormSchema = new Schema({
}, },
}, },
endPage: { endPage: {
show: { showEnd: {
alias: 'showEnd', alias: 'endPage.show',
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -157,8 +156,8 @@ export const FormSchema = new Schema({
fromField: { fromField: {
type: String, type: String,
}, },
toEmail: { toEmails: {
alias: 'toEmails', alias: 'selfNotifications.toEmail',
type: String, type: String,
}, },
subject: { subject: {
@ -176,8 +175,8 @@ export const FormSchema = new Schema({
toField: { toField: {
type: String, type: String,
}, },
fromEmail: { fromEmails: {
alias: 'fromEmails', alias: 'respondentNotifications.fromEmail',
type: String, type: String,
match: matchType.email, match: matchType.email,
}, },

View File

@ -1,9 +1,11 @@
import { FormFieldDefinition } from './form.field.schema';
import { FormDefinition } from './form.schema'; import { FormDefinition } from './form.schema';
import { FormSubmissionDefinition, FormSubmissionSchema } from './form.submission.schema'; import { SubmissionDefinition } from './submission.schema';
import { UserDefinition } from './user.schema'; import { UserDefinition } from './user.schema';
export const schema = [ export const schema = [
FormDefinition, FormDefinition,
FormSubmissionDefinition, FormFieldDefinition,
SubmissionDefinition,
UserDefinition, UserDefinition,
] ]

View 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: '',
},
})

View File

@ -1,16 +1,17 @@
import { Document, Schema } from 'mongoose'; import { Document, Schema } from 'mongoose';
import { FieldSchema } from './field.schema';
import { FormSchemaName } from './form.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: { fields: {
alias: 'form_fields', alias: 'form_fields',
type: [FieldSchema], type: [SubmissionFieldSchemaName],
default: [], default: [],
}, },
form: { form: {
@ -50,8 +51,8 @@ export const FormSubmissionSchema = new Schema({
} }
}) })
export const FormSubmissionDefinition = { export const SubmissionDefinition = {
name: FormSubmissionSchemaName, name: SubmissionSchemaName,
schema: FormSubmissionSchema, schema: SubmissionSchema,
} }

View File

@ -1,11 +1,11 @@
import { Document, Schema } from 'mongoose'; import { Document, Schema } from 'mongoose';
import { defaultLanguage, languages } from '../config/languages'; import { defaultLanguage, languages } from '../config/languages';
import { FieldDocument, FieldSchemaName } from './field.schema'; import { FormFieldDocument, FormFieldSchemaName } from './form.field.schema';
export interface VisitorDataDocument extends Document { export interface VisitorDataDocument extends Document {
readonly introParagraph?: string readonly introParagraph?: string
readonly referrer?: string readonly referrer?: string
readonly filledOutFields: [FieldDocument] readonly filledOutFields: [FormFieldDocument]
readonly timeElapsed: number readonly timeElapsed: number
readonly isSubmitted: boolean readonly isSubmitted: boolean
readonly language: string readonly language: string
@ -24,7 +24,7 @@ export const VisitorDataSchema = new Schema({
filledOutFields: { filledOutFields: {
type: [{ type: [{
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: FieldSchemaName, ref: FormFieldSchemaName,
}], }],
}, },
timeElapsed: { timeElapsed: {

View File

@ -16,6 +16,8 @@ export class AuthService {
async validateUser(username: string, password: string): Promise<UserDocument> { async validateUser(username: string, password: string): Promise<UserDocument> {
console.log('check user??', username) console.log('check user??', username)
// TODO only allow login for verified users!
const user = await this.userService.findByUsername(username); const user = await this.userService.findByUsername(username);
if (user && await this.passwordService.verify(password, user.passwordHash, user.salt)) { if (user && await this.passwordService.verify(password, user.passwordHash, user.salt)) {
return user; return user;

View File

@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; 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 { FormDocument, FormSchemaName } from '../../schema/form.schema';
import { UserDocument } from '../../schema/user.schema';
@Injectable() @Injectable()
export class FormCreateService { export class FormCreateService {
@ -10,4 +12,7 @@ export class FormCreateService {
) { ) {
} }
async create(admin: UserDocument, input: FormCreateInput): Promise<FormDocument> {
return null
}
} }

View 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')
}
}

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { GraphQLError } from 'graphql';
import { Model, Types } from 'mongoose'; import { Model, Types } from 'mongoose';
import { FormDocument, FormSchemaName } from '../../schema/form.schema'; import { FormDocument, FormSchemaName } from '../../schema/form.schema';
import { UserDocument } from '../../schema/user.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)) 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> { async findById(id: string): Promise<FormDocument> {
const form = await this.formModel.findById(id); const form = await this.formModel.findById(id);

View 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
}
}

View File

@ -1,7 +1,11 @@
import { FormCreateService } from './form.create.service'; import { FormCreateService } from './form.create.service';
import { FormDeleteService } from './form.delete.service';
import { FormService } from './form.service'; import { FormService } from './form.service';
import { FormUpdateService } from './form.update.service';
export const formServices = [ export const formServices = [
FormService, FormService,
FormCreateService, FormCreateService,
FormUpdateService,
FormDeleteService,
] ]

View File

@ -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 { authServices } from './auth';
import { formServices } from './form'; import { formServices } from './form';
import { MailService } from './mail.service'; import { MailService } from './mail.service';
@ -8,4 +13,27 @@ export const services = [
...formServices, ...formServices,
...authServices, ...authServices,
MailService, 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),
})
}
},
] ]

View File

@ -2979,6 +2979,11 @@ cls-hooked@^4.2.2:
emitter-listener "^1.0.1" emitter-listener "^1.0.1"
semver "^5.4.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: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 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" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
denque@^1.4.1: denque@^1.1.0, denque@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
@ -4763,7 +4768,16 @@ graphql-extensions@^0.12.0:
apollo-server-env "^2.4.3" apollo-server-env "^2.4.3"
apollo-server-types "^0.4.0" 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" version "1.1.0"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11"
integrity sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA== 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" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= 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: ip-regex@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" 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" resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
lodash.defaults@^4.0.1: lodash.defaults@^4.0.1, lodash.defaults@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= 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" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=
lodash.flatten@^4.2.0: lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
@ -8410,6 +8439,23 @@ rechoir@^0.6.2:
dependencies: dependencies:
resolve "^1.1.6" 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: reflect-metadata@^0.1.13:
version "0.1.13" version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" 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" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== 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: static-extend@^0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"