Compare commits

...

19 Commits

Author SHA1 Message Date
Michael Schramm
c2c421baa6 fix node prune location
fixes https://github.com/ohmyform/ohmyform/issues/184
2022-07-28 08:01:08 +02:00
Michael Schramm
b6f0c9d64a fix creation of new logic elements 2022-03-27 20:41:27 +02:00
Michael Schramm
3b5a6c92af release 1.0.3 2022-03-27 11:31:47 +02:00
Michael Schramm
d86b02cf2e fix field resolvers, add start and end page to create 2022-03-27 11:30:17 +02:00
Michael Schramm
ef7cd7b3fb update node-gyp to latest version to enable build on osx 12.3 2022-03-26 17:19:54 +01:00
Michael Schramm
c71e87ec50 fix build 2022-03-14 16:40:50 +01:00
Michael Schramm
27aa5ebfb0 notifications / hooks / pages and buttons encode and decode their ids 2022-03-14 15:47:20 +01:00
Michael Schramm
7ea4f51f4b form hooks should only be queryable for form admins 2022-03-14 13:49:16 +01:00
Michael Schramm
b3cacb8481 fix missing encode / decode for form fields within submissions 2022-03-14 13:48:17 +01:00
Michael Schramm
6d85dc660a release 1.0.2 2022-03-13 23:43:18 +01:00
Michael Schramm
30c2bc9c05 fix error sending notification when field is not defined 2022-03-13 23:35:40 +01:00
Michael Schramm
99fe3d2fe0 release 1.0.1 2022-03-01 23:37:54 +01:00
Michael Schramm
5bc6047507 change default form layout to card 2022-03-01 15:17:39 +01:00
Michael Schramm
4a19bd5ca4 add new form by id pipe 2022-03-01 08:40:25 +01:00
Michael Schramm
fe3821ad42 stop exposing internal ids and switch over to hashids 2022-02-28 22:50:20 +01:00
Michael Schramm
2d3589e13a fix form delete 2022-02-28 22:48:39 +01:00
Michael Schramm
5b717a2963 only update user fields if they changed 2022-02-28 22:48:19 +01:00
Michael Schramm
1e3f47c11c update changelog for version 1.0.0 2022-02-28 08:23:34 +01:00
Michael Schramm
cfd8d288d4
add tests for migrations (#39) 2022-02-28 08:20:51 +01:00
71 changed files with 1281 additions and 312 deletions

View File

@ -20,31 +20,6 @@ jobs:
name: Run linters
runs-on: ubuntu-latest
services:
maria:
image: mariadb
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ohmyform
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
postgres:
image: postgres:10-alpine
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: ohmyform
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Check out Git repository
uses: actions/checkout@v2
@ -67,20 +42,136 @@ jobs:
- name: Typecheck
uses: andoshin11/typescript-error-reporter-action@v1.0.2
run-postgres:
name: Run Postgres
runs-on: ubuntu-latest
services:
postgres:
image: postgres:10-alpine
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: ohmyform
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Node.js dependencies
run: yarn install --frozen-lockfile --silent
- name: PostgreSQL Migrations
run: yarn typeorm migration:run
env:
DATABASE_DRIVER: postgres
DATABASE_URL: postgresql://root@127.0.0.1:5432/ohmyform
TYPEORM_CONNECTION: postgres
TYPEORM_HOST: localhost
TYPEORM_PORT: 5432
TYPEORM_USERNAME: root
TYPEORM_PASSWORD: root
TYPEORM_DATABASE: ohmyform
TYPEORM_AUTO_SCHEMA_SYNC: false
TYPEORM_ENTITIES: src/entity/**/*.ts
TYPEORM_SUBSCRIBERS: src/subscriber/**/*.ts
TYPEORM_MIGRATIONS: src/migrations/postgres/**/*.ts
TYPEORM_MIGRATIONS_TRANSACTION_MODE: 'each'
TYPEORM_ENTITIES_DIR: src/entity
TYPEORM_MIGRATIONS_DIR: src/migrations/postgres
TYPEORM_SUBSCRIBERS_DIR: src/subscriber
run-mariadb:
name: Run MariaDB
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ohmyform
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Node.js dependencies
run: yarn install --frozen-lockfile --silent
- name: MariaDB Migrations
run: yarn typeorm migration:run
env:
DATABASE_DRIVER: mariadb
DATABASE_URL: mysql://root@127.0.0.1:3306/ohmyform
TYPEORM_CONNECTION: mariadb
TYPEORM_HOST: localhost
TYPEORM_PORT: 3306
TYPEORM_USERNAME: root
TYPEORM_PASSWORD: root
TYPEORM_DATABASE: ohmyform
TYPEORM_AUTO_SCHEMA_SYNC: false
TYPEORM_ENTITIES: src/entity/**/*.ts
TYPEORM_SUBSCRIBERS: src/subscriber/**/*.ts
TYPEORM_MIGRATIONS: src/migrations/mariadb/**/*.ts
TYPEORM_MIGRATIONS_TRANSACTION_MODE: 'each'
TYPEORM_ENTITIES_DIR: src/entity
TYPEORM_MIGRATIONS_DIR: src/migrations/mariadb
TYPEORM_SUBSCRIBERS_DIR: src/subscriber
run-sqlite:
name: Run SQLite
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Node.js dependencies
run: yarn install --frozen-lockfile --silent
- name: SQLite Migrations
run: yarn typeorm migration:run
run: yarn typeorm migration:run --transaction none
env:
DATABASE_DRIVER: sqlite
DATABASE_URL: sqlite://data.sqlite
TYPEORM_CONNECTION: sqlite
TYPEORM_USERNAME: root
TYPEORM_DATABASE: data.sqlite
TYPEORM_AUTO_SCHEMA_SYNC: false
TYPEORM_ENTITIES: src/entity/**/*.ts
TYPEORM_SUBSCRIBERS: src/subscriber/**/*.ts
TYPEORM_MIGRATIONS: src/migrations/sqlite/**/*.ts
TYPEORM_MIGRATIONS_TRANSACTION_MODE: 'none'
TYPEORM_ENTITIES_DIR: src/entity
TYPEORM_MIGRATIONS_DIR: src/migrations/sqlite
TYPEORM_SUBSCRIBERS_DIR: src/subscriber

View File

@ -4,11 +4,80 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
<!--
Template for next version
## [Unreleased]
### Added
### Changed
### Fixed
### Security
-->
## [Unreleased]
### Added
### Changed
### Fixed
- creation of new logic elements
- node prune location (https://github.com/ohmyform/ohmyform/issues/184)
### Security
## [1.0.3] - 2022-03-27
### Added
- add start and end page to form create call
### Changed
- notifications / hooks / pages and buttons encode and decode their ids
### Fixed
- missing encode / decode for form fields within submissions (https://github.com/ohmyform/ui/commit/30ff2c96bca20c1641d9cbb96c34cce934e1afea#r68602651)
- form field resolvers were missing
- node-gyp update to enable build on osx 12.3
- creating of new fields
### Security
- form hooks should only be queryable for form admins
## [1.0.2] - 2022-03-13
### Fixed
- error sending notification when field is not defined (https://github.com/ohmyform/ohmyform/issues/161)
## [1.0.1] - 2022-03-01
### Added
- allow one field nested data to be submitted
### Fixed
- only update user fields in update mutation if they changed
- form delete
- field submission without value field
### Security
- start using hashids to prevent insights into form ids (https://hashids.org/javascript/)
## [1.0.0] - 2022-02-28
### Added
- logic backend components
- forms now have multiple notification
- layout for forms
@ -19,6 +88,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- ability to load submission by id if token is present
- anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108)
- ability to filter for partial / completed or empty submissions
- migration tests for all commits
### Changed

View File

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
RUN apk update && apk add curl bash && rm -rf /var/cache/apk/*
# install node-prune (https://github.com/tj/node-prune)
RUN curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | bash -s -- -b /usr/local/bin
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
# just copy everhing
COPY . .

View File

@ -1,6 +1,6 @@
{
"name": "ohmyform-api",
"version": "1.0.0",
"version": "1.0.3",
"description": "",
"author": "",
"license": "AGPL-3.0-or-later",
@ -22,7 +22,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm:sqlite": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_sqlite.json",
"typeorm:postgres": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_postgres.json",
"typeorm:mariadb": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_mariadb.json"
"typeorm:mariadb": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ormconfig_mariadb.json",
"typeorm": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
},
"dependencies": {
"@ardatan/aggregate-error": "^0.0.6",
@ -50,6 +51,7 @@
"graphql-subscriptions": "^2.0.0",
"graphql-tools": "^8.2.0",
"handlebars": "^4.7.7",
"hashids": "^2.2.10",
"html-to-text": "^8.1.0",
"inquirer": "^8.2.0",
"ioredis": "^4.28.5",
@ -83,7 +85,7 @@
"@types/handlebars": "^4.1.0",
"@types/html-to-text": "^8.0.1",
"@types/inquirer": "^8.2.0",
"@types/jest": "27.4.1",
"@types/jest": "^27.4.1",
"@types/mjml": "^4.7.0",
"@types/node": "^17.0.21",
"@types/passport-jwt": "^3.0.6",
@ -98,6 +100,7 @@
"eslint-plugin-nestjs": "^1.2.3",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^27.5.1",
"node-gyp": "^9.0.0",
"prettier": "^2.5.1",
"supertest": "^6.2.2",
"ts-jest": "27.1.3",

View File

@ -1,11 +1,13 @@
import { commands } from './command'
import { guards } from './guard'
import { pipes } from './pipe'
import { resolvers } from './resolver'
import { services } from './service'
export const providers = [
...resolvers,
...commands,
...services,
...guards,
...pipes,
...resolvers,
...services,
]

View File

@ -1,8 +1,8 @@
import { Field, ObjectType } from '@nestjs/graphql'
import { Field, ID, ObjectType } from '@nestjs/graphql'
@ObjectType('Deleted')
export class DeletedModel {
@Field()
@Field(() => ID)
id: string
constructor(id: string) {

View File

@ -3,6 +3,8 @@ import { PageButtonEntity } from '../../entity/page.button.entity'
@ObjectType('Button')
export class ButtonModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -24,8 +26,9 @@ export class ButtonModel {
@Field({ nullable: true })
readonly color?: string
constructor(button: Partial<PageButtonEntity>) {
this.id = button.id.toString()
constructor(id: string, button: Partial<PageButtonEntity>) {
this._id = button.id
this.id = id
this.url = button.url
this.action = button.action
this.text = button.text

View File

@ -1,4 +1,5 @@
import { Field, InputType } from '@nestjs/graphql'
import { PageInput } from './page.input'
@InputType('FormCreateInput')
export class FormCreateInput {
@ -19,4 +20,10 @@ export class FormCreateInput {
@Field({ nullable: true })
readonly layout: string
@Field({ nullable: true })
readonly startPage: PageInput
@Field({ nullable: true })
readonly endPage: PageInput
}

View File

@ -3,6 +3,8 @@ import { FormFieldLogicEntity } from '../../entity/form.field.logic.entity'
@ObjectType('FormFieldLogic')
export class FormFieldLogicModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -30,8 +32,9 @@ export class FormFieldLogicModel {
@Field()
readonly enabled: boolean
constructor(document: FormFieldLogicEntity) {
this.id = document.id.toString()
constructor(id: string, document: FormFieldLogicEntity) {
this._id = document.id
this.id = id
this.enabled = document.enabled
this.formula = document.formula

View File

@ -1,11 +1,10 @@
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { FormFieldLogicModel } from './form.field.logic.model'
import { FormFieldOptionModel } from './form.field.option.model'
import { FormFieldRatingModel } from './form.field.rating.model'
@ObjectType('FormField')
export class FormFieldModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -30,17 +29,9 @@ export class FormFieldModel {
@Field({ nullable: true })
readonly defaultValue: string
@Field(() => [FormFieldOptionModel])
readonly options: FormFieldOptionModel[]
@Field(() => [FormFieldLogicModel])
readonly logic: FormFieldLogicModel[]
@Field(() => FormFieldRatingModel, { nullable: true })
readonly rating: FormFieldRatingModel
constructor(document: FormFieldEntity) {
this.id = document.id.toString()
constructor(id: string, document: FormFieldEntity) {
this._id = document.id
this.id = id
this.idx = document.idx
this.title = document.title
this.slug = document.slug
@ -48,8 +39,5 @@ export class FormFieldModel {
this.description = document.description
this.required = document.required
this.defaultValue = document.defaultValue
this.options = document.options?.map(option => new FormFieldOptionModel(option)) || []
this.logic = document.logic?.map(logic => new FormFieldLogicModel(logic)) || []
this.rating = document.rating ? new FormFieldRatingModel(document.rating) : null
}
}

View File

@ -3,6 +3,8 @@ import { FormFieldOptionEntity } from '../../entity/form.field.option.entity'
@ObjectType('FormFieldOption')
export class FormFieldOptionModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -15,8 +17,9 @@ export class FormFieldOptionModel {
@Field()
readonly value: string
constructor(option: FormFieldOptionEntity) {
this.id = option.id.toString()
constructor(id: string, option: FormFieldOptionEntity) {
this._id = option.id
this.id = id
this.key = option.key
this.title = option.title
this.value = option.value

View File

@ -3,6 +3,8 @@ import { FormHookEntity } from '../../entity/form.hook.entity'
@ObjectType('FormHook')
export class FormHookModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -15,8 +17,9 @@ export class FormHookModel {
@Field({ nullable: true })
readonly format?: string
constructor(hook: FormHookEntity) {
this.id = hook.id.toString()
constructor(id, hook: FormHookEntity) {
this._id = hook.id
this.id = id
this.enabled = hook.enabled
this.url = hook.url
this.format = hook.format

View File

@ -3,6 +3,8 @@ import { FormEntity } from '../../entity/form.entity'
@ObjectType('Form')
export class FormModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -24,8 +26,9 @@ export class FormModel {
@Field()
readonly anonymousSubmission: boolean
constructor(form: FormEntity) {
this.id = form.id.toString()
constructor(id: string, form: FormEntity) {
this._id = form.id
this.id = id
this.title = form.title
this.created = form.created
this.lastModified = form.lastModified

View File

@ -3,6 +3,8 @@ import { FormNotificationEntity } from '../../entity/form.notification.entity'
@ObjectType('FormNotification')
export class FormNotificationModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -27,8 +29,9 @@ export class FormNotificationModel {
@Field()
readonly enabled: boolean
constructor(partial: Partial<FormNotificationEntity>) {
this.id = partial.id.toString()
constructor(id: string, partial: Partial<FormNotificationEntity>) {
this._id = partial.id
this.id = id
this.subject = partial.subject
this.htmlTemplate = partial.htmlTemplate
this.enabled = partial.enabled

View File

@ -18,6 +18,6 @@ export class PageInput {
@Field({ nullable: true })
readonly buttonText?: string
@Field(() => [ButtonInput])
@Field(() => [ButtonInput], { nullable: true })
readonly buttons: ButtonInput[]
}

View File

@ -1,9 +1,10 @@
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { PageEntity } from '../../entity/page.entity'
import { ButtonModel } from './button.model'
@ObjectType('Page')
export class PageModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -19,22 +20,18 @@ export class PageModel {
@Field({ nullable: true })
readonly buttonText?: string
@Field(() => [ButtonModel])
readonly buttons: ButtonModel[]
constructor(page: Partial<PageEntity>) {
constructor(id: string, page?: Partial<PageEntity>) {
if (!page) {
this.id = Math.random().toString()
this.id = id
this.show = false
this.buttons = []
return
}
this.id = page.id.toString()
this._id = page.id
this.id = id
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))
}
}

View File

@ -7,8 +7,8 @@ export class ProfileModel extends UserModel {
@Field(() => [String])
readonly roles: string[]
constructor(user: UserEntity) {
super(user)
constructor(id: string, user: UserEntity) {
super(id, user)
this.roles = user.roles
}

View File

@ -3,6 +3,8 @@ import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
@ObjectType('SubmissionField')
export class SubmissionFieldModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -12,8 +14,9 @@ export class SubmissionFieldModel {
@Field()
readonly type: string
constructor(field: SubmissionFieldEntity) {
this.id = field.id.toString()
constructor(id: string, field: SubmissionFieldEntity) {
this._id = field.id
this.id = id
this.value = JSON.stringify(field.content)
this.type = field.type
}

View File

@ -5,8 +5,10 @@ import { GeoLocationModel } from './geo.location.model'
@ObjectType('Submission')
export class SubmissionModel {
readonly _id: number
@Field(() => ID)
readonly id: number
readonly id: string
@Field()
readonly ipAddr: string
@ -29,8 +31,9 @@ export class SubmissionModel {
@Field({ nullable: true })
readonly lastModified?: Date
constructor(submission: SubmissionEntity) {
this.id = submission.id
constructor(id: string, submission: SubmissionEntity) {
this._id = submission.id
this.id = id
this.ipAddr = submission.ipAddr
this.geoLocation = new GeoLocationModel(submission.geoLocation)

View File

@ -3,6 +3,8 @@ import { SubmissionEntity } from '../../entity/submission.entity'
@ObjectType('SubmissionProgress')
export class SubmissionProgressModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -18,8 +20,9 @@ export class SubmissionProgressModel {
@Field({ nullable: true })
readonly lastModified?: Date
constructor(submission: Partial<SubmissionEntity>) {
this.id = submission.id.toString()
constructor(id: string, submission: Partial<SubmissionEntity>) {
this._id = submission.id
this.id = id
this.timeElapsed = submission.timeElapsed
this.percentageComplete = submission.percentageComplete

View File

@ -3,6 +3,8 @@ import { UserEntity } from '../../entity/user.entity'
@ObjectType('User')
export class UserModel {
readonly _id: number
@Field(() => ID)
readonly id: string
@ -36,8 +38,9 @@ export class UserModel {
@Field({ nullable: true })
readonly lastModified: Date
constructor(user: UserEntity) {
this.id = user.id.toString()
constructor(id: string, user: UserEntity) {
this._id = user.id
this.id = id
this.username = user.username
this.email = user.email

View File

@ -72,4 +72,10 @@ export class FormEntity {
@UpdateDateColumn()
public lastModified: Date
constructor(partial?: Partial<FormEntity>) {
if (partial) {
Object.assign(this, partial)
}
}
}

View File

@ -2,8 +2,12 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from 't
import { FormFieldEntity } from './form.field.entity'
import { SubmissionEntity } from './submission.entity'
export interface SubmissionFieldContent {
[key: string]: string | string[] | number | number[] | boolean | boolean[]
type Simple = string | number | boolean
export type SubmissionFieldContent = Simple | Simple[] | {
[key: string]: Simple | Simple[] | {
[key: string]: Simple | Simple[]
}
}
@Entity({ name: 'submission_field' })

View File

@ -1,9 +1,10 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { QueryRunner } from 'typeorm'
import { SqliteMigration } from '../sqlite.migration'
export class initial1619723437787 implements MigrationInterface {
export class initial1619723437787 extends SqliteMigration {
name = 'initial1619723437787'
public async up(queryRunner: QueryRunner): Promise<void> {
public async realUp(queryRunner: QueryRunner): Promise<void> {
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)');
@ -16,13 +17,10 @@ export class initial1619723437787 implements MigrationInterface {
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" 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)');
}
public async down(queryRunner: QueryRunner): Promise<void> {
public async realDown(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE "form"');
await queryRunner.query('DROP TABLE "submission"');
await queryRunner.query('DROP TABLE "form_visitor"');

View File

@ -1,13 +1,14 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { QueryRunner } from 'typeorm'
import { SqliteMigration } from '../sqlite.migration'
export class defaultValue1645952169100 implements MigrationInterface {
export class defaultValue1645952169100 extends SqliteMigration {
name = 'defaultValue1645952169100'
public async up(queryRunner: QueryRunner): Promise<void> {
public async realUp(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "form_field" RENAME COLUMN "value" TO "defaultValue"');
}
public async down(queryRunner: QueryRunner): Promise<void> {
public async realDown(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "form_field" RENAME COLUMN "defaultValue" TO "value"');
}
}

View File

@ -0,0 +1,19 @@
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'
import { FormEntity } from '../../entity/form.entity'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
@Injectable()
export class FormByIdPipe implements PipeTransform<string, Promise<FormEntity>> {
constructor(
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
async transform(value: string, metadata: ArgumentMetadata): Promise<FormEntity> {
const id = this.idService.decode(value)
return await this.formService.findById(id)
}
}

3
src/pipe/form/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { FormByIdPipe } from './form.by.id.pipe'
export const formPipes = [FormByIdPipe]

9
src/pipe/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { formPipes } from './form'
import { submissionPipes } from './submission'
import { userPipes } from './user'
export const pipes = [
...formPipes,
...submissionPipes,
...userPipes,
]

View File

@ -0,0 +1,3 @@
import { SubmissionByIdPipe } from './submission.by.id.pipe'
export const submissionPipes = [SubmissionByIdPipe]

View File

@ -0,0 +1,19 @@
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'
import { SubmissionEntity } from '../../entity/submission.entity'
import { IdService } from '../../service/id.service'
import { SubmissionService } from '../../service/submission/submission.service'
@Injectable()
export class SubmissionByIdPipe implements PipeTransform<string, Promise<SubmissionEntity>> {
constructor(
private readonly submissionService: SubmissionService,
private readonly idService: IdService,
) {
}
async transform(value: string, metadata: ArgumentMetadata): Promise<SubmissionEntity> {
const id = this.idService.decode(value)
return await this.submissionService.findById(id)
}
}

3
src/pipe/user/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { UserByIdPipe } from './user.by.id.pipe'
export const userPipes = [UserByIdPipe]

View File

@ -0,0 +1,19 @@
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'
import { UserEntity } from '../../entity/user.entity'
import { IdService } from '../../service/id.service'
import { UserService } from '../../service/user/user.service'
@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<UserEntity>> {
constructor(
private readonly userService: UserService,
private readonly idService: IdService,
) {
}
async transform(value: string, metadata: ArgumentMetadata): Promise<UserEntity> {
const id = this.idService.decode(value)
return await this.userService.findById(id)
}
}

View File

@ -1,12 +1,10 @@
type ID = string | number
export class ContextCache<A = any> {
private cache: {
[key: string]: any
} = {}
public getCacheKey(type: string, id: ID): string {
public getCacheKey(type: string, id: number): string {
return `${type}:${id}`
}

View File

@ -7,12 +7,14 @@ import { FormModel } from '../../dto/form/form.model'
import { FormEntity } from '../../entity/form.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormCreateService } from '../../service/form/form.create.service'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class FormCreateMutation {
constructor(
private readonly createService: FormCreateService
private readonly createService: FormCreateService,
private readonly idService: IdService,
) {
}
@ -27,6 +29,6 @@ export class FormCreateMutation {
cache.add(cache.getCacheKey(FormEntity.name, form.id), form)
return new FormModel(form)
return new FormModel(this.idService.encode(form.id), form)
}
}

View File

@ -3,15 +3,19 @@ import { Args, ID, Mutation } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
import { DeletedModel } from '../../dto/deleted.model'
import { FormEntity } from '../../entity/form.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe'
import { FormDeleteService } from '../../service/form/form.delete.service'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
@Injectable()
export class FormDeleteMutation {
constructor(
private readonly deleteService: FormDeleteService,
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
@ -19,16 +23,14 @@ export class FormDeleteMutation {
@Roles('admin')
async deleteForm(
@User() user: UserEntity,
@Args({ name: 'id', type: () => ID}) id: string,
@Args('id', {type: () => ID}, FormByIdPipe) form: FormEntity,
): Promise<DeletedModel> {
const form = await this.formService.findById(id)
if (!form.isLive && !this.formService.isAdmin(form, user)) {
throw new Error('invalid form')
}
await this.deleteService.delete(id)
await this.deleteService.delete(form.id)
return new DeletedModel(id)
return new DeletedModel(this.idService.encode(form.id))
}
}

View File

@ -0,0 +1,73 @@
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { FormFieldLogicModel } from '../../dto/form/form.field.logic.model'
import { FormFieldModel } from '../../dto/form/form.field.model'
import { FormFieldOptionModel } from '../../dto/form/form.field.option.model'
import { FormFieldRatingModel } from '../../dto/form/form.field.rating.model'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Resolver(FormFieldModel)
export class FormFieldResolver {
constructor(
private readonly idService: IdService,
) {
}
@ResolveField(() => [FormFieldOptionModel])
async options(
@Parent() parent: FormFieldModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldOptionModel[]> {
const field = await cache.get<FormFieldEntity>(cache.getCacheKey(
FormFieldEntity.name,
parent._id
))
if (!field.options) {
return []
}
return field.options.map(option => new FormFieldOptionModel(
this.idService.encode(option.id),
option,
))
}
@ResolveField(() => [FormFieldLogicModel])
async logic(
@Parent() parent: FormFieldModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldLogicModel[]> {
const field = await cache.get<FormFieldEntity>(cache.getCacheKey(
FormFieldEntity.name,
parent._id
))
if (!field.logic) {
return []
}
return field.logic.map(logic => new FormFieldLogicModel(
this.idService.encode(logic.id),
logic,
))
}
@ResolveField(() => FormFieldRatingModel, { nullable: true })
async rating(
@Parent() parent: FormFieldModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldRatingModel> {
const field = await cache.get<FormFieldEntity>(cache.getCacheKey(
FormFieldEntity.name,
parent._id
))
if (!field.rating) {
return null
}
return new FormFieldRatingModel(field.rating)
}
}

View File

@ -7,12 +7,14 @@ import { FormPagerModel } from '../../dto/form/form.pager.model'
import { FormEntity } from '../../entity/form.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class FormListQuery {
constructor(
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
@ -34,7 +36,7 @@ export class FormListQuery {
forms.forEach(form => cache.add(cache.getCacheKey(FormEntity.name, form.id), form))
return new FormPagerModel(
forms.map(form => new FormModel(form)),
forms.map(form => new FormModel(this.idService.encode(form.id), form)),
total,
limit,
start,

View File

@ -4,30 +4,31 @@ import { User } from '../../decorator/user.decorator'
import { FormModel } from '../../dto/form/form.model'
import { FormEntity } from '../../entity/form.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class FormQuery {
constructor(
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
@Query(() => FormModel)
async getFormById(
getFormById(
@User() user: UserEntity,
@Args('id', {type: () => ID}) id,
@Args('id', {type: () => ID}, FormByIdPipe) form: FormEntity,
@Context('cache') cache: ContextCache,
): Promise<FormModel> {
const form = await this.formService.findById(id)
): FormModel {
if (!form.isLive && !this.formService.isAdmin(form, user)) {
throw new Error('invalid form')
}
cache.add(cache.getCacheKey(FormEntity.name, form.id), form)
return new FormModel(form)
return new FormModel(this.idService.encode(form.id), form)
}
}

View File

@ -9,14 +9,18 @@ import { FormNotificationModel } from '../../dto/form/form.notification.model'
import { PageModel } from '../../dto/form/page.model'
import { UserModel } from '../../dto/user/user.model'
import { FormEntity } from '../../entity/form.entity'
import { FormFieldEntity } from '../../entity/form.field.entity'
import { PageEntity } from '../../entity/page.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Resolver(() => FormModel)
export class FormResolver {
constructor(
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
@ -26,20 +30,45 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<FormFieldModel[]> {
const form = await cache.get<FormEntity>(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)).sort((a,b) => a.idx - b.idx) || []
if (!form.fields) {
return []
}
return form.fields
.sort((a,b) => a.idx - b.idx)
.map(field => {
cache.add(cache.getCacheKey(FormFieldEntity.name, field.id), field)
return new FormFieldModel(
this.idService.encode(field.id),
field,
)
})
}
@ResolveField(() => [FormHookModel])
@Roles('admin')
async hooks(
@User() user: UserEntity,
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<FormHookModel[]> {
const form = await cache.get<FormEntity>(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)) || []
if (!this.formService.isAdmin(form, user)) {
throw new Error('no access to field')
}
if (!form.hooks) {
return []
}
return form.hooks.map(hook => new FormHookModel(
this.idService.encode(hook.id),
hook
))
}
@ResolveField(() => Boolean)
@ -49,7 +78,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<boolean> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent._id))
if (!this.formService.isAdmin(form, user)) {
throw new Error('no access to field')
@ -65,13 +94,20 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<FormNotificationModel[]> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent._id))
if (!this.formService.isAdmin(form, user)) {
throw new Error('no access to field')
}
return form.notifications?.map(notification => new FormNotificationModel(notification)) || []
if (!form.notifications) {
return []
}
return form.notifications.map(notification => new FormNotificationModel(
this.idService.encode(notification.id),
notification
))
}
@ResolveField(() => DesignModel)
@ -80,7 +116,7 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<DesignModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent._id))
return new DesignModel(form.design)
}
@ -90,9 +126,21 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<PageModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const { startPage } = await cache.get<FormEntity>(cache.getCacheKey(
FormEntity.name,
parent._id
))
return new PageModel(form.startPage)
if (startPage) {
cache.add(cache.getCacheKey(PageEntity.name, startPage.id), startPage)
return new PageModel(
this.idService.encode(startPage.id),
startPage
)
}
return new PageModel(Math.random().toString())
}
@ResolveField(() => PageModel)
@ -100,9 +148,18 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<PageModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const { endPage } = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent._id))
return new PageModel(form.endPage)
if (endPage) {
cache.add(cache.getCacheKey(PageEntity.name, endPage.id), endPage)
return new PageModel(
this.idService.encode(endPage.id),
endPage
)
}
return new PageModel(Math.random().toString())
}
@ResolveField(() => UserModel, { nullable: true })
@ -111,12 +168,12 @@ export class FormResolver {
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<UserModel> {
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent.id))
const form = await cache.get<FormEntity>(cache.getCacheKey(FormEntity.name, parent._id))
if (!form.admin) {
return null
}
return new UserModel(form.admin)
return new UserModel(this.idService.encode(form.admin.id), form.admin)
}
}

View File

@ -8,6 +8,7 @@ import { FormEntity } from '../../entity/form.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { FormUpdateService } from '../../service/form/form.update.service'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Injectable()
@ -15,6 +16,7 @@ export class FormUpdateMutation {
constructor(
private readonly updateService: FormUpdateService,
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
@ -25,7 +27,7 @@ export class FormUpdateMutation {
@Args({ name: 'form', type: () => FormUpdateInput }) input: FormUpdateInput,
@Context('cache') cache: ContextCache,
): Promise<FormModel> {
const form = await this.formService.findById(input.id)
const form = await this.formService.findById(this.idService.decode(input.id))
if (!form.isLive && !this.formService.isAdmin(form, user)) {
throw new Error('invalid form')
@ -35,6 +37,6 @@ export class FormUpdateMutation {
cache.add(cache.getCacheKey(FormEntity.name, form.id), form)
return new FormModel(form)
return new FormModel(this.idService.encode(form.id), form)
}
}

View File

@ -1,19 +1,23 @@
import { FormCreateMutation } from './form.create.mutation'
import { FormDeleteMutation } from './form.delete.mutation'
import { FormFieldResolver } from './form.field.resolver'
import { FormListQuery } from './form.list.query'
import { FormQuery } from './form.query'
import { FormResolver } from './form.resolver'
import { FormStatisticQuery } from './form.statistic.query'
import { FormStatisticResolver } from './form.statistic.resolver'
import { FormUpdateMutation } from './form.update.mutation'
import { PageResolver } from './page.resolver'
export const formResolvers = [
FormCreateMutation,
FormDeleteMutation,
FormFieldResolver,
FormQuery,
FormResolver,
FormListQuery,
FormStatisticQuery,
FormStatisticResolver,
FormUpdateMutation,
PageResolver,
]

View File

@ -0,0 +1,32 @@
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { ButtonModel } from '../../dto/form/button.model'
import { FormModel } from '../../dto/form/form.model'
import { PageModel } from '../../dto/form/page.model'
import { PageEntity } from '../../entity/page.entity'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Resolver(() => PageModel)
export class PageResolver {
constructor(
private readonly idService: IdService,
) {
}
@ResolveField(() => [ButtonModel])
async buttons(
@Parent() parent: FormModel,
@Context('cache') cache: ContextCache,
): Promise<ButtonModel[]> {
if (!parent._id) {
return []
}
const page = await cache.get<PageEntity>(cache.getCacheKey(PageEntity.name, parent._id))
return page.buttons.map(button => new ButtonModel(
this.idService.encode(button.id),
button
))
}
}

View File

@ -4,10 +4,16 @@ import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
import { ProfileModel } from '../../dto/profile/profile.model'
import { UserEntity } from '../../entity/user.entity'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class ProfileQuery {
constructor(
private readonly idService: IdService,
) {
}
@Query(() => ProfileModel)
@Roles('user')
public me(
@ -16,6 +22,6 @@ export class ProfileQuery {
): ProfileModel {
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new ProfileModel(user)
return new ProfileModel(this.idService.encode(user.id), user)
}
}

View File

@ -5,6 +5,7 @@ import { User } from '../../decorator/user.decorator'
import { ProfileModel } from '../../dto/profile/profile.model'
import { ProfileUpdateInput } from '../../dto/profile/profile.update.input'
import { UserEntity } from '../../entity/user.entity'
import { IdService } from '../../service/id.service'
import { ProfileUpdateService } from '../../service/profile/profile.update.service'
import { ContextCache } from '../context.cache'
@ -12,6 +13,7 @@ import { ContextCache } from '../context.cache'
export class ProfileUpdateMutation {
constructor(
private readonly updateService: ProfileUpdateService,
private readonly idService: IdService,
) {
}
@ -26,7 +28,7 @@ export class ProfileUpdateMutation {
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new ProfileModel(user)
return new ProfileModel(this.idService.encode(user.id), user)
}
@Mutation(() => ProfileModel)
@ -40,6 +42,6 @@ export class ProfileUpdateMutation {
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new ProfileModel(user)
return new ProfileModel(this.idService.encode(user.id), user)
}
}

View File

@ -4,12 +4,14 @@ import { SubmissionFieldModel } from '../../dto/submission/submission.field.mode
import { FormFieldEntity } from '../../entity/form.field.entity'
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
import { FormFieldService } from '../../service/form/form.field.service'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Resolver(() => SubmissionFieldModel)
export class SubmissionFieldResolver {
constructor(
private readonly formFieldService: FormFieldService,
private readonly idService: IdService,
) {
}
@ -19,7 +21,7 @@ export class SubmissionFieldResolver {
@Context('cache') cache: ContextCache,
): Promise<FormFieldModel> {
const submissionField = await cache.get<SubmissionFieldEntity>(
cache.getCacheKey(SubmissionFieldEntity.name, parent.id)
cache.getCacheKey(SubmissionFieldEntity.name, parent._id)
)
const field = await cache.get<FormFieldEntity>(
@ -33,6 +35,9 @@ export class SubmissionFieldResolver {
return null
}
return new FormFieldModel(field)
return new FormFieldModel(
this.idService.encode(field.id),
field,
)
}
}

View File

@ -2,33 +2,31 @@ 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 { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { SubmissionService } from '../../service/submission/submission.service'
import { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe'
import { IdService } from '../../service/id.service'
import { SubmissionSetFieldService } from '../../service/submission/submission.set.field.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class SubmissionFinishMutation {
constructor(
private readonly submissionService: SubmissionService,
private readonly setFieldService: SubmissionSetFieldService,
private readonly idService: IdService,
) {
}
@Mutation(() => SubmissionProgressModel)
async submissionFinish(
@User() user: UserEntity,
@Args({ name: 'submission', type: () => ID }) id: string,
@Args({ name: 'submission', type: () => ID }, SubmissionByIdPipe) submission: SubmissionEntity,
@Context('cache') cache: ContextCache,
): Promise<SubmissionProgressModel> {
const submission = await this.submissionService.findById(id)
await this.setFieldService.finishSubmission(submission)
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)
return new SubmissionProgressModel(submission)
return new SubmissionProgressModel(this.idService.encode(submission.id), submission)
}
}

View File

@ -4,31 +4,31 @@ import { User } from '../../decorator/user.decorator'
import { SubmissionModel } from '../../dto/submission/submission.model'
import { SubmissionPagerFilterInput } from '../../dto/submission/submission.pager.filter.input'
import { SubmissionPagerModel } from '../../dto/submission/submission.pager.model'
import { FormEntity } from '../../entity/form.entity'
import { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormService } from '../../service/form/form.service'
import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe'
import { IdService } from '../../service/id.service'
import { SubmissionService } from '../../service/submission/submission.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class SubmissionListQuery {
constructor(
private readonly formService: FormService,
private readonly submissionService: SubmissionService,
private readonly idService: IdService,
) {
}
@Query(() => SubmissionPagerModel)
async listSubmissions(
@User() user: UserEntity,
@Args('form', {type: () => ID}) id: string,
@Args('form', {type: () => ID}, FormByIdPipe) form: FormEntity,
@Args('start', {type: () => Int, defaultValue: 0, nullable: true}) start: number,
@Args('limit', {type: () => Int, defaultValue: 50, nullable: true}) limit: number,
@Args('filter', {type: () => SubmissionPagerFilterInput, defaultValue: new SubmissionPagerFilterInput()}) filter: SubmissionPagerFilterInput,
@Context('cache') cache: ContextCache,
): Promise<SubmissionPagerModel> {
const form = await this.formService.findById(id)
const [submissions, total] = await this.submissionService.find(
form,
start,
@ -42,7 +42,10 @@ export class SubmissionListQuery {
})
return new SubmissionPagerModel(
submissions.map(submission => new SubmissionModel(submission)),
submissions.map(submission => new SubmissionModel(
this.idService.encode(submission.id),
submission
)),
total,
limit,
start,

View File

@ -4,8 +4,9 @@ import { User } from '../../decorator/user.decorator'
import { SubmissionModel } from '../../dto/submission/submission.model'
import { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe'
import { FormService } from '../../service/form/form.service'
import { SubmissionService } from '../../service/submission/submission.service'
import { IdService } from '../../service/id.service'
import { SubmissionTokenService } from '../../service/submission/submission.token.service'
import { ContextCache } from '../context.cache'
@ -13,20 +14,18 @@ import { ContextCache } from '../context.cache'
export class SubmissionQuery {
constructor(
private readonly formService: FormService,
private readonly submissionService: SubmissionService,
private readonly tokenService: SubmissionTokenService,
private readonly idService: IdService,
) {
}
@Query(() => SubmissionModel)
async getSubmissionById(
@User() user: UserEntity,
@Args('id', {type: () => ID}) id: string,
@Args('id', {type: () => ID}, SubmissionByIdPipe) submission: SubmissionEntity,
@Args('token', {nullable: true}) token: string,
@Context('cache') cache: ContextCache,
): Promise<SubmissionModel> {
const submission = await this.submissionService.findById(id)
if (
!await this.tokenService.verify(token, submission.tokenHash)
&& !this.formService.isAdmin(submission.form, user)
@ -36,6 +35,6 @@ export class SubmissionQuery {
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)
return new SubmissionModel(submission)
return new SubmissionModel(this.idService.encode(submission.id), submission)
}
}

View File

@ -5,10 +5,16 @@ import { SubmissionModel } from '../../dto/submission/submission.model'
import { SubmissionEntity } from '../../entity/submission.entity'
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
import { UserEntity } from '../../entity/user.entity'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Resolver(() => SubmissionModel)
export class SubmissionResolver {
constructor(
private readonly idService: IdService,
) {
}
@ResolveField(() => [SubmissionFieldModel])
async fields(
@User() user: UserEntity,
@ -16,12 +22,12 @@ export class SubmissionResolver {
@Context('cache') cache: ContextCache,
): Promise<SubmissionFieldModel[]> {
const submission = await cache.get<SubmissionEntity>(
cache.getCacheKey(SubmissionEntity.name, parent.id)
cache.getCacheKey(SubmissionEntity.name, parent._id)
)
return submission.fields.map(field => {
cache.add(cache.getCacheKey(SubmissionFieldEntity.name, field.id), field)
return new SubmissionFieldModel(field)
return new SubmissionFieldModel(this.idService.encode(field.id), field)
})
}
}

View File

@ -5,6 +5,8 @@ import { SubmissionProgressModel } from '../../dto/submission/submission.progres
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input'
import { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe'
import { IdService } from '../../service/id.service'
import { SubmissionService } from '../../service/submission/submission.service'
import { SubmissionSetFieldService } from '../../service/submission/submission.set.field.service'
import { ContextCache } from '../context.cache'
@ -14,18 +16,17 @@ export class SubmissionSetFieldMutation {
constructor(
private readonly submissionService: SubmissionService,
private readonly setFieldService: SubmissionSetFieldService,
private readonly idService: IdService,
) {
}
@Mutation(() => SubmissionProgressModel)
async submissionSetField(
@User() user: UserEntity,
@Args({ name: 'submission', type: () => ID }) id: string,
@Args({ name: 'submission', type: () => ID }, SubmissionByIdPipe) submission: SubmissionEntity,
@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')
}
@ -34,6 +35,6 @@ export class SubmissionSetFieldMutation {
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)
return new SubmissionProgressModel(submission)
return new SubmissionProgressModel(this.idService.encode(submission.id), submission)
}
}

View File

@ -4,9 +4,12 @@ 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'
import { FormEntity } from '../../entity/form.entity'
import { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormByIdPipe } from '../../pipe/form/form.by.id.pipe'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
import { SubmissionStartService } from '../../service/submission/submission.start.service'
import { ContextCache } from '../context.cache'
@ -15,19 +18,18 @@ export class SubmissionStartMutation {
constructor(
private readonly startService: SubmissionStartService,
private readonly formService: FormService,
private readonly idService: IdService,
) {
}
@Mutation(() => SubmissionProgressModel)
async submissionStart(
@User() user: UserEntity,
@Args({ name: 'form', type: () => ID }) id: string,
@Args({ name: 'form', type: () => ID }, FormByIdPipe) form: FormEntity,
@Args({ name: 'submission', type: () => SubmissionStartInput }) input: SubmissionStartInput,
@IpAddress() ipAddr: string,
@Context('cache') cache: ContextCache,
): Promise<SubmissionProgressModel> {
const form = await this.formService.findById(id)
if (!form.isLive && !this.formService.isAdmin(form, user)) {
throw new Error('invalid form')
}
@ -36,6 +38,6 @@ export class SubmissionStartMutation {
cache.add(cache.getCacheKey(SubmissionEntity.name, submission.id), submission)
return new SubmissionProgressModel(submission)
return new SubmissionProgressModel(this.idService.encode(submission.id), submission)
}
}

View File

@ -4,12 +4,15 @@ import { Roles } from '../../decorator/roles.decorator'
import { User } from '../../decorator/user.decorator'
import { DeletedModel } from '../../dto/deleted.model'
import { UserEntity } from '../../entity/user.entity'
import { UserByIdPipe } from '../../pipe/user/user.by.id.pipe'
import { IdService } from '../../service/id.service'
import { UserDeleteService } from '../../service/user/user.delete.service'
@Injectable()
export class UserDeleteMutation {
constructor(
private readonly deleteService: UserDeleteService,
private readonly idService: IdService,
) {
}
@ -17,14 +20,14 @@ export class UserDeleteMutation {
@Roles('admin')
async deleteUser(
@User() auth: UserEntity,
@Args({ name: 'id', type: () => ID}) id: string,
@Args({ name: 'id', type: () => ID}, UserByIdPipe) user: UserEntity,
): Promise<DeletedModel> {
if (auth.id.toString() === id) {
if (auth.id === user.id) {
throw new Error('cannot delete your own user')
}
await this.deleteService.delete(id)
await this.deleteService.delete(user.id)
return new DeletedModel(id)
return new DeletedModel(this.idService.encode(user.id))
}
}

View File

@ -3,6 +3,7 @@ import { Roles } from '../../decorator/roles.decorator'
import { UserModel } from '../../dto/user/user.model'
import { UserPagerModel } from '../../dto/user/user.pager.model'
import { UserEntity } from '../../entity/user.entity'
import { IdService } from '../../service/id.service'
import { UserService } from '../../service/user/user.service'
import { ContextCache } from '../context.cache'
@ -10,6 +11,7 @@ import { ContextCache } from '../context.cache'
export class UserListQuery {
constructor(
private readonly userService: UserService,
private readonly idService: IdService,
) {
}
@ -25,7 +27,7 @@ export class UserListQuery {
return new UserPagerModel(
entities.map(entity => {
cache.add(cache.getCacheKey(UserEntity.name, entity.id), entity)
return new UserModel(entity)
return new UserModel(this.idService.encode(entity.id), entity)
}),
total,
limit,

View File

@ -3,26 +3,25 @@ import { Args, Context, ID, Query } from '@nestjs/graphql'
import { Roles } from '../../decorator/roles.decorator'
import { UserModel } from '../../dto/user/user.model'
import { UserEntity } from '../../entity/user.entity'
import { UserService } from '../../service/user/user.service'
import { UserByIdPipe } from '../../pipe/user/user.by.id.pipe'
import { IdService } from '../../service/id.service'
import { ContextCache } from '../context.cache'
@Injectable()
export class UserQuery {
constructor(
private readonly userService: UserService,
private readonly idService: IdService,
) {
}
@Query(() => UserModel)
@Roles('admin')
public async getUserById(
@Args('id', {type: () => ID}) id: string,
public getUserById(
@Args('id', {type: () => ID}, UserByIdPipe) user: UserEntity,
@Context('cache') cache: ContextCache,
): Promise<UserModel> {
const user = await this.userService.findById(id)
): UserModel {
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new UserModel(user)
return new UserModel(this.idService.encode(user.id), user)
}
}

View File

@ -21,7 +21,7 @@ export class UserResolver {
@Context('cache') cache: ContextCache,
): Promise<string[]> {
return this.returnFieldForSuperuser(
await cache.get<UserEntity>(cache.getCacheKey(UserEntity.name, parent.id)),
await cache.get<UserEntity>(cache.getCacheKey(UserEntity.name, parent._id)),
user,
c => c.roles
)

View File

@ -5,6 +5,7 @@ import { User } from '../../decorator/user.decorator'
import { UserModel } from '../../dto/user/user.model'
import { UserUpdateInput } from '../../dto/user/user.update.input'
import { UserEntity } from '../../entity/user.entity'
import { IdService } from '../../service/id.service'
import { UserService } from '../../service/user/user.service'
import { UserUpdateService } from '../../service/user/user.update.service'
import { ContextCache } from '../context.cache'
@ -14,6 +15,7 @@ export class UserUpdateMutation {
constructor(
private readonly updateService: UserUpdateService,
private readonly userService: UserService,
private readonly idService: IdService,
) {
}
@ -28,12 +30,12 @@ export class UserUpdateMutation {
throw new Error('cannot update your own user')
}
const user = await this.userService.findById(input.id)
const user = await this.userService.findById(this.idService.decode(input.id))
await this.updateService.update(user, input)
cache.add(cache.getCacheKey(UserEntity.name, user.id), user)
return new UserModel(user)
return new UserModel(this.idService.encode(user.id), user)
}
}

View File

@ -3,13 +3,16 @@ import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { FormCreateInput } from '../../dto/form/form.create.input'
import { FormEntity } from '../../entity/form.entity'
import { PageEntity } from '../../entity/page.entity'
import { UserEntity } from '../../entity/user.entity'
import { FormPageCreateService } from './form.page.create.service'
@Injectable()
export class FormCreateService {
constructor(
@InjectRepository(FormEntity)
private readonly formRepository: Repository<FormEntity>
private readonly formRepository: Repository<FormEntity>,
private readonly formPageCreateService: FormPageCreateService,
) {
}
@ -23,9 +26,14 @@ export class FormCreateService {
form.language = input.language || 'en'
form.design.layout = input.layout
form.endPage = this.formPageCreateService.create(input.endPage)
form.startPage = this.formPageCreateService.create(input.startPage)
form.admin = admin
return await this.formRepository.save(form)
}
}

View File

@ -14,15 +14,13 @@ export class FormDeleteService {
) {
}
async delete(id: string): Promise<void> {
await this.submissionRepository.createQueryBuilder('s')
.delete()
.where('s.form = :form', { form: id })
.execute()
async delete(id: number): Promise<void> {
await this.submissionRepository.delete({
form: new FormEntity({ id }),
})
await this.formRepository.createQueryBuilder('f')
.delete()
.where('f.id = :form', { form: id })
.execute()
await this.formRepository.delete({
id,
})
}
}

View File

@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common'
import { PageInput } from '../../dto/form/page.input'
import { PageButtonEntity } from '../../entity/page.button.entity'
import { PageEntity } from '../../entity/page.entity'
import { IdService } from '../id.service'
@Injectable()
export class FormPageCreateService {
constructor(
private readonly idService: IdService,
) {
}
public create(input: PageInput): PageEntity {
const page = new PageEntity()
page.show = Boolean(input?.show)
page.buttons = []
if (!input) {
return page
}
page.title = input.title
page.buttonText = input.buttonText
page.paragraph = input.paragraph
if (input.buttons !== undefined) {
page.buttons = input.buttons.map(buttonInput => {
const button = new PageButtonEntity()
button.page = page
button.url = buttonInput.url
button.action = buttonInput.action
button.text = buttonInput.text
button.color = buttonInput.color
button.bgColor = buttonInput.bgColor
button.activeColor = buttonInput.activeColor
return button
})
}
return page
}
}

View File

@ -0,0 +1,73 @@
import { Injectable } from '@nestjs/common'
import { PageInput } from '../../dto/form/page.input'
import { PageButtonEntity } from '../../entity/page.button.entity'
import { PageEntity } from '../../entity/page.entity'
import { IdService } from '../id.service'
@Injectable()
export class FormPageUpdateService {
constructor(
private readonly idService: IdService,
) {
}
public update(page: PageEntity, input: PageInput): PageEntity {
if (!page) {
page = new PageEntity()
page.show = false
}
if (input.show !== undefined) {
page.show = input.show
}
if (input.title !== undefined) {
page.title = input.title
}
if (input.paragraph !== undefined) {
page.paragraph = input.paragraph
}
if (input.buttonText !== undefined) {
page.buttonText = input.buttonText
}
if (input.buttons !== undefined) {
page.buttons = input.buttons.map(buttonInput => {
const entity = this.findByIdInList(
page?.buttons,
buttonInput.id,
new PageButtonEntity()
)
entity.page = page
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
})
}
return page
}
private findByIdInList<T extends { id: number }>(list: T[], id: string, fallback: T): T {
if (!list || /^NEW-/.test(id)) {
return fallback
}
const found = list.find((value) => value.id === this.idService.decode(id))
if (found) {
return found
}
return fallback
}
}

View File

@ -10,6 +10,8 @@ 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'
import { IdService } from '../id.service'
import { FormPageUpdateService } from './form.page.update.service'
@Injectable()
export class FormUpdateService {
@ -20,6 +22,8 @@ export class FormUpdateService {
private readonly formFieldRepository: Repository<FormFieldEntity>,
@InjectRepository(FormHookEntity)
private readonly formHookRepository: Repository<FormHookEntity>,
private readonly idService: IdService,
private readonly pageService: FormPageUpdateService,
) {
}
@ -259,87 +263,11 @@ export class FormUpdateService {
*/
if (input.startPage !== undefined) {
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 = this.findByIdInList(
form.startPage?.buttons,
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
})
}
form.startPage = this.pageService.update(form.startPage, input.startPage)
}
if (input.endPage !== undefined) {
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 = this.findByIdInList(
form.endPage?.buttons,
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
})
}
form.endPage = this.pageService.update(form.endPage, input.endPage)
}
await this.formRepository.save(form)
@ -347,12 +275,12 @@ export class FormUpdateService {
return form
}
private findByIdInList<T>(list: T[], id: string, fallback: T): T {
if (!list) {
private findByIdInList<T extends { id: number }>(list: T[], id: string, fallback: T): T {
if (!list || /^NEW-/.test(id) || !id) {
return fallback
}
const found = list.find((value: any) => String(value.id) === String(id))
const found = list.find((value) => value.id === this.idService.decode(id))
if (found) {
return found

View File

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

32
src/service/id.service.ts Normal file
View File

@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import Hashids from 'hashids'
@Injectable()
export class IdService {
private readonly hashids: Hashids
constructor(
readonly config: ConfigService
) {
this.hashids = new Hashids(config.get('SECRET_KEY'), 6)
}
public encode(id: number): string {
return this.hashids.encode([ id ])
}
public decode(raw: string): number {
if (!this.hashids.isValidId(raw)) {
throw new Error('invalid id passed')
}
const results: number[] = this.hashids.decode(raw) as number[]
if (results[0] === undefined) {
throw new Error('invalid id passed')
}
return results[0]
}
}

View File

@ -5,6 +5,7 @@ import Redis from 'ioredis'
import { PinoLogger } from 'nestjs-pino'
import { authServices } from './auth'
import { formServices } from './form'
import { IdService } from './id.service'
import { InstallationMetricsService } from './installation.metrics.service'
import { MailService } from './mail.service'
import { profileServices } from './profile'
@ -44,4 +45,5 @@ export const services = [
})
},
},
IdService,
]

