update for sql based backend

This commit is contained in:
Michael Schramm 2021-05-01 10:32:51 +02:00
parent 93b6596b1b
commit 356ebb80c1
25 changed files with 269 additions and 87 deletions

View File

@ -6,6 +6,9 @@
| SECRET_KEY | `changeMe` | JWT Secret for authentication |
| CLI | *automatically* | activates pretty print for log output |
| NODE_ENV | `production` | |
| HIDE_CONTRIB | `false` | decide if backlings to ohmyform should be added |
| SIGNUP_DISABLED | `false` | if users can sign up |
| LOGIN_NOTE | *not set* | Info box on top of login screen |
## Mailing

View File

@ -1,7 +1,10 @@
import { Field, InputType } from '@nestjs/graphql'
import { Field, ID, InputType } from '@nestjs/graphql'
@InputType()
export class ButtonInput {
@Field(() => ID, { nullable: true })
readonly id?: string
@Field({ nullable: true })
readonly url?: string

View File

@ -1,8 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql'
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { PageButtonEntity } from '../../entity/page.button.entity'
@ObjectType('Button')
export class ButtonModel {
@Field(() => ID)
readonly id: string
@Field({ nullable: true })
readonly url?: string
@ -22,6 +25,7 @@ export class ButtonModel {
readonly color?: string
constructor(button: Partial<PageButtonEntity>) {
this.id = button.id.toString()
this.url = button.url
this.action = button.action
this.text = button.text

View File

@ -22,11 +22,11 @@ export class ColorsModel {
readonly buttonText: string
constructor(partial: Partial<ColorsEmbedded>) {
this.background = partial.background
this.question = partial.question
this.answer = partial.answer
this.button = partial.button
this.buttonActive = partial.buttonActive
this.buttonText = partial.buttonText
this.background = partial.background ?? '#fff'
this.question = partial.question ?? '#333'
this.answer = partial.answer ?? '#333'
this.button = partial.button ?? '#fff'
this.buttonActive = partial.buttonActive ?? '#40a9ff'
this.buttonText = partial.buttonText ?? '#666'
}
}

View File

@ -27,6 +27,9 @@ export class FormFieldInput {
@Field()
readonly value: string
@Field({ nullable: true })
readonly disabled?: boolean
@Field(() => [FormFieldOptionInput], { nullable: true })
readonly options: FormFieldOptionInput[]

View File

@ -2,6 +2,9 @@ import { Field, ID, InputType } from '@nestjs/graphql'
@InputType()
export class FormFieldLogicInput {
@Field(() => ID, { nullable: true })
readonly id?: string
@Field({ nullable: true })
readonly formula: string

View File

@ -1,7 +1,10 @@
import { Field, InputType } from '@nestjs/graphql'
import { Field, ID, InputType } from '@nestjs/graphql'
@InputType()
export class FormFieldOptionInput {
@Field(() => ID, { nullable: true })
readonly id?: string
@Field({ nullable: true })
readonly key: string

View File

@ -1,8 +1,11 @@
import { Field, ObjectType } from '@nestjs/graphql'
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { FormFieldOptionEntity } from '../../entity/form.field.option.entity'
@ObjectType('FormFieldOption')
export class FormFieldOptionModel {
@Field(() => ID)
readonly id: string
@Field({ nullable: true })
readonly key: string
@ -13,6 +16,7 @@ export class FormFieldOptionModel {
readonly value: string
constructor(option: FormFieldOptionEntity) {
this.id = option.id.toString()
this.key = option.key
this.title = option.title
this.value = option.value

View File

@ -1,7 +1,10 @@
import { Field, InputType } from '@nestjs/graphql'
import { Field, ID, InputType } from '@nestjs/graphql'
@InputType('FormNotificationInput')
export class FormNotificationInput {
@Field(() => ID, { nullable: true })
readonly id?: string
@Field({ nullable: true })
readonly subject?: string

View File

@ -1,8 +1,11 @@
import { Field, InterfaceType } from '@nestjs/graphql'
import { Field, ID, InterfaceType, ObjectType } from '@nestjs/graphql'
import { FormNotificationEntity } from '../../entity/form.notification.entity'
@InterfaceType('FormNotification')
@ObjectType('FormNotification')
export class FormNotificationModel {
@Field(() => ID)
readonly id: string
@Field({ nullable: true })
readonly subject?: string
@ -25,6 +28,7 @@ export class FormNotificationModel {
readonly enabled: boolean
constructor(partial: Partial<FormNotificationEntity>) {
this.id = partial.id.toString()
this.subject = partial.subject
this.htmlTemplate = partial.htmlTemplate
this.enabled = partial.enabled

View File

@ -1,8 +1,11 @@
import { Field, InputType } from '@nestjs/graphql'
import { Field, ID, InputType } from '@nestjs/graphql'
import { ButtonInput } from './button.input'
@InputType()
export class PageInput {
@Field(() => ID, { nullable: true })
readonly id?: string
@Field()
readonly show: boolean

View File

@ -1,9 +1,13 @@
import { Field, ObjectType } from '@nestjs/graphql'
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { randomBytes } from 'crypto'
import { PageEntity } from '../../entity/page.entity'
import { ButtonModel } from './button.model'
@ObjectType('Page')
export class PageModel {
@Field(() => ID)
readonly id: string
@Field()
readonly show: boolean
@ -20,10 +24,18 @@ export class PageModel {
readonly buttons: ButtonModel[]
constructor(page: Partial<PageEntity>) {
if (!page) {
this.id = Math.random().toString()
this.show = false
this.buttons = []
return
}
this.id = page.id.toString()
this.show = page.show
this.title = page.title
this.paragraph = page.paragraph
this.buttonText = page.buttonText
this.buttons = page.buttons.map(button => new ButtonModel(button))
this.buttons = (page.buttons || []).map(button => new ButtonModel(button))
}
}

View File

@ -37,22 +37,22 @@ export class FormEntity {
@OneToMany(() => SubmissionEntity, submission => submission.form)
public submissions: SubmissionEntity[]
@OneToMany(() => FormFieldEntity, field => field.form, { eager: true, orphanedRowAction: 'delete' })
@OneToMany(() => FormFieldEntity, field => field.form, { eager: true, orphanedRowAction: 'delete', cascade: true })
public fields: FormFieldEntity[]
@OneToMany(() => FormHookEntity, field => field.form, { eager: true, orphanedRowAction: 'delete' })
@OneToMany(() => FormHookEntity, field => field.form, { eager: true, orphanedRowAction: 'delete', cascade: true })
public hooks: FormHookEntity[]
@ManyToOne(() => UserEntity, { eager: true })
public admin: UserEntity
@ManyToOne(() => PageEntity, { eager: true })
@ManyToOne(() => PageEntity, { eager: true, cascade: true })
public startPage: PageEntity;
@ManyToOne(() => PageEntity, { eager: true })
@ManyToOne(() => PageEntity, { eager: true, cascade: true })
public endPage: PageEntity;
@OneToMany(() => FormNotificationEntity, notification => notification.form, { eager: true, orphanedRowAction: 'delete' })
@OneToMany(() => FormNotificationEntity, notification => notification.form, { eager: true, orphanedRowAction: 'delete', cascade: true })
public notifications: FormNotificationEntity[]
@Column()

View File

@ -33,8 +33,8 @@ export class FormFieldEntity {
@Column()
public required: boolean
@Column()
public disabled: boolean
@Column({ type: 'boolean' })
public disabled = false
@Column()
public type: string

View File

@ -11,23 +11,23 @@ export class FormNotificationEntity {
public form: FormEntity
@Column({ nullable: true })
readonly subject?: string
public subject?: string
@Column({ nullable: true })
readonly htmlTemplate?: string
public htmlTemplate?: string
@Column()
readonly enabled: boolean
public enabled: boolean
@ManyToOne(() => FormFieldEntity)
readonly fromField?: FormFieldEntity
public fromField?: FormFieldEntity
@ManyToOne(() => FormFieldEntity)
readonly toField?: FormFieldEntity
public toField?: FormFieldEntity
@Column({ nullable: true })
readonly toEmail?: string
public toEmail?: string
@Column({ nullable: true })
readonly fromEmail?: string
public fromEmail?: string
}

View File

@ -10,20 +10,20 @@ export class PageButtonEntity {
public page: PageEntity
@Column({ nullable: true })
readonly url?: string
public url?: string
@Column({ nullable: true })
readonly action?: string
public action?: string
@Column()
readonly text: string
public text: string
@Column({ nullable: true })
readonly bgColor?: string
public bgColor?: string
@Column({ nullable: true })
readonly activeColor?: string
public activeColor?: string
@Column({ nullable: true })
readonly color?: string
public color?: string
}

View File

@ -7,17 +7,17 @@ export class PageEntity {
public id: number
@Column()
readonly show: boolean
public show: boolean
@Column({ nullable: true })
readonly title?: string
public title?: string
@Column({ type: 'text', nullable: true })
readonly paragraph?: string
public paragraph?: string
@Column({ nullable: true })
readonly buttonText?: string
public buttonText?: string
@OneToMany(() => PageButtonEntity, button => button.page)
readonly buttons: PageButtonEntity[]
@OneToMany(() => PageButtonEntity, button => button.page, { eager: true, orphanedRowAction: 'delete', cascade: true })
public buttons: PageButtonEntity[]
}

View File

@ -14,7 +14,7 @@ import { FormService } from '../../service/form/form.service'
import { ContextCache } from '../context.cache'
@Resolver(() => FormModel)
export class FormResolver {
export class FormQuery {
constructor(
private readonly formService: FormService,
) {

View File

@ -113,11 +113,6 @@ export class FormResolver {
): Promise<UserModel> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
if (!form.populated('admin')) {
form.populate('admin')
await form.execPopulate()
}
if (!form.admin) {
return null
}

View File

@ -1,5 +1,6 @@
import { Args, Context, Query, Resolver } from '@nestjs/graphql'
import { GraphQLInt } from 'graphql'
import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
import { FormModel } from '../../dto/form/form.model'
import { FormPagerModel } from '../../dto/form/form.pager.model'
@ -16,12 +17,13 @@ export class FormSearchResolver {
}
@Query(() => FormPagerModel)
@Roles('user')
async listForms(
@User() user: UserEntity,
@Args('start', {type: () => GraphQLInt, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => GraphQLInt, defaultValue: 50, nullable: true}) limit: number,
@Context('cache') cache: ContextCache,
) {
): Promise<FormPagerModel> {
const [forms, total] = await this.formService.find(
start,
limit,

View File

@ -1,5 +1,6 @@
import { FormCreateMutation } from './form.create.mutation'
import { FormDeleteMutation } from './form.delete.mutation'
import { FormQuery } from './form.query'
import { FormResolver } from './form.resolver'
import { FormSearchResolver } from './form.search.resolver'
import { FormStatisticResolver } from './form.statistic.resolver'
@ -8,6 +9,7 @@ import { FormUpdateMutation } from './form.update.mutation'
export const formResolvers = [
FormCreateMutation,
FormDeleteMutation,
FormQuery,
FormResolver,
FormSearchResolver,
FormStatisticResolver,

View File

@ -36,6 +36,8 @@ export class AuthService {
username: user.username,
roles: user.roles,
sub: user.id,
}, {
expiresIn: '4h',
}),
refreshToken: this.jwtService.sign({
sub: user.id,

View File

@ -17,9 +17,9 @@ export class FormCreateService {
const form = new FormEntity()
form.title = input.title
form.isLive = input.isLive
form.showFooter = input.showFooter
form.language = input.language
form.isLive = Boolean(input.isLive)
form.showFooter = Boolean(input.showFooter)
form.language = input.language || 'en'
form.admin = admin

View File

@ -13,6 +13,10 @@ export class FormService {
}
async isAdmin(form: FormEntity, user: UserEntity): Promise<boolean> {
if (!user) {
return false
}
if (user.roles.includes('superuser')) {
return true
}
@ -23,6 +27,8 @@ export class FormService {
async find(start: number, limit: number, sort: any = {}, user?: UserEntity): Promise<[FormEntity[], number]> {
const qb = this.formRepository.createQueryBuilder('f')
qb.leftJoinAndSelect('f.admin', 'a')
if (user) {
qb.where('f.admin = :user', { user: user.id })
}

View File

@ -4,7 +4,12 @@ import { Repository } from 'typeorm'
import { FormUpdateInput } from '../../dto/form/form.update.input'
import { FormEntity } from '../../entity/form.entity'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { FormFieldLogicEntity } from '../../entity/form.field.logic.entity'
import { FormFieldOptionEntity } from '../../entity/form.field.option.entity'
import { FormHookEntity } from '../../entity/form.hook.entity'
import { FormNotificationEntity } from '../../entity/form.notification.entity'
import { PageButtonEntity } from '../../entity/page.button.entity'
import { PageEntity } from '../../entity/page.entity'
@Injectable()
export class FormUpdateService {
@ -35,36 +40,83 @@ export class FormUpdateService {
form.isLive = input.isLive
}
const fieldMapping = {}
if (input.fields !== undefined) {
form.fields = await Promise.all(input.fields.map(async (nextField) => {
let field = form.fields.find(field => field.id.toString() === nextField.id)
let field = form.fields.find(field => field.id?.toString() === nextField.id)
if (!field) {
field = new FormFieldEntity()
field.type = nextField.type
}
// ability for other fields to apply mapping
fieldMapping[nextField.id] = field.id.toString()
if (nextField.title !== undefined) {
field.title = nextField.title
}
if (nextField.description !== undefined) {
field.description = nextField.description
}
if (nextField.disabled !== undefined) {
field.disabled = nextField.disabled
}
if (nextField.required !== undefined) {
field.required = nextField.required
}
if (nextField.value !== undefined) {
field.value = nextField.value
}
if (nextField.slug !== undefined) {
field.slug = nextField.slug
}
if (nextField.logic !== undefined) {
// TODO prepare logic entries
// field.logicJump = nextField.logicJump
field.logic = nextField.logic.map(nextLogic => {
const logic = field.logic?.find(logic => logic.id?.toString() === nextLogic.id) || new FormFieldLogicEntity()
logic.field = field
if (nextLogic.formula !== undefined) {
logic.formula = nextLogic.formula
}
if (nextLogic.action !== undefined) {
logic.action = nextLogic.action as any
}
if (nextLogic.visible !== undefined) {
logic.visible = nextLogic.visible
}
if (nextLogic.require !== undefined) {
logic.require = nextLogic.require
}
if (nextLogic.disable !== undefined) {
logic.disable = nextLogic.disable
}
if (nextLogic.jumpTo !== undefined) {
logic.jumpTo = form.fields.find(value => value.id?.toString() === nextLogic.jumpTo)
}
if (nextLogic.enabled !== undefined) {
logic.enabled = nextLogic.enabled
}
return logic
})
}
if (nextField.options !== undefined) {
// TODO prepare options
// field.options = nextField.options
field.options = nextField.options.map(nextOption => {
const option = field.options?.find(option => option.id?.toString() === nextOption.id) || new FormFieldOptionEntity()
option.field = field
option.title = nextOption.title
option.value = nextOption.value
option.key = nextOption.key
return option
})
}
if (nextField.rating !== undefined) {
@ -78,11 +130,7 @@ export class FormUpdateService {
if (input.hooks !== undefined) {
form.hooks = input.hooks.map((nextHook) => {
let hook = form.hooks && form.hooks.find(hook => hook.id.toString() === nextHook.id)
if (!hook) {
hook = new FormHookEntity()
}
const hook = form.hooks?.find(hook => hook.id?.toString() === nextHook.id) || new FormHookEntity()
// ability for other fields to apply mapping
hook.url = nextHook.url
@ -97,14 +145,6 @@ export class FormUpdateService {
}
const extractField = (id) => {
if (id && fieldMapping[id]) {
return fieldMapping[id]
}
return null
}
if (input.design !== undefined) {
if (input.design.font !== undefined) {
form.design.font = input.design.font
@ -132,14 +172,38 @@ export class FormUpdateService {
}
}
/*
if (input.selfNotifications !== undefined) {
form.set('selfNotifications', {
...input.selfNotifications,
fromField: extractField(input.selfNotifications.fromField)
if (input.notifications !== undefined) {
form.notifications = input.notifications.map(notificationInput => {
const notification = form.notifications?.find(value => value.id?.toString() === notificationInput.id) || new FormNotificationEntity()
notification.form = form
notification.enabled = notificationInput.enabled
if (notificationInput.fromEmail !== undefined) {
notification.fromEmail = notificationInput.fromEmail
}
if (notificationInput.fromField !== undefined) {
notification.fromField = form.fields.find(value => value.id?.toString() === notificationInput.fromField)
}
if (notificationInput.subject !== undefined) {
notification.subject = notificationInput.subject
}
if (notificationInput.htmlTemplate !== undefined) {
notification.htmlTemplate = notificationInput.htmlTemplate
}
if (notificationInput.toEmail !== undefined) {
notification.toEmail = notificationInput.toEmail
}
if (notificationInput.toField !== undefined) {
notification.toField = form.fields.find(value => value.id?.toString() === notificationInput.toField)
}
return notification
})
}
/*
if (input.respondentNotifications !== undefined) {
form.set('respondentNotifications', {
...input.respondentNotifications,
@ -149,13 +213,79 @@ export class FormUpdateService {
*/
if (input.startPage !== undefined) {
// TODO fix start page
// form.set('startPage', input.startPage)
if (!form.startPage) {
form.startPage = new PageEntity()
form.startPage.show = false
}
if (input.startPage.show !== undefined) {
form.startPage.show = input.startPage.show
}
if (input.startPage.title !== undefined) {
form.startPage.title = input.startPage.title
}
if (input.startPage.paragraph !== undefined) {
form.startPage.paragraph = input.startPage.paragraph
}
if (input.startPage.buttonText !== undefined) {
form.startPage.buttonText = input.startPage.buttonText
}
if (input.startPage.buttons !== undefined) {
form.startPage.buttons = input.startPage.buttons.map(buttonInput => {
const entity = form.startPage?.buttons?.find(value => value.id?.toString() === buttonInput.id) || new PageButtonEntity()
entity.page = form.startPage
entity.url = buttonInput.url
entity.action = buttonInput.action
entity.text = buttonInput.text
entity.color = buttonInput.color
entity.bgColor = buttonInput.bgColor
entity.activeColor = buttonInput.activeColor
return entity
})
}
}
if (input.endPage !== undefined) {
// TODO fix end page
// form.set('endPage', input.endPage)
if (!form.endPage) {
form.endPage = new PageEntity()
form.endPage.show = false
}
if (input.endPage.show !== undefined) {
form.endPage.show = input.endPage.show
}
if (input.endPage.title !== undefined) {
form.endPage.title = input.endPage.title
}
if (input.endPage.paragraph !== undefined) {
form.endPage.paragraph = input.endPage.paragraph
}
if (input.endPage.buttonText !== undefined) {
form.endPage.buttonText = input.endPage.buttonText
}
if (input.endPage.buttons !== undefined) {
form.endPage.buttons = input.endPage.buttons.map(buttonInput => {
const entity = form.endPage?.buttons?.find(value => value.id?.toString() === buttonInput.id) || new PageButtonEntity()
entity.page = form.endPage
entity.url = buttonInput.url
entity.action = buttonInput.action
entity.text = buttonInput.text
entity.color = buttonInput.color
entity.bgColor = buttonInput.bgColor
entity.activeColor = buttonInput.activeColor
return entity
})
}
}
await this.formRepository.save(form)