implement first draft for send complete transaction

This commit is contained in:
einhorn_b 2023-09-04 10:50:00 +02:00
parent 9f4ce35edc
commit 94b8677625
40 changed files with 477 additions and 85 deletions

View File

@ -134,7 +134,11 @@ describe('transmitTransaction', () => {
const localTransaction = new DbTransaction()
localTransaction.typeId = 12
try {
await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction)
await DltConnectorClient.getInstance()?.transmitTransaction(
localTransaction,
'senderCommunityUUid',
'recipientCommunity',
)
} catch (e) {
expect(e).toMatchObject(
new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()),

View File

@ -78,27 +78,35 @@ export class DltConnectorClient {
* transmit transaction via dlt-connector to iota
* and update dltTransactionId of transaction in db with iota message id
*/
public async transmitTransaction(transaction?: DbTransaction | null): Promise<string> {
if (transaction) {
const typeString = getTransactionTypeString(transaction.typeId)
const secondsSinceEpoch = Math.round(transaction.balanceDate.getTime() / 1000)
const amountString = transaction.amount.toString()
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(sendTransaction, {
input: {
type: typeString,
amount: amountString,
createdAt: secondsSinceEpoch,
public async transmitTransaction(
transaction: DbTransaction,
senderCommunityUuid: string,
recipientCommunityUuid = '',
): Promise<string> {
const typeString = getTransactionTypeString(transaction.typeId)
const milliSecondsSinceEpoch = Math.round(transaction.balanceDate.getTime())
const amountString = transaction.amount.toString()
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(sendTransaction, {
input: {
senderUser: {
uuid: transaction.userGradidoID,
communityUuid: senderCommunityUuid,
},
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.sendTransaction.dltTransactionIdHex
} catch (e) {
throw new LogError('Error send sending transaction to dlt-connector: ', e)
}
} else {
throw new LogError('parameter transaction not set...')
recipientUser: {
uuid: transaction.linkedUserGradidoID,
communityUuid: recipientCommunityUuid,
},
amount: amountString,
type: typeString,
createdAt: milliSecondsSinceEpoch,
},
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.sendTransaction.dltTransactionIdHex
} catch (e) {
throw new LogError('Error send sending transaction to dlt-connector: ', e)
}
}
}

View File

@ -360,7 +360,7 @@ describe('create and send Transactions to DltConnector', () => {
txCREATION3 = await createTxCREATION3(false)
CONFIG.DLT_CONNECTOR = false
await sendTransactionsToDltConnector()
await sendTransactionsToDltConnector('senderCommunityUuid')
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
@ -429,7 +429,7 @@ describe('create and send Transactions to DltConnector', () => {
} as Response<unknown>
})
await sendTransactionsToDltConnector()
await sendTransactionsToDltConnector('senderCommunityUuid')
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
@ -507,7 +507,7 @@ describe('create and send Transactions to DltConnector', () => {
} as Response<unknown>
})
await sendTransactionsToDltConnector()
await sendTransactionsToDltConnector('senderCommunityUuid')
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')

View File

@ -6,7 +6,10 @@ import { DltConnectorClient } from '@/apis/DltConnectorClient'
import { backendLogger as logger } from '@/server/logger'
import { Monitor, MonitorNames } from '@/util/Monitor'
export async function sendTransactionsToDltConnector(): Promise<void> {
export async function sendTransactionsToDltConnector(
senderCommunityUuid: string,
recipientCommunityUuid = '',
): Promise<void> {
logger.info('sendTransactionsToDltConnector...')
// check if this logic is still occupied, no concurrecy allowed
if (!Monitor.isLocked(MonitorNames.SEND_DLT_TRANSACTIONS)) {
@ -24,8 +27,15 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
order: { createdAt: 'ASC', id: 'ASC' },
})
for (const dltTx of dltTransactions) {
if (!dltTx.transaction) {
continue
}
try {
const messageId = await dltConnector.transmitTransaction(dltTx.transaction)
const messageId = await dltConnector.transmitTransaction(
dltTx.transaction,
senderCommunityUuid,
recipientCommunityUuid,
)
const dltMessageId = Buffer.from(messageId, 'hex')
if (dltMessageId.length !== 32) {
logger.error(

View File

@ -0,0 +1,21 @@
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
export function isValidDateString(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isValidDateString',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: string) {
return new Date(value).toString() !== 'Invalid Date'
},
defaultMessage(args: ValidationArguments) {
return `${propertyName} must be a valid date string, ${args.property}`
},
},
})
}
}

View File

@ -0,0 +1,22 @@
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
import { Decimal } from 'decimal.js-light'
export function IsPositiveDecimal(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isPositiveDecimal',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: Decimal) {
return value.greaterThan(0)
},
defaultMessage(args: ValidationArguments) {
return `The ${propertyName} must be a positive value ${args.property}`
},
},
})
}
}

View File

@ -0,0 +1 @@
validator

View File

@ -0,0 +1,3 @@
export class Community {
}

View File

@ -0,0 +1,10 @@
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
import { TransactionBody } from '@/proto/3_3/TransactionBody'
export const create = (body: TransactionBody): GradidoTransaction => {
const transaction = new GradidoTransaction({
bodyBytes: Buffer.from(TransactionBody.encode(body).finish()),
})
// TODO: add correct signature(s)
return transaction
}

View File

@ -0,0 +1,6 @@
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
export abstract class TransactionBase {
// validate if transaction is valid, maybe expensive because depending on level several transactions will be fetched from db
public abstract validate(level: TransactionValidationLevel): boolean
}

View File

@ -0,0 +1,74 @@
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionType } from '@/graphql/enum/TransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { GradidoCreation } from '@/proto/3_3/GradidoCreation'
import { GradidoTransfer } from '@/proto/3_3/GradidoTransfer'
import { TransactionBody } from '@/proto/3_3/TransactionBody'
export const create = (transaction: TransactionDraft): TransactionBody => {
const body = new TransactionBody(transaction)
// TODO: load pubkeys for sender and recipient user from db
switch (transaction.type) {
case TransactionType.CREATION:
body.creation = new GradidoCreation(transaction)
body.data = 'gradidoCreation'
break
case TransactionType.SEND:
body.transfer = new GradidoTransfer(transaction)
body.data = 'gradidoTransfer'
break
case TransactionType.RECEIVE:
body.transfer = new GradidoTransfer(transaction)
body.data = 'gradidoTransfer'
break
default:
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
'transaction type unknown',
)
}
return body
}
export const determineCrossGroupType = ({
senderUser,
recipientUser,
type,
}: TransactionDraft): CrossGroupType => {
if (
recipientUser.communityUuid === '' ||
senderUser.communityUuid === recipientUser.communityUuid ||
type === TransactionType.CREATION
) {
return CrossGroupType.LOCAL
} else if (type === TransactionType.SEND) {
return CrossGroupType.INBOUND
} else if (type === TransactionType.RECEIVE) {
return CrossGroupType.OUTBOUND
}
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
'cannot determine CrossGroupType',
)
}
export const determineOtherGroup = (
type: CrossGroupType,
{ senderUser, recipientUser }: TransactionDraft,
): string => {
switch (type) {
case CrossGroupType.LOCAL:
return ''
case CrossGroupType.INBOUND:
return recipientUser.communityUuid
case CrossGroupType.OUTBOUND:
return senderUser.communityUuid
default:
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
type.toString() + ' for enum CrossGroupType not implemented yet',
)
}
}

