rewrite seed code with direct db usage

This commit is contained in:
einhornimmond 2025-11-28 19:01:28 +01:00
parent 25b344f6a6
commit c87fc511c9
12 changed files with 589 additions and 2 deletions

View File

@ -0,0 +1,7 @@
export interface ContributionLinkInterface {
amount: number
name: string
memo: string
validFrom?: Date
validTo?: Date
}

View File

@ -0,0 +1,18 @@
import { ContributionLinkInterface } from './ContributionLinkInterface'
export const contributionLinks: ContributionLinkInterface[] = [
{
name: 'Dokumenta 2017',
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017',
amount: 200,
validFrom: new Date(2017, 3, 8),
validTo: new Date(2017, 6, 16),
},
{
name: 'Dokumenta 2022',
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
amount: 200,
validFrom: new Date(2022, 5, 18),
validTo: new Date(2022, 8, 25),
},
]

View File

@ -0,0 +1,9 @@
export interface CreationInterface {
email: string
amount: number
memo: string
contributionDate: string
confirmed?: boolean
// number of months to move the confirmed creation to the past
moveCreationDate?: number
}

View File

@ -0,0 +1,155 @@
import { nMonthsBefore } from '../factory/creation'
import { CreationInterface } from './CreationInterface'
const bobsSendings = [
{
amount: 10,
memo: 'Herzlich Willkommen bei Gradido!',
},
{
amount: 10,
memo: 'für deine Hilfe, Betty',
},
{
amount: 23.37,
memo: 'für deine Hilfe, David',
},
{
amount: 47,
memo: 'für deine Hilfe, Frau Holle',
},
{
amount: 1.02,
memo: 'für deine Hilfe, Herr Müller',
},
{
amount: 5.67,
memo: 'für deine Hilfe, Maier',
},
{
amount: 72.93,
memo: 'für deine Hilfe, Elsbeth',
},
{
amount: 5.6,
memo: 'für deine Hilfe, Daniel',
},
{
amount: 8.87,
memo: 'für deine Hilfe, Yoda',
},
{
amount: 7.56,
memo: 'für deine Hilfe, Sabine',
},
{
amount: 7.89,
memo: 'für deine Hilfe, Karl',
},
{
amount: 8.9,
memo: 'für deine Hilfe, Darth Vader',
},
{
amount: 56.79,
memo: 'für deine Hilfe, Luci',
},
{
amount: 3.45,
memo: 'für deine Hilfe, Hanne',
},
{
amount: 8.74,
memo: 'für deine Hilfe, Luise',
},
{
amount: 7.85,
memo: 'für deine Hilfe, Annegred',
},
{
amount: 32.7,
memo: 'für deine Hilfe, Prinz von Zamunda',
},
{
amount: 44.2,
memo: 'für deine Hilfe, Charly Brown',
},
{
amount: 38.17,
memo: 'für deine Hilfe, Michael',
},
{
amount: 5.72,
memo: 'für deine Hilfe, Kaja',
},
{
amount: 3.99,
memo: 'für deine Hilfe, Maja',
},
{
amount: 4.5,
memo: 'für deine Hilfe, Martha',
},
{
amount: 8.3,
memo: 'für deine Hilfe, Ursula',
},
{
amount: 2.9,
memo: 'für deine Hilfe, Urs',
},
{
amount: 4.6,
memo: 'für deine Hilfe, Mecedes',
},
{
amount: 74.1,
memo: 'für deine Hilfe, Heidi',
},
{
amount: 4.5,
memo: 'für deine Hilfe, Peter',
},
{
amount: 5.8,
memo: 'für deine Hilfe, Fräulein Rottenmeier',
},
]
const bobsTransactions: CreationInterface[] = []
bobsSendings.forEach((sending) => {
bobsTransactions.push({
email: 'bob@baumeister.de',
amount: sending.amount,
memo: sending.memo,
contributionDate: nMonthsBefore(new Date()),
confirmed: true,
})
})
export const creations: CreationInterface[] = [
{
email: 'bibi@bloxberg.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
contributionDate: nMonthsBefore(new Date()),
confirmed: true,
moveCreationDate: 12,
},
{
email: 'bibi@bloxberg.de',
amount: 1000,
memo: '#Hexen',
contributionDate: nMonthsBefore(new Date()),
confirmed: true,
moveCreationDate: 12,
},
...bobsTransactions,
{
email: 'raeuber@hotzenplotz.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
contributionDate: nMonthsBefore(new Date()),
confirmed: true,
},
]

View File

