add new notification logic

This commit is contained in:
Michael Schramm 2021-05-02 12:03:54 +02:00
parent ddc1a9b378
commit 468ec6c637
24 changed files with 211 additions and 57 deletions

View File

@ -4,7 +4,7 @@
"username": "root",
"password": "root",
"database": "ohmyform",
"synchronize": true,
"synchronize": false,
"logging": false,
"entities": [
"src/entity/**/*.ts"

View File

@ -1,7 +1,7 @@
{
"type": "sqlite",
"database": "data.sqlite",
"synchronize": true,
"synchronize": false,
"logging": false,
"entities": [
"src/entity/**/*.ts"

View File

@ -63,6 +63,7 @@
"pg": "^8.6.0",
"pino-pretty": "^4.7.1",
"reflect-metadata": "^0.1.13",
"request-ip": "^2.1.3",
"rimraf": "^3.0.2",
"rxjs": "^6.6.7",
"sqlite3": "^5.0.2",
@ -81,6 +82,7 @@
"@types/node": "^15.0.1",
"@types/passport-jwt": "^3.0.5",
"@types/passport-local": "^1.0.33",
"@types/request-ip": "^0.0.35",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",

View File

@ -107,6 +107,7 @@ export const imports = [
inject: [ConfigService],
useFactory: (configService: ConfigService): TypeOrmModuleOptions => ({
name: 'ohmyform',
synchronize: false,
type: configService.get<string>('DB_TYPE', 'sqlite') as any,
url: configService.get<string>('DB_URI', 'sqlite://data.sqlite'),
entityPrefix: configService.get<string>('DB_TABLE_PREFIX', ''),

View File

@ -0,0 +1,19 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { GqlExecutionContext } from '@nestjs/graphql'
import { getClientIp } from 'request-ip'
export const IpAddress = createParamDecorator((data: string, ctx: ExecutionContext) => {
let req
if (ctx.getType<string>() === 'graphql') {
req = GqlExecutionContext.create(ctx).getContext().req
} else {
req = ctx.switchToHttp().getRequest()
}
if (req.clientIp) {
return req.clientIp
}
return getClientIp(req)
})

View File

@ -14,7 +14,7 @@ export class SubmissionFieldModel {
constructor(field: SubmissionFieldEntity) {
this.id = field.id.toString()
this.value = JSON.stringify(field.fieldValue)
this.value = field.fieldValue
this.type = field.fieldType
}
}

View File

@ -1,4 +1,4 @@
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { Field, Float, ID, ObjectType } from '@nestjs/graphql'
import { SubmissionEntity } from '../../entity/submission.entity'
import { DeviceModel } from './device.model'
import { GeoLocationModel } from './geo.location.model'

View File

@ -4,7 +4,7 @@ import {
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
PrimaryGeneratedColumn, RelationId,
UpdateDateColumn
} from 'typeorm'
import { DeviceEmbedded } from './embedded/device.embedded'
@ -25,7 +25,10 @@ export class SubmissionEntity {
@ManyToOne(() => FormEntity, form => form.submissions, { eager: true })
public form: FormEntity
@ManyToOne(() => VisitorEntity, visitor => visitor.submissions)
@RelationId('form')
readonly formId: number
@ManyToOne(() => VisitorEntity, visitor => visitor.submissions, { eager: true })
public visitor: VisitorEntity
@Column()
@ -40,10 +43,10 @@ export class SubmissionEntity {
@Column(() => DeviceEmbedded)
public device: DeviceEmbedded = new DeviceEmbedded()
@Column()
@Column({ type: 'numeric' })
public timeElapsed: number
@Column()
@Column({ type: 'numeric' })
public percentageComplete: number
@ManyToOne(() => UserEntity, { eager: true })

View File

@ -1,4 +1,5 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'
import { FormEntity } from './form.entity'
import { FormFieldEntity } from './form.field.entity'
import { SubmissionEntity } from './submission.entity'
@ -13,6 +14,9 @@ export class SubmissionFieldEntity {
@ManyToOne(() => FormFieldEntity, { eager: true })
public field: FormFieldEntity
@RelationId('field')
readonly fieldId: number
@Column()
public fieldType: string

View File

@ -5,17 +5,17 @@ export class initial1619723437787 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (queryRunner.connection.driver.options.type === 'sqlite') {
await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "firstName" varchar, "lastName" varchar, "email" varchar(255) NOT NULL, "username" varchar(255) NOT NULL, "passwordHash" varchar NOT NULL, "salt" varchar, "provider" varchar NOT NULL, "roles" text NOT NULL, "language" varchar NOT NULL, "resetPasswordToken" varchar, "resetPasswordExpires" datetime, "token" varchar, "apiKey" varchar, "created" datetime NOT NULL DEFAULT (datetime('now')), "lastModified" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`);
await queryRunner.query(`CREATE TABLE "page" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "show" boolean NOT NULL, "title" varchar, "paragraph" text, "buttonText" varchar)`);
await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "firstName" varchar, "lastName" varchar, "email" varchar(255) NOT NULL, "username" varchar(255) NOT NULL, "passwordHash" varchar NOT NULL, "salt" varchar, "provider" varchar NOT NULL, "roles" text NOT NULL, "language" varchar NOT NULL, "resetPasswordToken" varchar, "resetPasswordExpires" datetime, "token" varchar, "apiKey" varchar, "created" datetime NOT NULL DEFAULT (datetime('now')), "lastModified" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"))`);
await queryRunner.query(`CREATE TABLE "form_field_logic" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "formula" varchar NOT NULL, "action" varchar(10) NOT NULL, "visible" boolean, "require" boolean, "disable" boolean, "enabled" boolean NOT NULL, "fieldId" integer, "jumpToId" integer, CONSTRAINT "FK_6098b83f6759445d8cfdd03d545" FOREIGN KEY ("fieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_4a8019f2b753cfb3216dc3001a6" FOREIGN KEY ("jumpToId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "form_field_option" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "key" varchar, "title" varchar, "value" varchar NOT NULL, "fieldId" integer, CONSTRAINT "FK_c4484ad12c2c56db31dffdbfe97" FOREIGN KEY ("fieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "form_field" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar NOT NULL, "description" text NOT NULL, "slug" varchar, "required" boolean NOT NULL, "disabled" boolean NOT NULL, "type" varchar NOT NULL, "value" varchar NOT NULL, "formId" integer, "ratingSteps" integer, "ratingShape" varchar, CONSTRAINT "FK_2d83d8a334dd66445db13f92b77" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "form_hook" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enabled" boolean NOT NULL, "url" varchar NOT NULL, "format" varchar, "formId" integer, CONSTRAINT "FK_bbeb4d224d8857fd5a458538a30" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "form_notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "subject" varchar, "htmlTemplate" varchar, "enabled" boolean NOT NULL, "toEmail" varchar, "fromEmail" varchar, "formId" integer, "fromFieldId" integer, "toFieldId" integer, CONSTRAINT "FK_a9ed55144108ded893b502d6321" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_0876741ce2acdaee4553d7a3bbd" FOREIGN KEY ("fromFieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_4915ebae53e09b732322d0ff6ed" FOREIGN KEY ("toFieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "page_button" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "url" varchar, "action" varchar, "text" varchar NOT NULL, "bgColor" varchar, "activeColor" varchar, "color" varchar, "pageId" integer, CONSTRAINT "FK_d9f099286b75fa0034dcd8cf7c2" FOREIGN KEY ("pageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "submission_field" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "fieldType" varchar NOT NULL, "fieldValue" varchar NOT NULL, "submissionId" integer, "fieldId" integer, CONSTRAINT "FK_16fae661ce5b10f27abe2e524a0" FOREIGN KEY ("submissionId") REFERENCES "submission" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5befa92da2370b7eb1cab6ae30a" FOREIGN KEY ("fieldId") REFERENCES "form_field" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "form_visitor" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "referrer" varchar, "ipAddr" varchar NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), "formId" integer, "geoLocationCountry" varchar, "geoLocationCity" varchar, "deviceLanguage" varchar, "deviceType" varchar, "deviceName" varchar, CONSTRAINT "FK_72ade6c3a3e55d1fce94300f8b6" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "submission" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ipAddr" varchar NOT NULL, "tokenHash" varchar NOT NULL, "timeElapsed" integer NOT NULL, "percentageComplete" integer NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "lastModified" datetime NOT NULL DEFAULT (datetime('now')), "formId" integer, "visitorId" integer, "userId" integer, "geoLocationCountry" varchar, "geoLocationCity" varchar, "deviceLanguage" varchar, "deviceType" varchar, "deviceName" varchar, CONSTRAINT "FK_7bd626272858ef6464aa2579094" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_95b73c7faf2c199f005fda5e8c8" FOREIGN KEY ("visitorId") REFERENCES "form_visitor" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_6090e1d5cbf3433ffd14e3b53e7" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "page_button" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "url" varchar, "action" varchar, "text" varchar NOT NULL, "bgColor" varchar, "activeColor" varchar, "color" varchar, "pageId" integer, CONSTRAINT "FK_d9f099286b75fa0034dcd8cf7c2" FOREIGN KEY ("pageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "submission" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ipAddr" varchar NOT NULL, "tokenHash" varchar NOT NULL, "timeElapsed" numeric NOT NULL, "percentageComplete" numeric NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "lastModified" datetime NOT NULL DEFAULT (datetime('now')), "formId" integer, "visitorId" integer, "userId" integer, "geoLocationCountry" varchar, "geoLocationCity" varchar, "deviceLanguage" varchar, "deviceType" varchar, "deviceName" varchar, CONSTRAINT "FK_6090e1d5cbf3433ffd14e3b53e7" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_95b73c7faf2c199f005fda5e8c8" FOREIGN KEY ("visitorId") REFERENCES "form_visitor" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_7bd626272858ef6464aa2579094" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`CREATE TABLE "form" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar NOT NULL, "language" varchar(10) NOT NULL, "showFooter" boolean NOT NULL, "isLive" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "lastModified" datetime NOT NULL DEFAULT (datetime('now')), "adminId" integer, "startPageId" integer, "endPageId" integer, "analyticsGacode" varchar, "designFont" varchar, "designColorsBackground" varchar, "designColorsQuestion" varchar, "designColorsAnswer" varchar, "designColorsButton" varchar, "designColorsButtonactive" varchar, "designColorsButtontext" varchar, CONSTRAINT "FK_a7cb33580bca2b362e5e34fdfcd" FOREIGN KEY ("adminId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_023d9cf1d97e93facc96c86ca70" FOREIGN KEY ("startPageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_e5d158932e43cfbf9958931ee01" FOREIGN KEY ("endPageId") REFERENCES "page" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
} else {
await queryRunner.query(`CREATE TABLE "form_field_logic" ("id" SERIAL NOT NULL, "formula" character varying NOT NULL, "action" character varying(10) NOT NULL, "visible" boolean, "require" boolean, "disable" boolean, "enabled" boolean NOT NULL, "fieldId" integer, "jumpToId" integer, CONSTRAINT "PK_c40e7f583854ff1b60900d8cf1b" PRIMARY KEY ("id"))`);
@ -23,12 +23,12 @@ export class initial1619723437787 implements MigrationInterface {
await queryRunner.query(`CREATE TABLE "form_field" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "description" text NOT NULL, "slug" character varying, "required" boolean NOT NULL, "disabled" boolean NOT NULL, "type" character varying NOT NULL, "value" character varying NOT NULL, "formId" integer, "ratingSteps" integer, "ratingShape" character varying, CONSTRAINT "PK_135904ddb60085b07254ea4f485" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "form_hook" ("id" SERIAL NOT NULL, "enabled" boolean NOT NULL, "url" character varying NOT NULL, "format" character varying, "formId" integer, CONSTRAINT "PK_4b63bd9ff09f7b3e5c4a41fcbec" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "form_notification" ("id" SERIAL NOT NULL, "subject" character varying, "htmlTemplate" character varying, "enabled" boolean NOT NULL, "toEmail" character varying, "fromEmail" character varying, "formId" integer, "fromFieldId" integer, "toFieldId" integer, CONSTRAINT "PK_935306529aed07c9f6628f6e24f" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "submission_field" ("id" SERIAL NOT NULL, "fieldType" character varying NOT NULL, "fieldValue" character varying NOT NULL, "submissionId" integer, "fieldId" integer, CONSTRAINT "PK_5443f5f769fce3107982c16e0b5" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying, "lastName" character varying, "email" character varying(255) NOT NULL, "username" character varying(255) NOT NULL, "passwordHash" character varying NOT NULL, "salt" character varying, "provider" character varying NOT NULL, "roles" character varying(10) NOT NULL, "language" character varying NOT NULL, "resetPasswordToken" character varying, "resetPasswordExpires" TIMESTAMP, "token" character varying, "apiKey" character varying, "created" TIMESTAMP NOT NULL DEFAULT now(), "lastModified" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "form_visitor" ("id" SERIAL NOT NULL, "referrer" character varying, "ipAddr" character varying NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "formId" integer, "geoLocationCountry" character varying, "geoLocationCity" character varying, CONSTRAINT "PK_74224dc63e13cf5cb5f0420e65b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "submission" ("id" SERIAL NOT NULL, "ipAddr" character varying NOT NULL, "tokenHash" character varying NOT NULL, "timeElapsed" integer NOT NULL, "percentageComplete" integer NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "lastModified" TIMESTAMP NOT NULL DEFAULT now(), "formId" integer, "visitorId" integer, "userId" integer, "geoLocationCountry" character varying, "geoLocationCity" character varying, CONSTRAINT "PK_7faa571d0e4a7076e85890c9bd0" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "page_button" ("id" SERIAL NOT NULL, "url" character varying, "action" character varying, "text" character varying NOT NULL, "bgColor" character varying, "activeColor" character varying, "color" character varying, "pageId" integer, CONSTRAINT "PK_6609a75a7d82775aac8af1a591c" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "page" ("id" SERIAL NOT NULL, "show" boolean NOT NULL, "title" character varying, "paragraph" text, "buttonText" character varying, CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "submission_field" ("id" SERIAL NOT NULL, "fieldType" character varying NOT NULL, "fieldValue" character varying NOT NULL, "submissionId" integer, "fieldId" integer, CONSTRAINT "PK_5443f5f769fce3107982c16e0b5" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying, "lastName" character varying, "email" character varying(255) NOT NULL, "username" character varying(255) NOT NULL, "passwordHash" character varying NOT NULL, "salt" character varying, "provider" character varying NOT NULL, "roles" text NOT NULL, "language" character varying NOT NULL, "resetPasswordToken" character varying, "resetPasswordExpires" TIMESTAMP, "token" character varying, "apiKey" character varying, "created" TIMESTAMP NOT NULL DEFAULT now(), "lastModified" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "form_visitor" ("id" SERIAL NOT NULL, "referrer" character varying, "ipAddr" character varying NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "formId" integer, "geoLocationCountry" character varying, "geoLocationCity" character varying, "deviceLanguage" character varying, "deviceType" character varying, "deviceName" character varying, CONSTRAINT "PK_74224dc63e13cf5cb5f0420e65b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "submission" ("id" SERIAL NOT NULL, "ipAddr" character varying NOT NULL, "tokenHash" character varying NOT NULL, "timeElapsed" numeric NOT NULL, "percentageComplete" numeric NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "lastModified" TIMESTAMP NOT NULL DEFAULT now(), "formId" integer, "visitorId" integer, "userId" integer, "geoLocationCountry" character varying, "geoLocationCity" character varying, "deviceLanguage" character varying, "deviceType" character varying, "deviceName" character varying, CONSTRAINT "PK_7faa571d0e4a7076e85890c9bd0" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "form" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "language" character varying(10) NOT NULL, "showFooter" boolean NOT NULL, "isLive" boolean NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "lastModified" TIMESTAMP NOT NULL DEFAULT now(), "adminId" integer, "startPageId" integer, "endPageId" integer, "analyticsGacode" character varying, "designFont" character varying, "designColorsBackground" character varying, "designColorsQuestion" character varying, "designColorsAnswer" character varying, "designColorsButton" character varying, "designColorsButtonactive" character varying, "designColorsButtontext" character varying, CONSTRAINT "PK_8f72b95aa2f8ba82cf95dc7579e" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "form_field_logic" ADD CONSTRAINT "FK_6098b83f6759445d8cfdd03d545" FOREIGN KEY ("fieldId") REFERENCES "form_field"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form_field_logic" ADD CONSTRAINT "FK_4a8019f2b753cfb3216dc3001a6" FOREIGN KEY ("jumpToId") REFERENCES "form_field"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
@ -38,27 +38,16 @@ export class initial1619723437787 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE "form_notification" ADD CONSTRAINT "FK_a9ed55144108ded893b502d6321" FOREIGN KEY ("formId") REFERENCES "form"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form_notification" ADD CONSTRAINT "FK_0876741ce2acdaee4553d7a3bbd" FOREIGN KEY ("fromFieldId") REFERENCES "form_field"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form_notification" ADD CONSTRAINT "FK_4915ebae53e09b732322d0ff6ed" FOREIGN KEY ("toFieldId") REFERENCES "form_field"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "page_button" ADD CONSTRAINT "FK_d9f099286b75fa0034dcd8cf7c2" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "submission_field" ADD CONSTRAINT "FK_16fae661ce5b10f27abe2e524a0" FOREIGN KEY ("submissionId") REFERENCES "submission"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "submission_field" ADD CONSTRAINT "FK_5befa92da2370b7eb1cab6ae30a" FOREIGN KEY ("fieldId") REFERENCES "form_field"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form_visitor" ADD CONSTRAINT "FK_72ade6c3a3e55d1fce94300f8b6" FOREIGN KEY ("formId") REFERENCES "form"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "submission" ADD CONSTRAINT "FK_6090e1d5cbf3433ffd14e3b53e7" FOREIGN KEY ("formId") REFERENCES "form"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "submission" ADD CONSTRAINT "FK_95b73c7faf2c199f005fda5e8c8" FOREIGN KEY ("visitorId") REFERENCES "form_visitor"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "submission" ADD CONSTRAINT "FK_7bd626272858ef6464aa2579094" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "page_button" ADD CONSTRAINT "FK_d9f099286b75fa0034dcd8cf7c2" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form" ADD CONSTRAINT "FK_a7cb33580bca2b362e5e34fdfcd" FOREIGN KEY ("adminId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form" ADD CONSTRAINT "FK_023d9cf1d97e93facc96c86ca70" FOREIGN KEY ("startPageId") REFERENCES "page"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form" ADD CONSTRAINT "FK_e5d158932e43cfbf9958931ee01" FOREIGN KEY ("endPageId") REFERENCES "page"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "form_visitor" ADD "deviceLanguage" character varying`);
await queryRunner.query(`ALTER TABLE "form_visitor" ADD "deviceType" character varying`);
await queryRunner.query(`ALTER TABLE "form_visitor" ADD "deviceName" character varying`);
await queryRunner.query(`ALTER TABLE "submission" ADD "deviceLanguage" character varying`);
await queryRunner.query(`ALTER TABLE "submission" ADD "deviceType" character varying`);
await queryRunner.query(`ALTER TABLE "submission" ADD "deviceName" character varying`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "roles"`);
await queryRunner.query(`ALTER TABLE "user" ADD "roles" text NOT NULL`);
}
}
@ -67,13 +56,13 @@ export class initial1619723437787 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE "form" DROP CONSTRAINT "FK_e5d158932e43cfbf9958931ee01"`);
await queryRunner.query(`ALTER TABLE "form" DROP CONSTRAINT "FK_023d9cf1d97e93facc96c86ca70"`);
await queryRunner.query(`ALTER TABLE "form" DROP CONSTRAINT "FK_a7cb33580bca2b362e5e34fdfcd"`);
await queryRunner.query(`ALTER TABLE "page_button" DROP CONSTRAINT "FK_d9f099286b75fa0034dcd8cf7c2"`);
await queryRunner.query(`ALTER TABLE "submission" DROP CONSTRAINT "FK_7bd626272858ef6464aa2579094"`);
await queryRunner.query(`ALTER TABLE "submission" DROP CONSTRAINT "FK_95b73c7faf2c199f005fda5e8c8"`);
await queryRunner.query(`ALTER TABLE "submission" DROP CONSTRAINT "FK_6090e1d5cbf3433ffd14e3b53e7"`);
await queryRunner.query(`ALTER TABLE "form_visitor" DROP CONSTRAINT "FK_72ade6c3a3e55d1fce94300f8b6"`);
await queryRunner.query(`ALTER TABLE "submission_field" DROP CONSTRAINT "FK_5befa92da2370b7eb1cab6ae30a"`);
await queryRunner.query(`ALTER TABLE "submission_field" DROP CONSTRAINT "FK_16fae661ce5b10f27abe2e524a0"`);
await queryRunner.query(`ALTER TABLE "page_button" DROP CONSTRAINT "FK_d9f099286b75fa0034dcd8cf7c2"`);
await queryRunner.query(`ALTER TABLE "form_notification" DROP CONSTRAINT "FK_4915ebae53e09b732322d0ff6ed"`);
await queryRunner.query(`ALTER TABLE "form_notification" DROP CONSTRAINT "FK_0876741ce2acdaee4553d7a3bbd"`);
await queryRunner.query(`ALTER TABLE "form_notification" DROP CONSTRAINT "FK_a9ed55144108ded893b502d6321"`);
@ -85,12 +74,12 @@ export class initial1619723437787 implements MigrationInterface {
}
await queryRunner.query(`DROP TABLE "form"`);
await queryRunner.query(`DROP TABLE "page"`);
await queryRunner.query(`DROP TABLE "page_button"`);
await queryRunner.query(`DROP TABLE "submission"`);
await queryRunner.query(`DROP TABLE "form_visitor"`);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`DROP TABLE "submission_field"`);
await queryRunner.query(`DROP TABLE "page"`);
await queryRunner.query(`DROP TABLE "page_button"`);
await queryRunner.query(`DROP TABLE "form_notification"`);
await queryRunner.query(`DROP TABLE "form_hook"`);
await queryRunner.query(`DROP TABLE "form_field"`);

View File

@ -26,9 +26,9 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldModel[]> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
return form.fields.map(field => new FormFieldModel(field))
return form.fields?.map(field => new FormFieldModel(field)) || []
}
@ResolveField('hooks', () => [FormHookModel])
@ -37,9 +37,9 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<FormHookModel[]> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
return form.hooks.map(hook => new FormHookModel(hook))
return form.hooks?.map(hook => new FormHookModel(hook)) || []
}
@ResolveField('isLive', () => Boolean)
@ -49,7 +49,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<boolean> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
if (!await this.formService.isAdmin(form, user)) {
throw new Error('no access to field')
@ -71,7 +71,7 @@ export class FormResolver {
throw new Error('no access to field')
}
return form.notifications.map(notification => new FormNotificationModel(notification))
return form.notifications?.map(notification => new FormNotificationModel(notification)) || []
}
@ResolveField('design', () => DesignModel)
@ -80,7 +80,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<DesignModel> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
return new DesignModel(form.design)
}
@ -90,7 +90,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<PageModel> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
return new PageModel(form.startPage)
}
@ -100,7 +100,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<PageModel> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
return new PageModel(form.endPage)
}
@ -111,7 +111,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<UserModel> {
const form = await cache.get(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
if (!form.admin) {
return null

View File

@ -1,11 +1,18 @@
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { FormFieldModel } from '../../dto/form/form.field.model'
import { SubmissionFieldModel } from '../../dto/submission/submission.field.model'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
import { FormFieldService } from '../../service/form/form.field.service'
import { ContextCache } from '../context.cache'
@Resolver(() => SubmissionFieldModel)
export class SubmissionFieldResolver {
constructor(
private readonly formFieldService: FormFieldService,
) {
}
@ResolveField('field', () => FormFieldModel, { nullable: true })
async getFields(
@Parent() parent: SubmissionFieldModel,
@ -13,10 +20,12 @@ export class SubmissionFieldResolver {
): Promise<FormFieldModel> {
const submissionField = await cache.get<SubmissionFieldEntity>(cache.getCacheKey(SubmissionFieldEntity.name, parent.id))
if (!submissionField.field) {
const field = await cache.get<FormFieldEntity>(cache.getCacheKey(FormFieldEntity.name, submissionField.fieldId), () => this.formFieldService.findById(submissionField.fieldId, submissionField.field))
if (!field) {
return null
}
return new FormFieldModel(submissionField.field)
return new FormFieldModel(field)
}
}

View File

@ -2,8 +2,6 @@ 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 { FormEntity } from '../../entity/form.entity'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { SubmissionEntity } from '../../entity/submission.entity'
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
import { UserEntity } from '../../entity/user.entity'
@ -19,12 +17,6 @@ export class SubmissionResolver {
): Promise<SubmissionFieldModel[]> {
const submission = await cache.get<SubmissionEntity>(cache.getCacheKey(SubmissionEntity.name, parent.id))
cache.add(cache.getCacheKey(FormEntity.name, submission.form.id), submission.form)
submission.form.fields.forEach(field => {
cache.add(cache.getCacheKey(FormFieldEntity.name, field.id), field)
})
return submission.fields.map(field => {
cache.add(cache.getCacheKey(SubmissionFieldEntity.name, field.id), field)
return new SubmissionFieldModel(field)

View File

@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'
import { Args, Context, ID, Mutation } from '@nestjs/graphql'
import { IpAddress } from '../../decorator/ip.address.decorator'
import { User } from '../../decorator/user.decorator'
import { SubmissionProgressModel } from '../../dto/submission/submission.progress.model'
import { SubmissionStartInput } from '../../dto/submission/submission.start.input'
@ -22,11 +23,12 @@ export class SubmissionStartMutation {
@User() user: UserEntity,
@Args({ name: 'form', type: () => ID }) id: string,
@Args({ name: 'submission', type: () => SubmissionStartInput }) input: SubmissionStartInput,
@IpAddress() ipAddr: string,
@Context('cache') cache: ContextCache,
): Promise<SubmissionProgressModel> {
const form = await this.formService.findById(id)
const submission = await this.startService.start(form, input, user)
const submission = await this.startService.start(form, input, user, ipAddr)
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)

View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { FormEntity } from '../../entity/form.entity'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { UserEntity } from '../../entity/user.entity'
@Injectable()
export class FormFieldService {
constructor(
@InjectRepository(FormFieldEntity)
private readonly formFieldRepository: Repository<FormFieldEntity>,
) {
}
async findById(id: number | string, existing?: FormFieldEntity): Promise<FormFieldEntity> {
if (existing) return existing
const entity = await this.formFieldRepository.findOne(id);
if (!entity) {
throw new Error('no form field found')
}
return entity
}
}

View File

@ -41,7 +41,9 @@ export class FormService {
return await qb.getManyAndCount()
}
async findById(id: string): Promise<FormEntity> {
async findById(id: number | string, existing?: FormEntity): Promise<FormEntity> {
if (existing) return existing
const form = await this.formRepository.findOne(id);
if (!form) {

View File

@ -1,5 +1,6 @@
import { FormCreateService } from './form.create.service'
import { FormDeleteService } from './form.delete.service'
import { FormFieldService } from './form.field.service'
import { FormService } from './form.service'
import { FormStatisticService } from './form.statistic.service'
import { FormUpdateService } from './form.update.service'
@ -7,6 +8,7 @@ import { FormUpdateService } from './form.update.service'
export const formServices = [
FormCreateService,
FormDeleteService,
FormFieldService,
FormService,
FormStatisticService,
FormUpdateService,

View File

@ -1,4 +1,5 @@
import { SubmissionHookService } from './submission.hook.service'
import { SubmissionNotificationService } from './submission.notification.service'
import { SubmissionService } from './submission.service'
import { SubmissionSetFieldService } from './submission.set.field.service'
import { SubmissionStartService } from './submission.start.service'
@ -6,10 +7,11 @@ import { SubmissionStatisticService } from './submission.statistic.service'
import { SubmissionTokenService } from './submission.token.service'
export const submissionServices = [
SubmissionHookService,
SubmissionNotificationService,
SubmissionService,
SubmissionSetFieldService,
SubmissionStartService,
SubmissionStatisticService,
SubmissionTokenService,
SubmissionHookService,
]

View File

@ -23,7 +23,7 @@ export class SubmissionHookService {
await this.format(submission, hook.format)
).toPromise()
console.log('sent', response.data)
console.log('sent hook', response.data)
} catch (e) {
this.logger.error(`failed to post to "${hook.url}: ${e.message}`)
this.logger.error(e.stack)

View File

@ -0,0 +1,70 @@
import { MailerService } from '@nestjs-modules/mailer'
import { Injectable } from '@nestjs/common'
import handlebars from 'handlebars'
import htmlToText from 'html-to-text'
import { PinoLogger } from 'nestjs-pino/dist'
import { SubmissionEntity } from '../../entity/submission.entity'
import mjml2html from 'mjml'
@Injectable()
export class SubmissionNotificationService {
constructor(
private readonly nestMailer: MailerService,
private readonly logger: PinoLogger,
) {
}
public async process(submission: SubmissionEntity): Promise<void> {
await Promise.all(submission.form.notifications.map(async (notification) => {
if (!notification.enabled) {
return
}
try {
const to = this.getEmail(submission.fields.find(field => field.fieldId === notification.toField.id )?.fieldValue, notification.toEmail)
const from = this.getEmail(submission.fields.find(field => field.fieldId === notification.fromField.id )?.fieldValue, notification.fromEmail)
const html = mjml2html(
handlebars.compile(
notification.htmlTemplate
)({
// TODO add varialbes
}),
{
minify: true
}
).html
await this.nestMailer.sendMail({
to,
replyTo: from,
subject: notification.subject,
html,
text: htmlToText.fromString(html)
})
console.log('sent notification to', to)
} catch (e) {
this.logger.error(e.stack)
throw e
}
}))
}
private getEmail(raw: string, fallback: string): string {
if (!raw) {
return fallback
}
try {
const data = JSON.parse(raw)
if (data.value) {
return data.value
}
} catch (e) {
this.logger.error('could not decode field value', raw)
}
return fallback
}
}

View File

@ -19,6 +19,8 @@ export class SubmissionService {
async find(form: FormEntity, start: number, limit: number, sort: any = {}): Promise<[SubmissionEntity[], number]> {
const qb = this.submissionRepository.createQueryBuilder('s')
qb.leftJoinAndSelect('s.fields', 'fields')
qb.where('s.form = :form', { form: form.id })
// TODO readd sort

View File

@ -7,6 +7,7 @@ import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.fie
import { SubmissionEntity } from '../../entity/submission.entity'
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
import { SubmissionHookService } from './submission.hook.service'
import { SubmissionNotificationService } from './submission.notification.service'
@Injectable()
export class SubmissionSetFieldService {
@ -16,6 +17,7 @@ export class SubmissionSetFieldService {
@InjectRepository(SubmissionFieldEntity)
private readonly submissionFieldRepository: Repository<SubmissionFieldEntity>,
private readonly webHook: SubmissionHookService,
private readonly notifications: SubmissionNotificationService,
private readonly logger: PinoLogger,
) {
}
@ -24,7 +26,7 @@ export class SubmissionSetFieldService {
let field = submission.fields.find(field => field.field.id.toString() === input.field)
if (field) {
field.fieldValue = input.data
field.fieldValue = JSON.parse(input.data)
await this.submissionFieldRepository.save(field)
} else {
@ -33,7 +35,7 @@ export class SubmissionSetFieldService {
field.submission = submission
field.field = submission.form.fields.find(field => field.id.toString() === input.field)
field.fieldType = field.field.type
field.fieldValue = input.data
field.fieldValue = JSON.parse(input.data)
field = await this.submissionFieldRepository.save(field)
@ -50,6 +52,9 @@ export class SubmissionSetFieldService {
this.webHook.process(submission).catch(e => {
this.logger.error(`failed to send webhooks: ${e.message}`)
})
this.notifications.process(submission).catch(e => {
this.logger.error(`failed to send notifications: ${e.message}`)
})
}
}
}

View File

@ -20,11 +20,15 @@ export class SubmissionStartService {
form: FormEntity,
input: SubmissionStartInput,
user?: UserEntity,
ipAddr?: string,
): Promise<SubmissionEntity> {
const submission = new SubmissionEntity()
submission.form = form
submission.user = user
submission.ipAddr = ipAddr || '?'
submission.timeElapsed = 0
submission.percentageComplete = 0
submission.device.language = input.device.language
submission.device.name = input.device.name

View File

@ -2280,6 +2280,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/request-ip@^0.0.35":
version "0.0.35"
resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.35.tgz#c30e832533296151c221532c48c10591b7edd468"
integrity sha512-FtI7lv1EDaZnWmaCU7ZTwfOpW76EioocaWyiSeWdfW1cDPZYzzij781A5O/UeHQUN9yjtjEcD3StTzZjKG0XEA==
dependencies:
"@types/node" "*"
"@types/semver@^6.0.0":
version "6.2.2"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.2.2.tgz#5c27df09ca39e3c9beb4fae6b95f4d71426df0a9"
@ -6847,6 +6854,11 @@ is-wsl@^2.1.1, is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
is_js@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d"
integrity sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0=
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@ -10235,6 +10247,13 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
request-ip@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-2.1.3.tgz#99ab2bafdeaf2002626e28083cb10597511d9e14"
integrity sha512-J3qdE/IhVM3BXkwMIVO4yFrvhJlU3H7JH16+6yHucadT4fePnR8dyh+vEs6FIx0S2x5TCt2ptiPfHcn0sqhbYQ==
dependencies:
is_js "^0.9.0"
request-promise-core@1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"