View File

@ -0,0 +1,19 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { IsPositive, IsUUID } from 'class-validator'
import { Field, Int, ArgsType } from 'type-graphql'
@ArgsType()
export class User {
@Field(() => String)
@IsUUID('4')
uuid: string
@Field(() => String)
@IsUUID('4')
communityUuid: string
@Field(() => Int, { defaultValue: 1 })
@IsPositive()
accountNr: number
}

View File

@ -1,8 +1,9 @@
export enum AddressType {
NONE = 0, // if no address was found
HUMAN = 1,
PROJECT = 2, // no creations allowed
SUBACCOUNT = 3, // no creations allowed
CRYPTO_ACCOUNT = 4, // user control his keys, no creations
COMMUNITY_ACCOUNT = 5, // community control keys, creations allowed
COMMUNITY_HUMAN = 1, // creation account for human
COMMUNITY_GMW = 2, // community public budget account
COMMUNITY_AUF = 3, // community compensation and environment founds account
COMMUNITY_PROJECT = 4, // no creations allowed
SUBACCOUNT = 5, // no creations allowed
CRYPTO_ACCOUNT = 6, // user control his keys, no creations
}

View File

@ -0,0 +1,10 @@
import { registerEnumType } from 'type-graphql'
export enum TransactionErrorType {
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
}
registerEnumType(TransactionErrorType, {
name: 'TransactionErrorType',
description: 'Transaction Error Type',
})