@ -0,0 +1,33 @@
import Decimal from 'decimal.js-light'
import { ContributionLink } from '../../entity'
import { ContributionLinkInterface } from '../contributionLink/ContributionLinkInterface'
import { transactionLinkCode } from './transactionLink'
import { ContributionCycleType } from '../../enum'
export const contributionLinkFactory = async (
contributionLink: ContributionLinkInterface,
): Promise<ContributionLink> => {
return createContributionLink(contributionLink)
}
export async function createContributionLink(contributionLinkData: ContributionLinkInterface): Promise<ContributionLink> {
const contributionLink = new ContributionLink()
contributionLink.amount = new Decimal(contributionLinkData.amount)
contributionLink.name = contributionLinkData.name
contributionLink.memo = contributionLinkData.memo
contributionLink.createdAt = new Date()
contributionLink.code = transactionLinkCode(new Date())
contributionLink.cycle = ContributionCycleType.ONCE
if (contributionLinkData.validFrom) {
contributionLink.validFrom = contributionLinkData.validFrom
}
if (contributionLinkData.validTo) {
contributionLink.validTo = contributionLinkData.validTo
}
contributionLink.maxAmountPerMonth = new Decimal(200)
contributionLink.maxPerCycle = 1
return contributionLink.save()
}

View File

@ -0,0 +1,76 @@
import { Contribution, 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'
export function nMonthsBefore(date: Date, months = 1): string {
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()
}
export async function creationFactory(
creation: CreationInterface,
user?: User | null,
moderatorUser?: User | null,
): Promise<Contribution> {
if (!user) {
user = await findUserByIdentifier(creation.email)
}
if (!user) {
throw new Error(`User ${creation.email} not found`)
}
let contribution = await createContribution(creation, user)
if (creation.confirmed) {
if (!moderatorUser) {
moderatorUser = await findUserByIdentifier('peter@lustig.de')
}
if (!moderatorUser) {
throw new Error('Moderator user not found')
}
contribution = await confirmTransaction(contribution, moderatorUser)
}
return contribution
}
export async function createContribution(creation: CreationInterface, user: User, store: boolean = true): Promise<Contribution> {
const contribution = new Contribution()
contribution.user = user
contribution.userId = user.id
contribution.amount = new Decimal(creation.amount)
contribution.createdAt = new Date()
contribution.contributionDate = getContributionDate(creation)
contribution.memo = creation.memo
contribution.contributionType = ContributionType.USER
contribution.contributionStatus = ContributionStatus.PENDING
return store ? contribution.save() : contribution
}
export async function confirmTransaction(contribution: Contribution, moderatorUser: User, store: boolean = true): Promise<Contribution> {
const now = new Date()
const transaction = await createTransaction(
contribution.amount,
contribution.memo,
contribution.user,
moderatorUser,
TransactionTypeId.CREATION,
now,
contribution.contributionDate,
true,
)
contribution.confirmedAt = now
contribution.confirmedBy = moderatorUser.id
contribution.transactionId = transaction.id
contribution.transaction = transaction
contribution.contributionStatus = ContributionStatus.CONFIRMED
return store ? contribution.save() : contribution
}
function getContributionDate(creation: CreationInterface): Date {
if (creation.moveCreationDate) {
return new Date(nMonthsBefore(new Date(creation.contributionDate), creation.moveCreationDate))
}
return new Date(creation.contributionDate)
}

View File

@ -0,0 +1,55 @@
import Decimal from 'decimal.js-light'
import { User, Transaction } from '../../entity'
import { TransactionTypeId } from '../../enum'
import { fullName } from 'shared'
import { getLastTransaction } from '../../queries'
import { calculateDecay, Decay } from 'shared'
export async function createTransaction(
amount: Decimal,
memo: string,
user: User,
linkedUser: User,
type: TransactionTypeId,
balanceDate: Date,
creationDate?: Date,
store: boolean = true,
): Promise<Transaction> {
const lastTransaction = await getLastTransaction(user.id)
// balance and decay calculation
let newBalance = new Decimal(0)
let decay: Decay | null = null
if (lastTransaction) {
decay = calculateDecay(
lastTransaction.balance,
lastTransaction.balanceDate,
balanceDate,
)
newBalance = decay.balance
}
newBalance = newBalance.add(amount.toString())
const transaction = new Transaction()
transaction.typeId = type
transaction.memo = memo
transaction.userId = user.id
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.userCommunityUuid = user.communityUuid
transaction.linkedUserId = linkedUser.id
transaction.linkedUserGradidoID = linkedUser.gradidoID
transaction.linkedUserName = fullName(linkedUser.firstName, linkedUser.lastName)
transaction.linkedUserCommunityUuid = linkedUser.communityUuid
transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = amount
if (creationDate) {
transaction.creationDate = creationDate
}
transaction.balance = newBalance
transaction.balanceDate = balanceDate
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
return store ? transaction.save() : transaction
}

View File

@ -0,0 +1,59 @@
import { TransactionLinkInterface } from '../transactionLink/TransactionLinkInterface'
import { TransactionLink } from '../../entity'
import { Decimal } from 'decimal.js-light'
import { findUserByIdentifier } from '../../queries'
import { compoundInterest } from 'shared'
import { randomBytes } from 'node:crypto'
export async function transactionLinkFactory(
transactionLinkData: TransactionLinkInterface,
userId?: number,
): Promise<TransactionLink> {
if (!userId) {
const user = await findUserByIdentifier(transactionLinkData.email)
if (!user) {
throw new Error(`User ${transactionLinkData.email} not found`)
}
userId = user.id
}
return createTransactionLink(transactionLinkData, userId)
}
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()
const validUntil = transactionLinkExpireDate(createdAt)
const transactionLink = new TransactionLink()
transactionLink.userId = userId
transactionLink.amount = new Decimal(transactionLinkData.amount)
transactionLink.memo = transactionLinkData.memo
transactionLink.holdAvailableAmount = holdAvailableAmount
transactionLink.code = transactionLinkCode(createdAt)
transactionLink.createdAt = createdAt
transactionLink.validUntil = validUntil
if (transactionLinkData.deletedAt) {
transactionLink.deletedAt = new Date(createdAt.getTime() + 1000)
}
return store ? transactionLink.save() : transactionLink
}
////// Transaction Link BUSINESS LOGIC //////
// TODO: move business logic to shared
export const CODE_VALID_DAYS_DURATION = 14
export const transactionLinkExpireDate = (date: Date): Date => {
const validUntil = new Date(date)
return new Date(validUntil.setDate(date.getDate() + CODE_VALID_DAYS_DURATION))
}
export const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16)
return (
randomBytes(12)
.toString('hex')
.substring(0, 24 - time.length) + time
)
}