View File

@ -25,12 +25,12 @@ export class SubmissionNotificationService {
try {
const to = this.getEmail(
submission,
notification.toField.id,
notification.toField?.id,
notification.toEmail
)
const from = this.getEmail(
submission,
notification.fromField.id,
notification.fromField?.id,
notification.fromEmail
)
@ -73,14 +73,18 @@ export class SubmissionNotificationService {
}
private getEmail(submission: SubmissionEntity, fieldId: number, fallback: string): string {
if (!fieldId) {
return fallback
}
const data = submission.fields.find(field => field.fieldId === fieldId)?.content
if (!data) {
return fallback
}
if (typeof data.value === 'string') {
return data.value
if (typeof data === 'string') {
return data
}
return fallback

View File

@ -58,7 +58,7 @@ export class SubmissionService {
return await qb.getManyAndCount()
}
async findById(id: string): Promise<SubmissionEntity> {
async findById(id: number): Promise<SubmissionEntity> {
const submission = await this.submissionRepository.findOne(id);
if (!submission) {

View File

@ -7,6 +7,7 @@ import { Repository } from 'typeorm'
import { SubmissionSetFieldInput } from '../../dto/submission/submission.set.field.input'
import { SubmissionEntity } from '../../entity/submission.entity'
import { SubmissionFieldContent, SubmissionFieldEntity } from '../../entity/submission.field.entity'
import { IdService } from '../id.service'
import { SubmissionHookService } from './submission.hook.service'
import { SubmissionNotificationService } from './submission.notification.service'
@ -19,13 +20,16 @@ export class SubmissionSetFieldService {
private readonly submissionFieldRepository: Repository<SubmissionFieldEntity>,
private readonly webHook: SubmissionHookService,
private readonly notifications: SubmissionNotificationService,
private readonly idService: IdService,
private readonly logger: PinoLogger,
) {
logger.setContext(this.constructor.name)
}
async saveField(submission: SubmissionEntity, input: SubmissionSetFieldInput): Promise<void> {
let field = submission.fields.find(field => field.field.id.toString() === input.field)
const formFieldId = this.idService.decode(input.field)
let field = submission.fields.find(field => field.field.id === formFieldId)
submission.timeElapsed = dayjs().diff(dayjs(submission.created), 'second')
@ -38,7 +42,7 @@ export class SubmissionSetFieldService {
field = new SubmissionFieldEntity()
field.submission = submission
field.field = submission.form.fields.find(field => field.id.toString() === input.field)
field.field = submission.form.fields.find(field => field.id === formFieldId)
field.type = field.field.type
field.content = this.parseData(field, input.data)
@ -84,7 +88,7 @@ export class SubmissionSetFieldService {
field: SubmissionFieldEntity,
data: string
): SubmissionFieldContent {
let raw: { [key: string]: unknown }
let raw: SubmissionFieldContent
const context = {
field: field.fieldId,
@ -92,21 +96,49 @@ export class SubmissionSetFieldService {
}
try {
raw = JSON.parse(data) as { [key: string]: unknown }
raw = JSON.parse(data) as SubmissionFieldContent
} catch (e) {
this.logger.warn(context, 'received invalid data for field')
return { value: null }
}
if (typeof raw !== 'object' || Array.isArray(raw)) {
this.logger.warn(context, 'only object supported for data')
return { value: null }
if (Array.isArray(raw)) {
return raw.map((row: unknown, index) => {
switch (typeof row) {
case 'number':
case 'string':
case 'boolean':
case 'undefined':
return row
}
if (row === null) {
return row
}
this.logger.warn({
...context,
path: `${index}`,
}, 'invalid data in array')
valid = false
return null
})
}
if (
[
'number',
'string',
'boolean',
'undefined',
].includes(typeof raw)
) {
return raw
}
// now ensure data structure
const result = {
value: null,
}
const result = {}
let valid = true
@ -147,6 +179,48 @@ export class SubmissionSetFieldService {
return
}
if (typeof value === 'object') {
result[String(key)] = {}
for (const subKey of Object.keys(value)) {
const subValue = raw[String(key)][String(subKey)]
switch (typeof subValue) {
case 'number':
case 'string':
case 'boolean':
result[String(key)][String(subKey)] = subValue
return
}
if (Array.isArray(subValue)) {
result[String(key)][String(subKey)] = subValue.map((row: unknown, index) => {
switch (typeof row) {
case 'number':
case 'string':
case 'boolean':
case 'undefined':
return row
}
if (row === null) {
return row
}
this.logger.warn({
...context,
path: `${key}/${subKey}/${index}`,
}, 'invalid data in array')
valid = false
return null
})
return
}
}
}
this.logger.warn({
...context,
path: String(key),

View File

@ -11,7 +11,7 @@ export class UserDeleteService {
) {
}
async delete(id: string): Promise<void> {
async delete(id: number): Promise<void> {
await this.userRepository.delete(id)
}
}

View File

@ -26,7 +26,7 @@ export class UserService {
return await qb.getManyAndCount()
}
async findById(id: string): Promise<UserEntity> {
async findById(id: number): Promise<UserEntity> {
const user = await this.userRepository.findOne(id);
if (!user) {

View File

@ -16,15 +16,15 @@ export class UserUpdateService {
}
async update(user: UserEntity, input: UserUpdateInput): Promise<UserEntity> {
if (input.firstName !== undefined) {
if (this.shouldUpdate(input, user, 'firstName')) {
user.firstName = input.firstName
}
if (input.lastName !== undefined) {
if (this.shouldUpdate(input, user, 'lastName')) {
user.lastName = input.lastName
}
if (input.email !== undefined) {
if (this.shouldUpdate(input, user, 'email')) {
user.email = input.email
user.emailVerified = false
// TODO request email verification
@ -34,15 +34,15 @@ export class UserUpdateService {
}
}
if (input.username !== undefined) {
if (this.shouldUpdate(input, user, 'username')) {
user.username = input.username
}
if (input.roles !== undefined) {
if (this.shouldUpdate(input, user, 'roles')) {
user.roles = input.roles as rolesType
}
if (input.language !== undefined) {
if (this.shouldUpdate(input, user, 'language')) {
user.language = input.language
}
@ -54,4 +54,16 @@ export class UserUpdateService {
return user
}
private shouldUpdate(
input: UserUpdateInput,
user: UserEntity,
property: keyof UserUpdateInput
): boolean {
if (input[property] === undefined) {
return false
}
return input[property] == user[property]
}
}

345
yarn.lock
View File

@ -532,6 +532,11 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@gar/promisify@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@graphql-tools/merge@8.2.2":
version "8.2.2"
resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.2.2.tgz#433566c662a33f5a9c3cc5f3ce3753fb0019477a"
@ -1070,6 +1075,22 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@npmcli/fs@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.0.tgz#f2a21c28386e299d1a9fae8051d35ad180e33109"
integrity sha512-DmfBvNXGaetMxj9LTp8NAN9vEidXURrf5ZTslQzEAi/6GbW+4yjaLFQc6Tue5cpZ9Frlk4OBo/Snf1Bh/S7qTQ==
dependencies:
"@gar/promisify" "^1.1.3"
semver "^7.3.5"
"@npmcli/move-file@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
dependencies:
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@nuxtjs/opencollective@0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c"
@ -1164,6 +1185,11 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
@ -1356,7 +1382,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@27.4.1":
"@types/jest@^27.4.1":
version "27.4.1"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
@ -1837,6 +1863,23 @@ agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2:
dependencies:
debug "4"
agentkeepalive@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717"
integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==
dependencies:
debug "^4.1.0"
depd "^1.1.2"
humanize-ms "^1.2.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
dependencies:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv-formats@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
@ -2064,6 +2107,14 @@ are-we-there-yet@^2.0.0:
delegates "^1.0.0"
readable-stream "^3.6.0"
are-we-there-yet@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz#ba20bd6b553e31d62fc8c31bd23d22b95734390d"
integrity sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==
dependencies:
delegates "^1.0.0"
readable-stream "^3.6.0"
are-we-there-yet@~1.1.2:
version "1.1.7"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
@ -2436,6 +2487,30 @@ bytes@3.1.1:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a"
integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==
cacache@^16.0.2:
version "16.0.3"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.0.3.tgz#0b6314bde969bd4098b03a5f90a351e8a1483f48"
integrity sha512-eC7wYodNCVb97kuHGk5P+xZsvUJHkhSEOyNwkenqQPAsOtrTjvWOE5vSPNBpz9d8X3acIf6w2Ub5s4rvOCTs4g==
dependencies:
"@npmcli/fs" "^2.1.0"
"@npmcli/move-file" "^1.1.2"
chownr "^2.0.0"
fs-minipass "^2.1.0"
glob "^7.2.0"
infer-owner "^1.0.4"
lru-cache "^7.7.1"
minipass "^3.1.6"
minipass-collect "^1.0.2"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
mkdirp "^1.0.4"
p-map "^4.0.0"
promise-inflight "^1.0.1"
rimraf "^3.0.2"
ssri "^8.0.1"
tar "^6.1.11"
unique-filename "^1.1.1"
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@ -2645,6 +2720,11 @@ clean-css@^4.2.1:
dependencies:
source-map "~0.6.0"
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@ -2741,7 +2821,7 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-support@^1.1.2:
color-support@^1.1.2, color-support@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
@ -3093,7 +3173,7 @@ denque@^2.0.1:
resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a"
integrity sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==
depd@~1.1.2:
depd@^1.1.2, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
@ -3350,6 +3430,13 @@ encoding-japanese@1.0.30:
resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-1.0.30.tgz#537c4d62881767925d601acb4c79fb14db81703a"
integrity sha512-bd/DFLAoJetvv7ar/KIpE3CNO8wEuyrt9Xuw6nSMiZ+Vrz/Q21BPsMHvARL2Wz6IKHKXgb+DWZqtRg1vql9cBg==
encoding@^0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
dependencies:
iconv-lite "^0.6.2"
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@ -3375,6 +3462,16 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
err-code@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -4059,7 +4156,7 @@ fs-minipass@^1.2.7:
dependencies:
minipass "^2.6.0"
fs-minipass@^2.0.0:
fs-minipass@^2.0.0, fs-minipass@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
@ -4124,6 +4221,20 @@ gauge@^3.0.0:
strip-ansi "^6.0.1"
wide-align "^1.1.2"
gauge@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.3.tgz#286cf105c1962c659f0963058fb05116c1b82d3f"
integrity sha512-ICw1DhAwMtb22rYFwEHgJcx1JCwJGv3x6G0OQUq56Nge+H4Q8JEwr8iveS0XFlsUNSI67F5ffMGK25bK4Pmskw==
dependencies:
aproba "^1.0.3 || ^2.0.0"
color-support "^1.1.3"
console-control-strings "^1.1.0"
has-unicode "^2.0.1"
signal-exit "^3.0.7"
string-width "^4.2.3"
strip-ansi "^6.0.1"
wide-align "^1.1.5"
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -4239,7 +4350,7 @@ glob@7.1.7:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@ -4280,7 +4391,7 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
graceful-fs@^4.2.9:
graceful-fs@^4.2.6, graceful-fs@^4.2.9:
version "4.2.9"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
@ -4392,6 +4503,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hashids@^2.2.10:
version "2.2.10"
resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.10.tgz#82f45538cf03ce73e31b78d1abe78d287cf760c4"
integrity sha512-nXnYums7F8B5Y+GSThutLPlKMaamW1yjWNZVt0WModiJfdjaDZHnhYTWblS+h1OoBx3yjwiBwxldPP3nIbFSSA==
he@1.2.0, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@ -4502,6 +4618,11 @@ htmlparser2@^6.1.0:
domutils "^2.5.2"
entities "^2.0.0"
http-cache-semantics@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http-errors@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
@ -4522,6 +4643,15 @@ http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
http-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
dependencies:
"@tootallnate/once" "2"
agent-base "6"
debug "4"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@ -4549,6 +4679,13 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=
dependencies:
ms "^2.0.0"
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -4563,7 +4700,7 @@ iconv-lite@0.6.2:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@0.6.3, iconv-lite@^0.6.3:
iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@ -4613,6 +4750,16 @@ imurmurhash@^0.1.4:
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
infer-owner@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -4839,6 +4986,11 @@ is-interactive@^1.0.0:
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
is-lambda@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=
is-negative-zero@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@ -5923,6 +6075,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
lru-cache@^7.7.1:
version "7.7.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.7.1.tgz#03d2846b1ad2dcc7931a9340b8711d9798fcb0c6"
integrity sha512-cRffBiTW8s73eH4aTXqBcTLU0xQnwGV3/imttRHGWCrbergmnK4D6JXQd8qin5z43HnDwRI+o7mVW0LEB+tpAw==
macos-release@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2"
@ -5971,6 +6128,28 @@ make-error@1.x, make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
make-fetch-happen@^10.0.3:
version "10.1.0"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.1.0.tgz#7232ef7002a635c04d4deac325df144f21871408"
integrity sha512-HeP4QlkadP/Op+hE+Une1070kcyN85FshQObku3/rmzRh4zDcKXA19d2L3AQR6UoaX3uZmhSOpTLH15b1vOFvQ==
dependencies:
agentkeepalive "^4.2.1"
cacache "^16.0.2"
http-cache-semantics "^4.1.0"
http-proxy-agent "^5.0.0"
https-proxy-agent "^5.0.0"
is-lambda "^1.0.1"
lru-cache "^7.7.1"
minipass "^3.1.6"
minipass-collect "^1.0.2"
minipass-fetch "^2.0.3"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
negotiator "^0.6.3"
promise-retry "^2.0.1"
socks-proxy-agent "^6.1.1"
ssri "^8.0.1"
makeerror@1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"
@ -6074,6 +6253,45 @@ minimist@1.2.5, minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minipass-collect@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
dependencies:
minipass "^3.0.0"
minipass-fetch@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.0.tgz#ca1754a5f857a3be99a9271277246ac0b44c3ff8"
integrity sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==
dependencies:
minipass "^3.1.6"
minipass-sized "^1.0.3"
minizlib "^2.1.2"
optionalDependencies:
encoding "^0.1.13"
minipass-flush@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
dependencies:
minipass "^3.0.0"
minipass-pipeline@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
dependencies:
minipass "^3.0.0"
minipass-sized@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
dependencies:
minipass "^3.0.0"
minipass@^2.6.0, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
@ -6082,7 +6300,7 @@ minipass@^2.6.0, minipass@^2.9.0:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^3.0.0:
minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==
@ -6096,7 +6314,7 @@ minizlib@^1.3.3:
dependencies:
minipass "^2.9.0"
minizlib@^2.1.1:
minizlib@^2.1.1, minizlib@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
@ -6468,7 +6686,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.1.1:
ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@ -6551,6 +6769,11 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
negotiator@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
neo-async@^2.6.0, neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
@ -6625,6 +6848,22 @@ node-gyp@3.x:
tar "^2.0.0"
which "1"
node-gyp@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.0.0.tgz#e1da2067427f3eb5bb56820cb62bc6b1e4bd2089"
integrity sha512-Ma6p4s+XCTPxCuAMrOA/IJRmVy16R8Sdhtwl4PrCr7IBlj4cPawF0vg/l7nOT1jPbuNS7lIRJpBSvVsXwEZuzw==
dependencies:
env-paths "^2.2.0"
glob "^7.1.4"
graceful-fs "^4.2.6"
make-fetch-happen "^10.0.3"
nopt "^5.0.0"
npmlog "^6.0.0"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.2"
which "^2.0.2"
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -6736,6 +6975,16 @@ npmlog@^5.0.1:
gauge "^3.0.0"
set-blocking "^2.0.0"
npmlog@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.1.tgz#06f1344a174c06e8de9c6c70834cfba2964bba17"
integrity sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg==
dependencies:
are-we-there-yet "^3.0.0"
console-control-strings "^1.1.0"
gauge "^4.0.0"
set-blocking "^2.0.0"
nth-check@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
@ -6949,6 +7198,13 @@ p-map@^2.1.0:
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
dependencies:
aggregate-error "^3.0.0"
p-try@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
@ -7336,6 +7592,19 @@ process-warning@^1.0.0:
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616"
integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==
promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
promise-retry@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
dependencies:
err-code "^2.0.2"
retry "^0.12.0"
promise@^7.0.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@ -7805,6 +8074,11 @@ retry@0.13.1:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
retry@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@ -8052,6 +8326,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af"
integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==
signal-exit@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -8067,7 +8346,7 @@ slick@^1.12.2:
resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7"
integrity sha1-vQSN23TefRymkV+qSldXCzVQwtc=
smart-buffer@^4.1.0:
smart-buffer@^4.1.0, smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
@ -8081,6 +8360,15 @@ socks-proxy-agent@5, socks-proxy-agent@^5.0.0:
debug "4"
socks "^2.3.3"
socks-proxy-agent@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87"
integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==
dependencies:
agent-base "^6.0.2"
debug "^4.3.1"
socks "^2.6.1"
socks@^2.3.3:
version "2.6.1"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e"
@ -8089,6 +8377,14 @@ socks@^2.3.3:
ip "^1.1.5"
smart-buffer "^4.1.0"
socks@^2.6.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.2.tgz#ec042d7960073d40d94268ff3bb727dc685f111a"
integrity sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==
dependencies:
ip "^1.1.5"
smart-buffer "^4.2.0"
sonic-boom@^2.2.0, sonic-boom@^2.2.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.4.2.tgz#34c0965b1a498abedaaca794c752d190f74b5e8f"
@ -8169,6 +8465,13 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
ssri@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
dependencies:
minipass "^3.1.1"
stack-utils@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
@ -8442,7 +8745,7 @@ tar@^4:
safe-buffer "^5.2.1"
yallist "^3.1.1"
tar@^6.1.11:
tar@^6.1.11, tar@^6.1.2:
version "6.1.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
@ -8800,6 +9103,20 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
unique-filename@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
dependencies:
unique-slug "^2.0.0"
unique-slug@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
dependencies:
imurmurhash "^0.1.4"
universalify@^0.1.0, universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@ -9050,14 +9367,14 @@ which@1:
dependencies:
isexe "^2.0.0"
which@^2.0.1:
which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0, wide-align@^1.1.2:
wide-align@^1.1.0, wide-align@^1.1.2, wide-align@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==