View File

@ -0,0 +1,15 @@
import { registerEnumType } from 'type-graphql'
export enum TransactionValidationLevel {
SINGLE = 1, // check only the transaction
SINGLE_PREVIOUS = 2, // check also with previous transaction
DATE_RANGE = 3, // check all transaction from within date range by creation automatic the same month
PAIRED = 4, // check paired transaction on another group by cross group transactions
CONNECTED_GROUP = 5, // check all transactions in the group which connected with this transaction address(es)
CONNECTED_BLOCKCHAIN = 6, // check all transactions which connected with this transaction
}
registerEnumType(TransactionValidationLevel, {
name: 'TransactionValidationLevel',
description: 'Transaction Validation Levels',
})

View File

@ -0,0 +1,20 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { InputType, Field } from 'type-graphql'
import { isValidDateString } from '../validator/DateString'
import { IsBoolean, IsUUID } from 'class-validator'
@InputType()
export class AddCommunityDraft {
@Field(() => String)
@IsUUID('4')
communityUuid: string
@Field(() => String)
@isValidDateString()
createdAt: string
@Field(() => Boolean)
@IsBoolean()
foreign: boolean
}

View File

@ -0,0 +1,20 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { IsUUID } from 'class-validator'
import { isValidDateString } from '../validator/DateString'
import { InputType, Field } from 'type-graphql'
@InputType()
export class AddUserDraft {
@Field(() => String)
@IsUUID('4')
uuid: string
@Field(() => String)
@IsUUID('4')
communityUuid: string
@Field(() => String)
@isValidDateString()
createdAt: string
}

View File

@ -0,0 +1,40 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { Decimal } from 'decimal.js-light'
import { TransactionType } from '@enum/TransactionType'
import { InputType, Field } from 'type-graphql'
import { User } from '@arg/User'
import { isValidDateString } from '../validator/DateString'
import { IsPositiveDecimal } from '../validator/Decimal'
import { IsEnum, IsObject, ValidateNested, IsNumber, Min } from 'class-validator'
@InputType()
export class TransactionDraft {
@Field(() => User)
@IsObject()
@ValidateNested()
senderUser: User
@Field(() => User)
@IsObject()
@ValidateNested()
recipientUser: User
@Field(() => Decimal)
@IsPositiveDecimal()
amount: Decimal
@Field(() => TransactionType)
@IsEnum(TransactionType)
type: TransactionType
@Field(() => Number)
@IsNumber()
@Min(9783072000000) // 01.01.2001
createdAt: number // in milliseconds
// only for creation transactions
@Field(() => String, { nullable: true })
@isValidDateString()
targetDate?: string
}

View File

@ -1,21 +0,0 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { Decimal } from 'decimal.js-light'
import { TransactionType } from '../enum/TransactionType'
import { InputType, Field } from 'type-graphql'
@InputType()
export class TransactionInput {
@Field(() => TransactionType)
type: TransactionType
@Field(() => Decimal)
amount: Decimal
@Field(() => Number)
createdAt: number
// @protoField.d(4, 'string')
// @Field(() => Decimal)
// communitySum: Decimal
}

View File

@ -0,0 +1,16 @@
import { ObjectType, Field } from 'type-graphql'
import { TransactionErrorType } from '../enum/TransactionErrorType'
@ObjectType()
export class TransactionError {
constructor(type: TransactionErrorType, message: string) {
this.type = type
this.message = message
}
@Field(() => TransactionErrorType)
type: TransactionErrorType
@Field(() => String)
message: string
}

View File

@ -0,0 +1,21 @@
import { ObjectType, Field } from 'type-graphql'
import { TransactionError } from './TransactionError'
@ObjectType()
export class TransactionResult {
constructor(content: TransactionError | Buffer) {
if (content instanceof TransactionError) {
this.error = content
} else if (content instanceof Buffer) {
this.messageId = content.toString('hex')
}
}
// the error if one happened
@Field(() => TransactionError, { nullable: true })
error?: TransactionError
// if no error happend, the message id of the iota transaction
@Field(() => String, { nullable: true })
messageId?: string
}