View File

@ -4,8 +4,9 @@ import { v4 } from 'uuid'
import { UserContactType, OptInType, PasswordEncryptionType } from 'shared'
import { getHomeCommunity } from '../../queries/communities'
import random from 'crypto-random-bigint'
import { Community } from '../../entity'
export const userFactory = async (user: UserInterface): Promise<User> => {
export async function userFactory(user: UserInterface, homeCommunity?: Community | null): Promise<User> {
let dbUserContact = new UserContact()
dbUserContact.email = user.email ?? ''
@ -30,7 +31,9 @@ export const userFactory = async (user: UserInterface): Promise<User> => {
dbUser.password = random(64)
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
}
const homeCommunity = await getHomeCommunity()
if (!homeCommunity) {
homeCommunity = await getHomeCommunity()
}
if (homeCommunity) {
dbUser.community = homeCommunity
dbUser.communityUuid = homeCommunity.communityUuid!
@ -41,5 +44,6 @@ export const userFactory = async (user: UserInterface): Promise<User> => {
dbUserContact = await dbUserContact.save()
dbUser.emailId = dbUserContact.id
dbUser.emailContact = dbUserContact
return dbUser.save()
}

106
database/src/seeds/index.ts Normal file
View File

@ -0,0 +1,106 @@
import { AppDatabase } from '../AppDatabase'
import { clearDatabase } from '../../migration/clear'
import { createCommunity } from './community'
import { userFactory } from './factory/user'
import { users } from './users'
import { datatype, internet, name } from 'faker'
import { creationFactory } from './factory/creation'
import { creations } from './creation'
import { transactionLinkFactory } 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'
const RANDOM_USER_COUNT = 100
async function run() {
const now = new Date()
// clear database, use mysql2 directly, not AppDatabase
await clearDatabase()
const db = AppDatabase.getInstance()
await db.init()
// seed home community
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 defaultUsersPromise = Promise.all(userCreationIndexedByEmail.values()).then(() => {
// log message after all users are created
console.info('##seed## seeding all standard users successful...')
})
// seed 100 random users
// start creation of all random users in parallel
const randomUsersCreation: Promise<User>[] = []
for (let i = 0; i < RANDOM_USER_COUNT; i++) {
randomUsersCreation.push(userFactory({
firstName: name.firstName(),
lastName: name.lastName(),
email: internet.email(),
language: datatype.boolean() ? 'en' : 'de',
}, homeCommunity))
}
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...')
})
// 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)
}
// wait for all promises to be resolved
await Promise.all([
defaultUsersPromise,
randomUsersPromise,
contributionLinksPromise,
transactionLinksPromise,
])
await db.destroy()
const timeDiffSeconds = (new Date().getTime() - now.getTime()) / 1000
console.info(`##seed## seeding successful... after ${timeDiffSeconds} seconds`)
}
run().catch((err) => {
// biome-ignore lint/suspicious/noConsole: no logger present
console.error('error on seeding', err)
})

View File

@ -0,0 +1,10 @@
export interface TransactionLinkInterface {
email: string
amount: number
memo: string
createdAt?: Date
// TODO: for testing
// redeemedAt?: Date
// redeemedBy?: number
deletedAt?: boolean
}

View File

@ -0,0 +1,55 @@
import { TransactionLinkInterface } from './TransactionLinkInterface'
export const transactionLinks: TransactionLinkInterface[] = [
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: 'Leider wollte niemand meine Gradidos haben :(',
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
// TODO: for testing
// memo: `Yeah, eingelöst!`,
// redeemedAt: new Date(2022, 2, 2),
// redeemedBy: not null,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: `Kein Trick, keine Zauberrei,
bei Gradidio sei dabei!`,
},
{
email: 'bibi@bloxberg.de',
amount: 19.99,
memo: 'Da habe ich mich wohl etwas übernommen.',
deletedAt: true,
},
]