add logic to create forms, to add submissions, to read submissions, etc
This commit is contained in:
parent
eb5bc26e5c
commit
eda8a3920c
@ -38,6 +38,7 @@
|
||||
"commander": "^5.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.2",
|
||||
"dayjs": "^1.8.28",
|
||||
"graphql": "15.0.0",
|
||||
"graphql-redis-subscriptions": "^2.2.1",
|
||||
"graphql-subscriptions": "^1.1.0",
|
||||
|
||||
@ -16,8 +16,8 @@ export const fieldTypes = [
|
||||
]
|
||||
|
||||
export const matchType = {
|
||||
color: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
||||
url: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/,
|
||||
color: /^#([A-F0-9]{6}|[A-F0-9]{3})$/i,
|
||||
url: /((([A-Z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/i,
|
||||
email: /.+@.+\..+/,
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('ButtonInput')
|
||||
@InputType()
|
||||
export class ButtonInput {
|
||||
@Field({ nullable: true })
|
||||
readonly url?: string
|
||||
@ -14,6 +14,9 @@ export class ButtonInput {
|
||||
@Field({ nullable: true })
|
||||
readonly bgColor?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly activeColor?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly color?: string
|
||||
}
|
||||
|
||||
@ -14,6 +14,9 @@ export class ButtonModel {
|
||||
@Field({ nullable: true })
|
||||
readonly bgColor?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly activeColor?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly color?: string
|
||||
|
||||
@ -22,6 +25,7 @@ export class ButtonModel {
|
||||
this.action = button.action
|
||||
this.text = button.text
|
||||
this.bgColor = button.bgColor
|
||||
this.activeColor = button.activeColor
|
||||
this.color = button.color
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('ColorsInput')
|
||||
@InputType()
|
||||
export class ColorsInput {
|
||||
@Field()
|
||||
readonly backgroundColor: string
|
||||
@ -14,6 +14,9 @@ export class ColorsInput {
|
||||
@Field()
|
||||
readonly buttonColor: string
|
||||
|
||||
@Field()
|
||||
readonly buttonActiveColor: string
|
||||
|
||||
@Field()
|
||||
readonly buttonTextColor: string
|
||||
}
|
||||
|
||||
@ -15,6 +15,9 @@ export class ColorsModel {
|
||||
@Field()
|
||||
readonly buttonColor: string
|
||||
|
||||
@Field()
|
||||
readonly buttonActiveColor: string
|
||||
|
||||
@Field()
|
||||
readonly buttonTextColor: string
|
||||
|
||||
@ -23,6 +26,7 @@ export class ColorsModel {
|
||||
this.questionColor = partial.questionColor
|
||||
this.answerColor = partial.answerColor
|
||||
this.buttonColor = partial.buttonColor
|
||||
this.buttonActiveColor = partial.buttonActiveColor
|
||||
this.buttonTextColor = partial.buttonTextColor
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { ColorsInput } from './colors.input';
|
||||
|
||||
@InputType('DesignInput')
|
||||
@InputType()
|
||||
export class DesignInput {
|
||||
@Field()
|
||||
readonly colors: ColorsInput
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
import { FormUpdateInput } from './form.update.input';
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('FormCreateInput')
|
||||
export class FormCreateInput extends FormUpdateInput {
|
||||
@Field(() => ID, { nullable: true })
|
||||
readonly id: string
|
||||
export class FormCreateInput {
|
||||
@Field()
|
||||
readonly title: string
|
||||
|
||||
@Field()
|
||||
readonly language: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly showFooter: boolean
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly isLive: boolean
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType('FormFieldInput')
|
||||
@InputType()
|
||||
export class FormFieldInput {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@ -5,7 +5,7 @@ import { PageInput } from './page.input';
|
||||
import { RespondentNotificationsInput } from './respondent.notifications.input';
|
||||
import { SelfNotificationsInput } from './self.notifications.input';
|
||||
|
||||
@InputType('FormUpdateInput')
|
||||
@InputType()
|
||||
export class FormUpdateInput {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { ButtonInput } from './button.input';
|
||||
|
||||
@InputType('PageInput')
|
||||
@InputType()
|
||||
export class PageInput {
|
||||
@Field()
|
||||
readonly show: boolean
|
||||
|
||||
10
src/dto/submission/device.input.ts
Normal file
10
src/dto/submission/device.input.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType()
|
||||
export class DeviceInput {
|
||||
@Field()
|
||||
readonly type: string
|
||||
|
||||
@Field()
|
||||
readonly name: string
|
||||
}
|
||||
16
src/dto/submission/device.model.ts
Normal file
16
src/dto/submission/device.model.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { Device } from '../../schema/submission.schema';
|
||||
|
||||
@ObjectType('Device')
|
||||
export class DeviceModel {
|
||||
@Field()
|
||||
readonly type: string
|
||||
|
||||
@Field()
|
||||
readonly name: string
|
||||
|
||||
constructor(device: Device) {
|
||||
this.type = device.type
|
||||
this.name = device.name
|
||||
}
|
||||
}
|
||||
16
src/dto/submission/geo.location.model.ts
Normal file
16
src/dto/submission/geo.location.model.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { GeoLocation } from '../../schema/submission.schema';
|
||||
|
||||
@ObjectType('GeoLocation')
|
||||
export class GeoLocationModel {
|
||||
@Field({ nullable: true })
|
||||
country?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
city?: string
|
||||
|
||||
constructor(geo: GeoLocation) {
|
||||
this.country = geo.country
|
||||
this.city = geo.city
|
||||
}
|
||||
}
|
||||
25
src/dto/submission/pager.submission.model.ts
Normal file
25
src/dto/submission/pager.submission.model.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { GraphQLInt } from 'graphql';
|
||||
import { SubmissionModel } from './submission.model';
|
||||
|
||||
@ObjectType('PagerSubmission')
|
||||
export class PagerSubmissionModel {
|
||||
@Field(() => [SubmissionModel])
|
||||
entries: SubmissionModel[]
|
||||
|
||||
@Field(() => GraphQLInt)
|
||||
total: number
|
||||
|
||||
@Field(() => GraphQLInt)
|
||||
limit: number
|
||||
|
||||
@Field(() => GraphQLInt)
|
||||
start: number
|
||||
|
||||
constructor(entries: SubmissionModel[], total: number, limit: number, start: number) {
|
||||
this.entries = entries
|
||||
this.total = total
|
||||
this.limit = limit
|
||||
this.start = start
|
||||
}
|
||||
}
|
||||
20
src/dto/submission/submission.field.model.ts
Normal file
20
src/dto/submission/submission.field.model.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { SubmissionFieldDocument } from '../../schema/submission.field.schema';
|
||||
|
||||
@ObjectType('SubmissionField')
|
||||
export class SubmissionFieldModel {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
readonly value: string
|
||||
|
||||
@Field()
|
||||
readonly type: string
|
||||
|
||||
constructor(field: SubmissionFieldDocument) {
|
||||
this.id = field.id
|
||||
this.value = JSON.stringify(field.fieldValue)
|
||||
this.type = field.fieldType
|
||||
}
|
||||
}
|
||||
45
src/dto/submission/submission.model.ts
Normal file
45
src/dto/submission/submission.model.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { SubmissionDocument } from '../../schema/submission.schema';
|
||||
import { DeviceModel } from './device.model';
|
||||
import { GeoLocationModel } from './geo.location.model';
|
||||
|
||||
@ObjectType('Submission')
|
||||
export class SubmissionModel {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
readonly ipAddr: string
|
||||
|
||||
@Field(() => GeoLocationModel)
|
||||
readonly geoLocation: GeoLocationModel
|
||||
|
||||
@Field(() => DeviceModel)
|
||||
readonly device: DeviceModel
|
||||
|
||||
@Field()
|
||||
readonly timeElapsed: number
|
||||
|
||||
@Field()
|
||||
readonly percentageComplete: number
|
||||
|
||||
@Field()
|
||||
readonly created: Date
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly lastModified?: Date
|
||||
|
||||
constructor(submission: SubmissionDocument) {
|
||||
this.id = submission.id
|
||||
|
||||
this.ipAddr = submission.ipAddr
|
||||
this.geoLocation = new GeoLocationModel(submission.geoLocation)
|
||||
this.device = new DeviceModel(submission.device)
|
||||
|
||||
this.timeElapsed = submission.timeElapsed
|
||||
this.percentageComplete = submission.percentageComplete
|
||||
|
||||
this.created = submission.created
|
||||
this.lastModified = submission.lastModified
|
||||
}
|
||||
}
|
||||
30
src/dto/submission/submission.progress.model.ts
Normal file
30
src/dto/submission/submission.progress.model.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { SubmissionDocument } from '../../schema/submission.schema';
|
||||
|
||||
@ObjectType('SubmissionProgress')
|
||||
export class SubmissionProgressModel {
|
||||
@Field(() => ID)
|
||||
readonly id: string
|
||||
|
||||
@Field()
|
||||
readonly timeElapsed: number
|
||||
|
||||
@Field()
|
||||
readonly percentageComplete: number
|
||||
|
||||
@Field()
|
||||
readonly created: Date
|
||||
|
||||
@Field({ nullable: true })
|
||||
readonly lastModified?: Date
|
||||
|
||||
constructor(submission: Partial<SubmissionDocument>) {
|
||||
this.id = submission.id
|
||||
|
||||
this.timeElapsed = submission.timeElapsed
|
||||
this.percentageComplete = submission.percentageComplete
|
||||
|
||||
this.created = submission.created
|
||||
this.lastModified = submission.lastModified
|
||||
}
|
||||
}
|
||||
13
src/dto/submission/submission.set.field.input.ts
Normal file
13
src/dto/submission/submission.set.field.input.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType()
|
||||
export class SubmissionSetFieldInput {
|
||||
@Field()
|
||||
readonly token: string
|
||||
|
||||
@Field(() => ID)
|
||||
readonly field: string
|
||||
|
||||
@Field()
|
||||
readonly data: string
|
||||
}
|
||||
11
src/dto/submission/submission.start.input.ts
Normal file
11
src/dto/submission/submission.start.input.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { DeviceInput } from './device.input';
|
||||
|
||||
@InputType()
|
||||
export class SubmissionStartInput {
|
||||
@Field()
|
||||
readonly token: string
|
||||
|
||||
@Field(() => DeviceInput)
|
||||
readonly device: DeviceInput
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
import { FormFieldDocument } from '../schema/form.field.schema';
|
||||
import { FormDocument } from '../schema/form.schema';
|
||||
import { SubmissionDocument } from '../schema/submission.schema';
|
||||
import { UserDocument } from '../schema/user.schema';
|
||||
|
||||
export class ContextCache {
|
||||
@ -10,6 +12,14 @@ export class ContextCache {
|
||||
[id: string]: FormDocument,
|
||||
} = {}
|
||||
|
||||
private submissions: {
|
||||
[id: string]: SubmissionDocument,
|
||||
} = {}
|
||||
|
||||
private formField: {
|
||||
[id: string]: FormFieldDocument,
|
||||
} = {}
|
||||
|
||||
public addUser(user: UserDocument) {
|
||||
this.users[user.id] = user;
|
||||
}
|
||||
@ -25,4 +35,20 @@ export class ContextCache {
|
||||
public async getForm(id: any): Promise<FormDocument> {
|
||||
return this.forms[id]
|
||||
}
|
||||
|
||||
public addSubmission(submission: SubmissionDocument) {
|
||||
this.submissions[submission.id] = submission
|
||||
}
|
||||
|
||||
public async getSubmission(id: any): Promise<SubmissionDocument> {
|
||||
return this.submissions[id]
|
||||
}
|
||||
|
||||
public addFormField(formField: FormFieldDocument) {
|
||||
this.formField[formField.id] = formField
|
||||
}
|
||||
|
||||
public async getFormField(id: any): Promise<FormFieldDocument> {
|
||||
return this.formField[id]
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,12 @@ export class FormSearchResolver {
|
||||
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit,
|
||||
@Context('cache') cache: ContextCache,
|
||||
) {
|
||||
const [forms, total] = await this.formService.find(user, start, limit)
|
||||
const [forms, total] = await this.formService.find(
|
||||
start,
|
||||
limit,
|
||||
{},
|
||||
user.roles.includes('superuser') ? null : user,
|
||||
)
|
||||
|
||||
forms.forEach(form => cache.addForm(form))
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { authServices } from './auth';
|
||||
import { formResolvers } from './form';
|
||||
import { myResolvers } from './me';
|
||||
import { StatusResolver } from './status.resolver';
|
||||
import { submissionResolvers } from './submission';
|
||||
import { userResolvers } from './user';
|
||||
|
||||
export const resolvers = [
|
||||
@ -10,4 +11,5 @@ export const resolvers = [
|
||||
...authServices,
|
||||
...myResolvers,
|
||||
...formResolvers,
|
||||
...submissionResolvers,
|
||||
]
|
||||
|
||||
13
src/resolver/submission/index.ts
Normal file
13
src/resolver/submission/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { SubmissionProgressResolver } from './submission.progress.resolver';
|
||||
import { SubmissionResolver } from './submission.resolver';
|
||||
import { SubmissionSearchResolver } from './submission.search.resolver';
|
||||
import { SubmissionSetFieldMutation } from './submission.set.field.mutation';
|
||||
import { SubmissionStartMutation } from './submission.start.mutation';
|
||||
|
||||
export const submissionResolvers = [
|
||||
SubmissionProgressResolver,
|
||||
SubmissionResolver,
|
||||
SubmissionSetFieldMutation,
|
||||
SubmissionStartMutation,
|
||||
SubmissionSearchResolver,
|
||||
]
|
||||
7
src/resolver/submission/submission.progress.resolver.ts
Normal file
7
src/resolver/submission/submission.progress.resolver.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Resolver } from '@nestjs/graphql';
|
||||
import { SubmissionProgressModel } from '../../dto/submission/submission.progress.model';
|
||||
|
||||
@Resolver(() => SubmissionProgressModel)
|
||||
export class SubmissionProgressResolver {
|
||||
|
||||
}
|
||||
30
src/resolver/submission/submission.resolver.ts
Normal file
30
src/resolver/submission/submission.resolver.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { SubmissionFieldModel } from '../../dto/submission/submission.field.model';
|
||||
import { SubmissionModel } from '../../dto/submission/submission.model';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Resolver(() => SubmissionModel)
|
||||
export class SubmissionResolver {
|
||||
@ResolveField('fields', () => [SubmissionFieldModel])
|
||||
async getFields(
|
||||
@User() user: UserDocument,
|
||||
@Parent() parent: SubmissionModel,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<SubmissionFieldModel[]> {
|
||||
const submission = await cache.getSubmission(parent.id)
|
||||
|
||||
if (!submission.populated('form')) {
|
||||
submission.populate('form')
|
||||
await submission.execPopulate()
|
||||
}
|
||||
|
||||
cache.addForm(submission.form)
|
||||
submission.form.fields.forEach(field => {
|
||||
cache.addFormField(field)
|
||||
})
|
||||
|
||||
return submission.fields.map(field => new SubmissionFieldModel(field))
|
||||
}
|
||||
}
|
||||
45
src/resolver/submission/submission.search.resolver.ts
Normal file
45
src/resolver/submission/submission.search.resolver.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Args, Context, ID, Query, Resolver } from '@nestjs/graphql';
|
||||
import { GraphQLInt } from 'graphql';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { PagerSubmissionModel } from '../../dto/submission/pager.submission.model';
|
||||
import { SubmissionModel } from '../../dto/submission/submission.model';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { FormService } from '../../service/form/form.service';
|
||||
import { SubmissionService } from '../../service/submission/submission.service';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Resolver(() => PagerSubmissionModel)
|
||||
export class SubmissionSearchResolver {
|
||||
constructor(
|
||||
private readonly formService: FormService,
|
||||
private readonly submissionService: SubmissionService,
|
||||
) {
|
||||
}
|
||||
|
||||
@Query(() => PagerSubmissionModel)
|
||||
async listSubmissions(
|
||||
@User() user: UserDocument,
|
||||
@Args('form', {type: () => ID}) id: string,
|
||||
@Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start,
|
||||
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<PagerSubmissionModel> {
|
||||
const form = await this.formService.findById(id)
|
||||
|
||||
const [submissions, total] = await this.submissionService.find(
|
||||
form,
|
||||
start,
|
||||
limit,
|
||||
{},
|
||||
)
|
||||
|
||||
submissions.forEach(submission => cache.addSubmission(submission))
|
||||
|
||||
return new PagerSubmissionModel(
|
||||
submissions.map(submission => new SubmissionModel(submission)),
|
||||
total,
|
||||
limit,
|
||||
start,
|
||||
)
|
||||
}
|
||||
}
|
||||
38
src/resolver/submission/submission.set.field.mutation.ts
Normal file
38
src/resolver/submission/submission.set.field.mutation.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Args, Context, ID, Mutation } from '@nestjs/graphql';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { SubmissionProgressModel } from '../../dto/submission/submission.progress.model';
|
||||
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { SubmissionService } from '../../service/submission/submission.service';
|
||||
import { SubmissionSetFieldService } from '../../service/submission/submission.set.field.service';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionSetFieldMutation {
|
||||
constructor(
|
||||
private readonly submissionService: SubmissionService,
|
||||
private readonly setFieldService: SubmissionSetFieldService,
|
||||
) {
|
||||
}
|
||||
|
||||
@Mutation(() => SubmissionProgressModel)
|
||||
async submissionSetField(
|
||||
@User() user: UserDocument,
|
||||
@Args({ name: 'submission', type: () => ID }) id: string,
|
||||
@Args({ name: 'field', type: () => SubmissionSetFieldInput }) input: SubmissionSetFieldInput,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<SubmissionProgressModel> {
|
||||
const submission = await this.submissionService.findById(id)
|
||||
|
||||
if (!await this.submissionService.isOwner(submission, input.token)) {
|
||||
throw new Error('no access to submission')
|
||||
}
|
||||
|
||||
await this.setFieldService.saveField(submission, input)
|
||||
|
||||
cache.addSubmission(submission)
|
||||
|
||||
return new SubmissionProgressModel(submission)
|
||||
}
|
||||
}
|
||||
34
src/resolver/submission/submission.start.mutation.ts
Normal file
34
src/resolver/submission/submission.start.mutation.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Args, Context, ID, Mutation } from '@nestjs/graphql';
|
||||
import { User } from '../../decorator/user.decorator';
|
||||
import { SubmissionProgressModel } from '../../dto/submission/submission.progress.model';
|
||||
import { SubmissionStartInput } from '../../dto/submission/submission.start.input';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { FormService } from '../../service/form/form.service';
|
||||
import { SubmissionStartService } from '../../service/submission/submission.start.service';
|
||||
import { ContextCache } from '../context.cache';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionStartMutation {
|
||||
constructor(
|
||||
private readonly startService: SubmissionStartService,
|
||||
private readonly formService: FormService,
|
||||
) {
|
||||
}
|
||||
|
||||
@Mutation(() => SubmissionProgressModel)
|
||||
async submissionStart(
|
||||
@User() user: UserDocument,
|
||||
@Args({ name: 'form', type: () => ID }) id: string,
|
||||
@Args({ name: 'submission', type: () => SubmissionStartInput }) input: SubmissionStartInput,
|
||||
@Context('cache') cache: ContextCache,
|
||||
): Promise<SubmissionProgressModel> {
|
||||
const form = await this.formService.findById(id)
|
||||
|
||||
const submission = await this.startService.start(form, input, user)
|
||||
|
||||
cache.addSubmission(submission)
|
||||
|
||||
return new SubmissionProgressModel(submission)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Schema, Document } from 'mongoose';
|
||||
import { Document, Schema } from 'mongoose';
|
||||
import { matchType } from '../config/fields';
|
||||
|
||||
export interface ButtonDocument extends Document{
|
||||
@ -6,6 +6,7 @@ export interface ButtonDocument extends Document{
|
||||
readonly action?: string
|
||||
readonly text?: string
|
||||
readonly bgColor?: string
|
||||
readonly activeColor?: string
|
||||
readonly color?: string
|
||||
}
|
||||
|
||||
@ -23,11 +24,16 @@ export const ButtonSchema = new Schema({
|
||||
bgColor: {
|
||||
type: String,
|
||||
match: matchType.color,
|
||||
default: '#5bc0de',
|
||||
default: '#fff',
|
||||
},
|
||||
activeColor: {
|
||||
type: String,
|
||||
match: matchType.color,
|
||||
default: '#40a9ff',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
match: matchType.color,
|
||||
default: '#ffffff'
|
||||
default: '#666'
|
||||
},
|
||||
})
|
||||
|
||||
@ -37,6 +37,7 @@ export interface Colors {
|
||||
readonly questionColor: string
|
||||
readonly answerColor: string
|
||||
readonly buttonColor: string
|
||||
readonly buttonActiveColor: string
|
||||
readonly buttonTextColor: string
|
||||
}
|
||||
|
||||
@ -93,6 +94,14 @@ export const FormSchema = new Schema({
|
||||
default: defaultLanguage,
|
||||
required: true,
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
analytics: {
|
||||
gaCode: {
|
||||
type: String,
|
||||
@ -193,14 +202,6 @@ export const FormSchema = new Schema({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
design: {
|
||||
colors: {
|
||||
backgroundColor: {
|
||||
@ -223,10 +224,15 @@ export const FormSchema = new Schema({
|
||||
match: matchType.color,
|
||||
default: '#fff'
|
||||
},
|
||||
buttonActiveColor: {
|
||||
type: String,
|
||||
match: matchType.color,
|
||||
default: '#40a9ff'
|
||||
},
|
||||
buttonTextColor: {
|
||||
type: String,
|
||||
match: matchType.color,
|
||||
default: '#333'
|
||||
default: '#666'
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FormFieldDefinition } from './form.field.schema';
|
||||
import { FormDefinition } from './form.schema';
|
||||
import { SubmissionFieldDefinition } from './submission.field.schema';
|
||||
import { SubmissionDefinition } from './submission.schema';
|
||||
import { UserDefinition } from './user.schema';
|
||||
|
||||
@ -7,5 +8,6 @@ export const schema = [
|
||||
FormDefinition,
|
||||
FormFieldDefinition,
|
||||
SubmissionDefinition,
|
||||
SubmissionFieldDefinition,
|
||||
UserDefinition,
|
||||
]
|
||||
|
||||
@ -5,12 +5,12 @@ import { FormFieldDocument, FormFieldSchemaName } from './form.field.schema';
|
||||
export const SubmissionFieldSchemaName = 'SubmissionField'
|
||||
|
||||
export interface SubmissionFieldDocument extends Document {
|
||||
field: FormFieldDocument
|
||||
fieldType: string
|
||||
fieldValue: any
|
||||
readonly field: FormFieldDocument
|
||||
readonly fieldType: string
|
||||
readonly fieldValue: any
|
||||
}
|
||||
|
||||
export const SubmissionFormFieldSchema = new Schema({
|
||||
export const SubmissionFieldSchema = new Schema({
|
||||
field: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: FormFieldSchemaName
|
||||
@ -24,3 +24,9 @@ export const SubmissionFormFieldSchema = new Schema({
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
export const SubmissionFieldDefinition = {
|
||||
name: SubmissionFieldSchemaName,
|
||||
schema: SubmissionFieldSchema,
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,38 @@
|
||||
import { Document, Schema } from 'mongoose';
|
||||
import { FormSchemaName } from './form.schema';
|
||||
import { SubmissionFieldDocument, SubmissionFieldSchemaName } from './submission.field.schema';
|
||||
import { FormDocument, FormSchemaName } from './form.schema';
|
||||
import { SubmissionFieldDocument, SubmissionFieldSchema } from './submission.field.schema';
|
||||
import { UserDocument, UserSchemaName } from './user.schema';
|
||||
|
||||
export const SubmissionSchemaName = 'FormSubmission'
|
||||
export const SubmissionSchemaName = 'Submission'
|
||||
|
||||
export interface GeoLocation {
|
||||
readonly country?: string
|
||||
readonly city?: string
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
readonly type?: string
|
||||
readonly name?: string
|
||||
}
|
||||
|
||||
export interface SubmissionDocument extends Document {
|
||||
fields: SubmissionFieldDocument[]
|
||||
readonly fields: SubmissionFieldDocument[]
|
||||
readonly form: FormDocument
|
||||
readonly ipAddr: string
|
||||
readonly tokenHash: string
|
||||
readonly geoLocation: GeoLocation
|
||||
readonly device: Device
|
||||
readonly timeElapsed: number
|
||||
readonly percentageComplete: number
|
||||
|
||||
readonly user?: UserDocument
|
||||
readonly created: Date
|
||||
readonly lastModified: Date
|
||||
}
|
||||
|
||||
export const SubmissionSchema = new Schema({
|
||||
fields: {
|
||||
alias: 'form_fields',
|
||||
type: [SubmissionFieldSchemaName],
|
||||
type: [SubmissionFieldSchema],
|
||||
default: [],
|
||||
},
|
||||
form: {
|
||||
@ -19,14 +40,21 @@ export const SubmissionSchema = new Schema({
|
||||
ref: FormSchemaName,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: UserSchemaName,
|
||||
},
|
||||
ipAddr: {
|
||||
type: String
|
||||
},
|
||||
geoLocation: {
|
||||
Country: {
|
||||
tokenHash: {
|
||||
type: String
|
||||
},
|
||||
City: {
|
||||
geoLocation: {
|
||||
country: {
|
||||
type: String
|
||||
},
|
||||
city: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
@ -39,10 +67,12 @@ export const SubmissionSchema = new Schema({
|
||||
}
|
||||
},
|
||||
timeElapsed: {
|
||||
type: Number
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
percentageComplete: {
|
||||
type: Number
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
@ -4,21 +4,19 @@ 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';
|
||||
import { FormUpdateService } from './form.update.service';
|
||||
|
||||
@Injectable()
|
||||
export class FormCreateService {
|
||||
constructor(
|
||||
@InjectModel(FormSchemaName) private readonly formModel: Model<FormDocument>,
|
||||
private readonly updateService: FormUpdateService,
|
||||
) {
|
||||
}
|
||||
|
||||
async create(admin: UserDocument, input: FormCreateInput): Promise<FormDocument> {
|
||||
const form = await this.formModel.create({
|
||||
admin
|
||||
return await this.formModel.create({
|
||||
admin,
|
||||
...input,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return await this.updateService.update(form, input)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model, Types } from 'mongoose';
|
||||
import { FilterQuery, Model, Types } from 'mongoose';
|
||||
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
|
||||
@ -19,8 +19,16 @@ 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()
|
||||
async find(start: number, limit: number, sort: any = {}, user?: UserDocument): Promise<[FormDocument[], number]> {
|
||||
let conditions: FilterQuery<FormDocument>
|
||||
|
||||
if (user) {
|
||||
conditions = {
|
||||
admin: user
|
||||
}
|
||||
}
|
||||
|
||||
const qb = this.formModel.find(conditions)
|
||||
|
||||
// TODO apply restrictions based on user!
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@ import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||
@Injectable()
|
||||
export class FormUpdateService {
|
||||
constructor(
|
||||
@InjectModel(FormSchemaName) private formModel: Model<FormDocument>,
|
||||
@InjectModel(FormFieldSchemaName) private formFieldModel: Model<FormFieldDocument>,
|
||||
@InjectModel(FormSchemaName) private readonly formModel: Model<FormDocument>,
|
||||
@InjectModel(FormFieldSchemaName) private readonly formFieldModel: Model<FormFieldDocument>,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export class FormUpdateService {
|
||||
let field = form.fields.find(field => field.id.toString() === nextField.id)
|
||||
|
||||
if (!field) {
|
||||
field = await this.formFieldModel.create({
|
||||
field = new this.formFieldModel({
|
||||
type: nextField.type,
|
||||
})
|
||||
}
|
||||
|
||||
@ -6,12 +6,14 @@ import { PinoLogger } from 'nestjs-pino/dist';
|
||||
import { authServices } from './auth';
|
||||
import { formServices } from './form';
|
||||
import { MailService } from './mail.service';
|
||||
import { submissionServices } from './submission';
|
||||
import { userServices } from './user';
|
||||
|
||||
export const services = [
|
||||
...userServices,
|
||||
...formServices,
|
||||
...authServices,
|
||||
...submissionServices,
|
||||
MailService,
|
||||
{
|
||||
provide: 'PUB_SUB',
|
||||
|
||||
11
src/service/submission/index.ts
Normal file
11
src/service/submission/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { SubmissionService } from './submission.service';
|
||||
import { SubmissionSetFieldService } from './submission.set.field.service';
|
||||
import { SubmissionStartService } from './submission.start.service';
|
||||
import { SubmissionTokenService } from './submission.token.service';
|
||||
|
||||
export const submissionServices = [
|
||||
SubmissionSetFieldService,
|
||||
SubmissionStartService,
|
||||
SubmissionService,
|
||||
SubmissionTokenService,
|
||||
]
|
||||
40
src/service/submission/submission.service.ts
Normal file
40
src/service/submission/submission.service.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { FormDocument } from '../../schema/form.schema';
|
||||
import { SubmissionDocument, SubmissionSchemaName } from '../../schema/submission.schema';
|
||||
import { SubmissionTokenService } from './submission.token.service';
|
||||
|
||||
export class SubmissionService {
|
||||
constructor(
|
||||
@InjectModel(SubmissionSchemaName) private readonly submissionModel: Model<SubmissionDocument>,
|
||||
private readonly tokenService: SubmissionTokenService
|
||||
) {
|
||||
}
|
||||
|
||||
async isOwner(submission: SubmissionDocument, token: string): Promise<boolean> {
|
||||
return await this.tokenService.verify(token, submission.tokenHash)
|
||||
}
|
||||
|
||||
async find(form: FormDocument, start: number, limit: number, sort: any = {}): Promise<[SubmissionDocument[], number]> {
|
||||
const qb = this.submissionModel.find({
|
||||
form
|
||||
})
|
||||
|
||||
return [
|
||||
await qb.sort(sort)
|
||||
.skip(start)
|
||||
.limit(limit),
|
||||
await qb.count()
|
||||
]
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<SubmissionDocument> {
|
||||
const submission = await this.submissionModel.findById(id);
|
||||
|
||||
if (!submission) {
|
||||
throw new Error('no form found')
|
||||
}
|
||||
|
||||
return submission
|
||||
}
|
||||
}
|
||||
49
src/service/submission/submission.set.field.service.ts
Normal file
49
src/service/submission/submission.set.field.service.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import dayjs from 'dayjs';
|
||||
import { Model } from 'mongoose';
|
||||
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input';
|
||||
import { SubmissionFieldDocument, SubmissionFieldSchemaName } from '../../schema/submission.field.schema';
|
||||
import { SubmissionDocument, SubmissionSchemaName } from '../../schema/submission.schema';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionSetFieldService {
|
||||
constructor(
|
||||
@InjectModel(SubmissionSchemaName) private readonly submissionModel: Model<SubmissionDocument>,
|
||||
@InjectModel(SubmissionFieldSchemaName) private readonly submissionFieldModel: Model<SubmissionFieldDocument>,
|
||||
) {
|
||||
}
|
||||
|
||||
async saveField(submission: SubmissionDocument, input: SubmissionSetFieldInput) {
|
||||
const existing = submission.fields.find(field => field.field.toString() === input.field)
|
||||
|
||||
const data = JSON.parse(input.data)
|
||||
|
||||
if (existing) {
|
||||
existing.set('fieldValue', data)
|
||||
} else {
|
||||
if (!submission.populated('form')) {
|
||||
submission.populate('form')
|
||||
await submission.execPopulate()
|
||||
}
|
||||
|
||||
const field = submission.form.fields.find(field => field.id.toString() === input.field)
|
||||
|
||||
const newField = new this.submissionFieldModel({
|
||||
field,
|
||||
fieldType: field.type,
|
||||
fieldValue: data
|
||||
})
|
||||
|
||||
submission.set('percentageComplete', (1 + submission.fields.length) / submission.form.fields.length)
|
||||
submission.set('timeElapsed', dayjs().diff(dayjs(submission.created), 'second'))
|
||||
|
||||
submission.set('fields', [
|
||||
...submission.fields,
|
||||
newField,
|
||||
])
|
||||
}
|
||||
|
||||
await submission.save()
|
||||
}
|
||||
}
|
||||
30
src/service/submission/submission.start.service.ts
Normal file
30
src/service/submission/submission.start.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { SubmissionStartInput } from '../../dto/submission/submission.start.input';
|
||||
import { FormDocument } from '../../schema/form.schema';
|
||||
import { SubmissionDocument, SubmissionSchemaName } from '../../schema/submission.schema';
|
||||
import { UserDocument } from '../../schema/user.schema';
|
||||
import { SubmissionTokenService } from './submission.token.service';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionStartService {
|
||||
constructor(
|
||||
@InjectModel(SubmissionSchemaName) private submissionModel: Model<SubmissionDocument>,
|
||||
private readonly tokenService: SubmissionTokenService
|
||||
) {
|
||||
}
|
||||
|
||||
async start(
|
||||
form: FormDocument,
|
||||
input: SubmissionStartInput,
|
||||
user?: UserDocument,
|
||||
): Promise<SubmissionDocument> {
|
||||
return await this.submissionModel.create({
|
||||
form,
|
||||
device: input.device,
|
||||
user,
|
||||
tokenHash: await this.tokenService.hash(input.token)
|
||||
})
|
||||
}
|
||||
}
|
||||
12
src/service/submission/submission.token.service.ts
Normal file
12
src/service/submission/submission.token.service.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class SubmissionTokenService {
|
||||
async hash(token: string): Promise<string> {
|
||||
return token
|
||||
}
|
||||
|
||||
async verify(token: string, hash: string): Promise<boolean> {
|
||||
return token == hash
|
||||
}
|
||||
}
|
||||
@ -3355,6 +3355,11 @@ dayjs@^1.8.16:
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.26.tgz#c6d62ccdf058ca72a8d14bb93a23501058db9f1e"
|
||||
integrity sha512-KqtAuIfdNfZR5sJY1Dixr2Is4ZvcCqhb0dZpCOt5dGEFiMzoIbjkTSzUb4QKTCsP+WNpGwUjAFIZrnZvUxxkhw==
|
||||
|
||||
dayjs@^1.8.28:
|
||||
version "1.8.28"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.28.tgz#37aa6201df483d089645cb6c8f6cef6f0c4dbc07"
|
||||
integrity sha512-ccnYgKC0/hPSGXxj7Ju6AV/BP4HUkXC2u15mikXT5mX9YorEaoi1bEKOmAqdkJHN4EEkmAf97SpH66Try5Mbeg==
|
||||
|
||||
debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user