View File

@ -1,9 +1,12 @@
import { Resolver, Query, Arg, Mutation } from 'type-graphql'
import { TransactionInput } from '@input/TransactionInput'
import { TransactionBody } from '@proto/TransactionBody'
import { TransactionDraft } from '@input/TransactionDraft'
import { create as createTransactionBody } from '@controller/TransactionBody'
import { create as createGradidoTransaction } from '@controller/GradidoTransaction'
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
@Resolver()
export class TransactionResolver {
@ -21,10 +24,11 @@ export class TransactionResolver {
@Mutation(() => String)
async sendTransaction(
@Arg('data')
transaction: TransactionInput,
transaction: TransactionDraft,
): Promise<string> {
const message = TransactionBody.fromObject(transaction)
const messageBuffer = TransactionBody.encode(message).finish()
const body = createTransactionBody(transaction)
const message = createGradidoTransaction(body)
const messageBuffer = GradidoTransaction.encode(message).finish()
const resultMessage = await iotaSendMessage(messageBuffer)
return resultMessage.messageId
}

View File

@ -0,0 +1 @@
../../../common/src/graphql/validator

View File

@ -0,0 +1,34 @@
import { Field, Message } from '@apollo/protobufjs'
import { GradidoTransaction } from './GradidoTransaction'
import { TimestampSeconds } from './TimestampSeconds'
/*
id will be set by Node server
running_hash will be also set by Node server,
calculated from previous transaction running_hash and this id, transaction and received
*/
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoConfirmedTransaction extends Message<GradidoConfirmedTransaction> {
@Field.d(1, 'uint64')
id: number
@Field.d(2, 'GradidoTransaction')
transaction: GradidoTransaction
@Field.d(3, 'TimestampSeconds')
confirmedAt: TimestampSeconds
@Field.d(4, 'string')
versionNumber: string
@Field.d(5, 'bytes')
runningHash: Buffer
@Field.d(6, 'bytes')
messageId: Buffer
@Field.d(7, 'string')
accountBalance: string
}

View File

@ -2,12 +2,20 @@ import { Field, Message } from '@apollo/protobufjs'
import { TimestampSeconds } from './TimestampSeconds'
import { TransferAmount } from './TransferAmount'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
// need signature from group admin or
// percent of group users another than the receiver
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoCreation extends Message<GradidoCreation> {
constructor(transaction: TransactionDraft) {
super({
recipient: new TransferAmount({ amount: transaction.amount.toString() }),
targetDate: new TimestampSeconds(),
})
}
@Field.d(1, TransferAmount)
public recipient: TransferAmount

View File

@ -1,7 +1,7 @@
import { Field, Message } from '@apollo/protobufjs'
import { GradidoTransfer } from './GradidoTransfer'
import { Timestamp } from './Timestamp'
import { TimestampSeconds } from './TimestampSeconds'
// transaction type for chargeable transactions
// for transaction for people which haven't a account already
@ -23,8 +23,8 @@ export class GradidoDeferredTransfer extends Message<GradidoDeferredTransfer> {
// the decay for amount and the seconds until timeout is lost no matter what happened
// consider is as fee for this service
// rest decay could be transferred back as separate transaction
@Field.d(2, 'Timestamp')
public timeout: Timestamp
@Field.d(2, 'TimestampSeconds')
public timeout: TimestampSeconds
// split for n recipient
// max gradido per recipient? or per transaction with cool down?

View File

@ -1,10 +1,20 @@
import { Field, Message } from '@apollo/protobufjs'
import { TransferAmount } from './TransferAmount'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoTransfer extends Message<GradidoTransfer> {
constructor(transaction: TransactionDraft, coinOrigin?: string) {
super({
sender: new TransferAmount({
amount: transaction.amount.toString(),
communityId: coinOrigin,
}),
})
}
@Field.d(1, TransferAmount)
public sender: TransferAmount

View File

@ -0,0 +1,27 @@
import { Field, Message } from '@apollo/protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class Timestamp extends Message<Timestamp> {
public constructor(input?: Date | number) {
let seconds = 0
let nanoSeconds = 0
if (input instanceof Date) {
seconds = Math.floor(input.getTime() / 1000)
nanoSeconds = (input.getTime() % 1000) * 1000000 // Convert milliseconds to nanoseconds
} else if (typeof input === 'number') {
// Calculate seconds and nanoseconds from milliseconds
seconds = Math.floor(input / 1000)
nanoSeconds = (input % 1000) * 1000000
}
super({ seconds, nanoSeconds })
}
// Number of complete seconds since the start of the epoch
@Field.d(1, 'int64')
public seconds: number
// Number of nanoseconds since the start of the last second
@Field.d(2, 'int32')
public nanoSeconds: number
}

View File

@ -3,6 +3,17 @@ import { Field, Message } from '@apollo/protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class TimestampSeconds extends Message<TimestampSeconds> {
public constructor(input?: Date | number) {
let seconds = 0
// Calculate seconds from milliseconds
if (input instanceof Date) {
seconds = Math.floor(input.getTime() / 1000)
} else if (typeof input === 'number') {
seconds = Math.floor(input / 1000)
}
super({ seconds })
}
// Number of complete seconds since the start of the epoch
@Field.d(1, 'int64')
public seconds: number

View File

@ -1,6 +1,6 @@
import 'reflect-metadata'
import { TransactionType } from '@enum/TransactionType'
import { TransactionInput } from '@input/TransactionInput'
// import { TransactionInput } from '@input/TransactionInput'
import Decimal from 'decimal.js-light'
import { TransactionBody } from './TransactionBody'
import { TimestampSeconds } from './TimestampSeconds'
@ -15,7 +15,7 @@ describe('proto/TransactionBodyTest', () => {
// init both objects
// graphql input object
const transactionInput = new TransactionInput()
/* const transactionInput = new TransactionInput()
transactionInput.type = type
transactionInput.amount = amount
transactionInput.createdAt = createdAt.seconds
@ -34,5 +34,6 @@ describe('proto/TransactionBodyTest', () => {
// compare
expect(messageBuffer).toStrictEqual(messageBuffer2)
*/
})
})

View File

@ -2,28 +2,34 @@ import { Field, Message, OneOf } from '@apollo/protobufjs'
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
import { TimestampSeconds } from './TimestampSeconds'
import { Timestamp } from './Timestamp'
import { GradidoTransfer } from './GradidoTransfer'
import { GradidoCreation } from './GradidoCreation'
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
import { RegisterAddress } from './RegisterAddress'
/*interface OneofExample {
result:
| { oneofKind: 'value'; value: number }
| { oneofKind: 'error'; error: string }
| { oneofKind: undefined }
}*/
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { determineCrossGroupType, determineOtherGroup } from '@/controller/TransactionBody'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class TransactionBody extends Message<TransactionBody> {
public constructor(transaction: TransactionDraft) {
const type = determineCrossGroupType(transaction)
super({
memo: 'Not implemented yet',
createdAt: new Timestamp(transaction.createdAt),
versionNumber: '3.3',
type,
otherGroup: determineOtherGroup(type, transaction),
})
}
@Field.d(1, 'string')
public memo: string
@Field.d(2, TimestampSeconds)
public createdAt: TimestampSeconds
@Field.d(2, Timestamp)
public createdAt: Timestamp
@Field.d(3, 'string')
public versionNumber: string

View File

@ -1,3 +1,4 @@
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { Field, Message } from '@apollo/protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs

View File

@ -1,13 +0,0 @@
import { Field, Message } from '@apollo/protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class Timestamp extends Message<Timestamp> {
// Number of complete seconds since the start of the epoch
@Field.d(1, 'int64')
public seconds: number
// Number of nanoseconds since the start of the last second
@Field.d(2, 'int32')
public nanoSeconds: number
}

View File

@ -51,10 +51,12 @@
"@arg/*": ["src/graphql/arg/*"],
"@enum/*": ["src/graphql/enum/*"],
"@input/*": ["src/graphql/input/*"],
"@model/*": ["src/graphql/model/*"],
"@resolver/*": ["src/graphql/resolver/*"],
"@scalar/*": ["src/graphql/scalar/*"],
"@test/*": ["test/*"],
"@proto/*" : ["src/proto/*"],
"@controller/*": ["src/controller/*"],
/* external */
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */