speed up seeding

This commit is contained in:
einhornimmond 2025-11-29 11:04:35 +01:00
parent 80079bc2d4
commit 15cd2a6e7f
5 changed files with 187 additions and 89 deletions

View File

@ -1,9 +1,10 @@
import { Contribution, User } from '../../entity'
import { Contribution, Transaction, User } from '../../entity'
import { Decimal } from 'decimal.js-light'
import { CreationInterface } from '../creation/CreationInterface'
import { ContributionType, ContributionStatus, TransactionTypeId } from '../../enum'
import { findUserByIdentifier } from '../../queries'
import { createTransaction } from './transaction'
import { AppDatabase } from '../../AppDatabase'
export function nMonthsBefore(date: Date, months = 1): string {
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()
@ -28,11 +29,42 @@ export async function creationFactory(
if (!moderatorUser) {
throw new Error('Moderator user not found')
}
contribution = await confirmTransaction(contribution, moderatorUser)
await confirmTransaction(contribution, moderatorUser)
}
return contribution
}
export async function creationFactoryBulk(
creations: CreationInterface[],
userCreationIndexedByEmail: Map<string, User>,
moderatorUser: User,
): Promise<Contribution[]> {
const lastTransaction = await Transaction.findOne({ order: { id: 'DESC' }, select: ['id'], where: {} })
let transactionId = lastTransaction ? lastTransaction.id + 1 : 1
const dbContributions: Contribution[] = []
const dbTransactions: Transaction[] = []
for (const creation of creations) {
const user = userCreationIndexedByEmail.get(creation.email)
if (!user) {
throw new Error(`User ${creation.email} not found`)
}
let contribution = await createContribution(creation, user, false)
if (creation.confirmed) {
const { contribution: _, transaction } = await confirmTransaction(contribution, moderatorUser, transactionId, false)
dbTransactions.push(transaction)
transactionId++
}
dbContributions.push(contribution)
}
const dataSource = AppDatabase.getInstance().getDataSource()
await dataSource.transaction(async (transaction) => {
await dataSource.getRepository(Contribution).insert(dbContributions)
await dataSource.getRepository(Transaction).insert(dbTransactions)
})
return dbContributions
}
export async function createContribution(creation: CreationInterface, user: User, store: boolean = true): Promise<Contribution> {
const contribution = new Contribution()
contribution.user = user
@ -47,7 +79,12 @@ export async function createContribution(creation: CreationInterface, user: User
return store ? contribution.save() : contribution
}
export async function confirmTransaction(contribution: Contribution, moderatorUser: User, store: boolean = true): Promise<Contribution> {
export async function confirmTransaction(
contribution: Contribution,
moderatorUser: User,
transactionId?: number,
store: boolean = true
): Promise<{ contribution: Contribution, transaction: Transaction }> {
const now = new Date()
const transaction = await createTransaction(
contribution.amount,
@ -57,7 +94,8 @@ export async function confirmTransaction(contribution: Contribution, moderatorUs
TransactionTypeId.CREATION,
now,
contribution.contributionDate,
true,
transactionId,
store,
)
contribution.confirmedAt = now
contribution.confirmedBy = moderatorUser.id
@ -65,7 +103,12 @@ export async function confirmTransaction(contribution: Contribution, moderatorUs
contribution.transaction = transaction
contribution.contributionStatus = ContributionStatus.CONFIRMED
return store ? contribution.save() : contribution
if (store) {
await contribution.save()
await transaction.save()
}
return { contribution, transaction }
}
function getContributionDate(creation: CreationInterface): Date {

View File

@ -12,7 +12,8 @@ export async function createTransaction(
linkedUser: User,
type: TransactionTypeId,
balanceDate: Date,
creationDate?: Date,
creationDate?: Date,
id?: number,
store: boolean = true,
): Promise<Transaction> {
@ -31,6 +32,9 @@ export async function createTransaction(
newBalance = newBalance.add(amount.toString())
const transaction = new Transaction()
if (id) {
transaction.id = id
}
transaction.typeId = type
transaction.memo = memo
transaction.userId = user.id

View File

@ -1,9 +1,10 @@
import { TransactionLinkInterface } from '../transactionLink/TransactionLinkInterface'
import { TransactionLink } from '../../entity'
import { TransactionLink, User } from '../../entity'
import { Decimal } from 'decimal.js-light'
import { findUserByIdentifier } from '../../queries'
import { compoundInterest } from 'shared'
import { randomBytes } from 'node:crypto'
import { AppDatabase } from '../../AppDatabase'
export async function transactionLinkFactory(
transactionLinkData: TransactionLinkInterface,
@ -19,6 +20,23 @@ export async function transactionLinkFactory(
return createTransactionLink(transactionLinkData, userId)
}
export async function transactionLinkFactoryBulk(
transactionLinks: TransactionLinkInterface[],
userCreationIndexedByEmail: Map<string, User>
): Promise<TransactionLink[]> {
const dbTransactionLinks: TransactionLink[] = []
for (const transactionLink of transactionLinks) {
const user = userCreationIndexedByEmail.get(transactionLink.email)
if (!user) {
throw new Error(`User ${transactionLink.email} not found`)
}
dbTransactionLinks.push(await createTransactionLink(transactionLink, user.id, false))
}
const dataSource = AppDatabase.getInstance().getDataSource()
await dataSource.getRepository(TransactionLink).insert(dbTransactionLinks)
return dbTransactionLinks
}
export async function createTransactionLink(transactionLinkData: TransactionLinkInterface, userId: number, store: boolean = true): Promise<TransactionLink> {
const holdAvailableAmount = compoundInterest(new Decimal(transactionLinkData.amount.toString()), CODE_VALID_DAYS_DURATION * 24 * 60 * 60)
let createdAt = transactionLinkData.createdAt || new Date()

View File

@ -5,13 +5,55 @@ import { UserContactType, OptInType, PasswordEncryptionType } from 'shared'
import { getHomeCommunity } from '../../queries/communities'
import random from 'crypto-random-bigint'
import { Community } from '../../entity'
import { AppDatabase } from '../..'
export async function userFactory(user: UserInterface, homeCommunity?: Community | null): Promise<User> {
let dbUserContact = new UserContact()
dbUserContact.email = user.email ?? ''
dbUserContact.type = UserContactType.USER_CONTACT_EMAIL
// TODO: improve with cascade
let dbUser = await createUser(user, homeCommunity)
let dbUserContact = await createUserContact(user, dbUser.id)
dbUserContact = await dbUserContact.save()
dbUser.emailId = dbUserContact.id
dbUser.emailContact = dbUserContact
dbUser = await dbUser.save()
return dbUser
}
// only use in non-parallel environment (seeding for example)
export async function userFactoryBulk(users: UserInterface[], homeCommunity?: Community | null): Promise<User[]> {
const dbUsers: User[] = []
const dbUserContacts: UserContact[] = []
const lastUser = await User.findOne({ order: { id: 'DESC' }, select: ['id'], where: {} })
const lastUserContact = await UserContact.findOne({ order: { id: 'DESC' }, select: ['id'], where: {} })
let userId = lastUser ? lastUser.id + 1 : 1
let emailId = lastUserContact ? lastUserContact.id + 1 : 1
for(const user of users) {
const dbUser = await createUser(user, homeCommunity, false)
dbUser.id = userId
dbUser.emailId = emailId
const dbUserContact = await createUserContact(user, userId, false)
dbUserContact.id = emailId
dbUserContact.userId = userId
dbUser.emailContact = dbUserContact
dbUsers.push(dbUser)
dbUserContacts.push(dbUserContact)
userId++
emailId++
}
const dataSource = AppDatabase.getInstance().getDataSource()
await dataSource.transaction(async transaction => {
await Promise.all([
transaction.getRepository(User).insert(dbUsers),
transaction.getRepository(UserContact).insert(dbUserContacts)
])
})
return dbUsers
}
export async function createUser(user: UserInterface, homeCommunity?: Community | null, store: boolean = true): Promise<User> {
let dbUser = new User()
dbUser.firstName = user.firstName ?? ''
dbUser.lastName = user.lastName ?? ''
@ -25,9 +67,6 @@ export async function userFactory(user: UserInterface, homeCommunity?: Community
dbUser.gradidoID = v4()
if (user.emailChecked) {
dbUserContact.emailVerificationCode = random(64).toString()
dbUserContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
dbUserContact.emailChecked = true
dbUser.password = random(64)
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
}
@ -38,12 +77,25 @@ export async function userFactory(user: UserInterface, homeCommunity?: Community
dbUser.community = homeCommunity
dbUser.communityUuid = homeCommunity.communityUuid!
}
// TODO: improve with cascade
dbUser = await dbUser.save()
dbUserContact.userId = dbUser.id
dbUserContact = await dbUserContact.save()
dbUser.emailId = dbUserContact.id
dbUser.emailContact = dbUserContact
return dbUser.save()
return store ? dbUser.save() : dbUser
}
export async function createUserContact(user: UserInterface, userId?: number, store: boolean = true): Promise<UserContact> {
let dbUserContact = new UserContact()
dbUserContact.email = user.email ?? ''
dbUserContact.type = UserContactType.USER_CONTACT_EMAIL
if (user.emailChecked) {
dbUserContact.emailVerificationCode = random(64).toString()
dbUserContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
dbUserContact.emailChecked = true
}
if (userId) {
dbUserContact.userId = userId
}
return store ? dbUserContact.save() : dbUserContact
}

View File

@ -1,102 +1,83 @@
import { AppDatabase } from '../AppDatabase'
import { clearDatabase } from '../../migration/clear'
import { createCommunity } from './community'
import { userFactory } from './factory/user'
import { userFactoryBulk } from './factory/user'
import { users } from './users'
import { datatype, internet, name } from 'faker'
import { creationFactory } from './factory/creation'
import { internet, name } from 'faker'
import { creationFactoryBulk } from './factory/creation'
import { creations } from './creation'
import { transactionLinkFactory } from './factory/transactionLink'
import { transactionLinkFactoryBulk } from './factory/transactionLink'
import { transactionLinks } from './transactionLink'
import { contributionLinkFactory } from './factory/contributionLink'
import { contributionLinks } from './contributionLink'
import { User } from '../entity'
import { TransactionLink } from '../entity'
import { ContributionLink } from '../entity'
import { UserInterface } from './users/UserInterface'
const RANDOM_USER_COUNT = 100
async function run() {
const now = new Date()
// clear database, use mysql2 directly, not AppDatabase
await clearDatabase()
console.info('##seed## seeding started...')
const db = AppDatabase.getInstance()
await db.init()
await clearDatabase()
// seed home community
const homeCommunity = await createCommunity(false)
console.info('##seed## seeding home community successful...')
const homeCommunity = await createCommunity(false)
console.info(`##seed## seeding home community successful ...`)
// seed standard users
// start creation of all users in parallel
// put into map for later direct access
const userCreationIndexedByEmail = new Map<string, Promise<User>>()
for (const user of users) {
userCreationIndexedByEmail.set(user.email!, userFactory(user, homeCommunity))
const userCreationIndexedByEmail = new Map<string, User>()
const defaultUsers = await userFactoryBulk(users, homeCommunity)
for (const dbUser of defaultUsers) {
userCreationIndexedByEmail.set(dbUser.emailContact.email, dbUser)
}
const defaultUsersPromise = Promise.all(userCreationIndexedByEmail.values()).then(() => {
// log message after all users are created
console.info('##seed## seeding all standard users successful...')
})
console.info(`##seed## seeding all standard users successful ...`)
// seed 100 random users
// start creation of all random users in parallel
const randomUsersCreation: Promise<User>[] = []
// seed 100 random users
const randomUsers = new Array<UserInterface>(RANDOM_USER_COUNT)
for (let i = 0; i < RANDOM_USER_COUNT; i++) {
randomUsersCreation.push(userFactory({
randomUsers[i] = {
firstName: name.firstName(),
lastName: name.lastName(),
email: internet.email(),
language: datatype.boolean() ? 'en' : 'de',
}, homeCommunity))
language: Math.random() < 0.5 ? 'en' : 'de',
}
}
const randomUsersPromise = Promise.all(randomUsersCreation).then(() => {
// log message after all random users are created
console.info(`##seed## seeding ${RANDOM_USER_COUNT} random users successful...`)
})
// create Contribution Links
// start creation of all contribution links in parallel
const contributionLinksPromises: Promise<ContributionLink>[] = []
for (const contributionLink of contributionLinks) {
contributionLinksPromises.push(contributionLinkFactory(contributionLink))
}
const contributionLinksPromise = Promise.all(contributionLinksPromises).then(() => {
// log message after all contribution links are created
console.info('##seed## seeding all contributionLinks successful...')
})
// create Transaction Links
// start creation of all transaction links in parallel
const transactionLinksPromises: Promise<TransactionLink>[] = []
for (const transactionLink of transactionLinks) {
const user = await userCreationIndexedByEmail.get(transactionLink.email)!
transactionLinksPromises.push(transactionLinkFactory(transactionLink, user.id))
}
const transactionLinksPromise = Promise.all(transactionLinksPromises).then(() => {
// log message after all transaction links are created
console.info('##seed## seeding all transactionLinks successful...')
})
await userFactoryBulk(randomUsers, homeCommunity)
console.info(`##seed## seeding ${RANDOM_USER_COUNT} random users successful ...`)
// create GDD serial, must be called one after another because seeding don't use semaphore
const moderatorUser = await userCreationIndexedByEmail.get('peter@lustig.de')!
for (const creation of creations) {
const user = await userCreationIndexedByEmail.get(creation.email)!
await creationFactory(creation, user, moderatorUser)
}
const moderatorUser = userCreationIndexedByEmail.get('peter@lustig.de')!
await creationFactoryBulk(creations, userCreationIndexedByEmail, moderatorUser)
console.info(`##seed## seeding all creations successful ...`)
// wait for all promises to be resolved
await Promise.all([
defaultUsersPromise,
randomUsersPromise,
contributionLinksPromise,
transactionLinksPromise,
])
// create Contribution Links
for (const contributionLink of contributionLinks) {
await contributionLinkFactory(contributionLink)
}
console.info(`##seed## seeding all contributionLinks successful ...`)
// create Transaction Links
await transactionLinkFactoryBulk(transactionLinks, userCreationIndexedByEmail)
console.info(`##seed## seeding all transactionLinks successful ...`)
await db.destroy()
const timeDiffSeconds = (new Date().getTime() - now.getTime()) / 1000
console.info(`##seed## seeding successful... after ${timeDiffSeconds} seconds`)
console.info(`##seed## seeding successful...`)
}
async function clearDatabase() {
await AppDatabase.getInstance().getDataSource().transaction(async trx => {
await trx.query(`SET FOREIGN_KEY_CHECKS = 0`)
await trx.query(`TRUNCATE TABLE contributions`)
await trx.query(`TRUNCATE TABLE contribution_links`)
await trx.query(`TRUNCATE TABLE users`)
await trx.query(`TRUNCATE TABLE user_contacts`)
await trx.query(`TRUNCATE TABLE transactions`)
await trx.query(`TRUNCATE TABLE transaction_links`)
await trx.query(`TRUNCATE TABLE communities`)
await trx.query(`SET FOREIGN_KEY_CHECKS = 1`)
})
}
run().catch((err) => {