Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add seeders for development #206

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
implementation("ch.qos.logback:logback-core:1.4.8")
implementation("org.slf4j:slf4j-api:2.0.7")
implementation("com.cloudinary:cloudinary:1.0.14")
implementation("net.datafaker:datafaker:2.2.2")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
implementation("org.springframework.boot:spring-boot-starter-validation:3.1.1")
developmentOnly("org.springframework.boot:spring-boot-devtools")
Expand Down Expand Up @@ -134,6 +135,12 @@ tasks.register<Copy>("generateDocs") {
into(File("docs"))
}

tasks.bootRun {
if (project.hasProperty("seed")) {
systemProperties(mapOf("seed" to "true"))
}
}

tasks.register("fixExamples") {
dependsOn(tasks.named("openapi3"))
doLast {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package pt.up.fe.ni.website.backend.hooks

import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component
import pt.up.fe.ni.website.backend.config.Logging
import pt.up.fe.ni.website.backend.model.seeders.ApplicationSeeder

@Component
class ApplicationStartupHook(
val applicationSeeder: ApplicationSeeder
) : ApplicationRunner, Logging {

@Value("\${app.debug}")
val debug: Boolean = true // false

var seed: Boolean = true // false
Comment on lines +16 to +18
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both of these should be false here by default

Comment on lines +15 to +18
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Value("\${app.debug}")
val debug: Boolean = true // false
var seed: Boolean = true // false
@Value("\${app.debug:false}")
val debug: Boolean = false
@Value("\${app.seed:false}")
private var configSeed: Boolean = false
val seed: Boolean
get() = System.getProperty("seed")?.toBooleanStrictOrNull() ?: configSeed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would allow us to either specify app.seedon application.properties, or override whatever its value is (even null) if we run bootRun with ./gradlew bootRun -Pseed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also defaults to if app.debugor app.seed are missing they are assumed false


fun checkSeedArgument() {
try {
val seedProperty = System.getProperty("seed")
if (seedProperty == "true") {
seed = true
}
} catch (_: NullPointerException) { } catch (_: IllegalArgumentException) {}
}
Comment on lines +20 to +27
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is not being used we can remove it


override fun run(args: ApplicationArguments?) {
logger.info("Running Startup hook...")
// checkSeedArgument()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove here as well

if (debug && seed) {
logger.info("Running application seeder...")
applicationSeeder.seedDatabase()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pt.up.fe.ni.website.backend.model.seeders

import net.datafaker.Faker
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.repository.CrudRepository

@Suppress("SpringJavaInjectionPointsAutowiringInspection")
abstract class AbstractSeeder<T, U, P> where T : CrudRepository<U, P> {

protected val faker = Faker()

@Autowired
protected lateinit var repository: T

abstract fun createObjects()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package pt.up.fe.ni.website.backend.model.seeders

import jakarta.persistence.EntityManager
import jakarta.transaction.Transactional
import java.util.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component
import pt.up.fe.ni.website.backend.config.Logging
import pt.up.fe.ni.website.backend.model.Account
import pt.up.fe.ni.website.backend.repository.AccountRepository
import pt.up.fe.ni.website.backend.repository.CustomWebsiteRepository
import pt.up.fe.ni.website.backend.repository.RoleRepository

@Component @Transactional
class AccountSeeder(
private val encoder: PasswordEncoder,
@Autowired val roleRepository: RoleRepository,
@Autowired val customWebsiteRepository: CustomWebsiteRepository,
@Autowired val entityManager: EntityManager // Inject EntityManager
) : AbstractSeeder<AccountRepository, Account, Long>(), Logging {

override fun createObjects() {
logger.info("Running account seeder...")

// Fetch all roles from the database
val roles = roleRepository.findAll().toList()
if (roles.isEmpty()) {
logger.warn("No roles found in the database. Accounts will be created without roles.")
}

val websites = customWebsiteRepository.findAll().toList()
.map { entityManager.merge(it) }.toMutableList()
if (websites.isEmpty()) {
logger.warn("No Custom websites in database. Accounts will be created without websites")
}

val accounts = listOf(
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = faker.lorem().paragraph(1),
birthDate = Date.from(faker.date().birthday(25, 35).toInstant()),
photo = faker.internet().image(),
github = faker.internet().url(),
websites = websites.shuffled().take(1).toMutableList(),
linkedin = faker.internet().url(),
roles = roles.shuffled().take(2).toMutableList()
),
// Account Without Photo
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = faker.lorem().sentence(),
birthDate = Date.from(faker.date().birthday(30, 40).toInstant()),
photo = null,
github = faker.internet().url(),
linkedin = faker.internet().url(),
roles = roles.shuffled().take(1).toMutableList()
),
// Account Without Bio
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = null,
birthDate = Date.from(faker.date().birthday(18, 28).toInstant()),
photo = faker.internet().image(),
github = faker.internet().url(),
linkedin = faker.internet().url(),
websites = websites.shuffled().take(3).toMutableList(),
roles = roles.shuffled().take(1).toMutableList()
),
// Account Without Bio or Photo
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = null,
birthDate = Date.from(faker.date().birthday(20, 30).toInstant()),
photo = null,
github = null,
linkedin = faker.internet().url(),
websites = websites.shuffled().take(5).toMutableList(),
roles = roles.shuffled().take(1).toMutableList()
),
// Account with Long Bio
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = faker.lorem().paragraph(5),
birthDate = Date.from(faker.date().birthday(35, 45).toInstant()),
photo = faker.internet().image(),
github = faker.internet().url(),
linkedin = faker.internet().url(),
roles = roles.shuffled().take(3).toMutableList()
),
// Account with Single Role
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = "User with only one role.",
birthDate = Date.from(faker.date().birthday(22, 32).toInstant()),
photo = faker.internet().image(),
github = faker.internet().url(),
linkedin = faker.internet().url(),
websites = websites.shuffled().take(2).toMutableList(),
roles = roles.shuffled().take(1).toMutableList()
),
// Account with All Optional Fields Null
Account(
name = faker.name().fullName(),
email = faker.internet().emailAddress(),
password = encoder.encode("password123"),
bio = null,
birthDate = null,
photo = null,
github = null,
linkedin = null,
roles = mutableListOf()
)
)

val repeatedAccounts = mutableListOf<Account>()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you repeat the accounts?

for (i in 1..5) {
repeatedAccounts.addAll(accounts)
}

val staticAccounts = listOf( // edge cases
Account(
name = "Zoë Löwe Dinis Canas", // Special characters in name
email = faker.internet().emailAddress(),
password = encoder.encode("specialpass"),
bio = faker.lorem().sentence(),
birthDate = Date.from(faker.date().birthday(20, 40).toInstant()),
photo = faker.internet().image(),
github = faker.internet().url(),
linkedin = faker.internet().url(),
roles = roles.shuffled().take(2).toMutableList()
)
)

val allAccounts = repeatedAccounts + staticAccounts

repository.saveAll(allAccounts)

logger.info("Account data seeded successfully.")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package pt.up.fe.ni.website.backend.model.seeders

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import pt.up.fe.ni.website.backend.config.Logging
import pt.up.fe.ni.website.backend.model.Event
import pt.up.fe.ni.website.backend.repository.ActivityRepository

@Component
class ActivitySeeder(
@Autowired val eventSeeder: EventSeeder,
@Autowired val projectSeeder: ProjectSeeder,
@Autowired val perActivityRoleSeeder: PerActivityRoleSeeder
) : AbstractSeeder<ActivityRepository<Event>, Event, Long>(), Logging {

override fun createObjects() {
// Delegate seeding to the EventSeeder and ProjectSeeder
logger.info("Running Activity Seeder")

// Seed Roles per Activity
perActivityRoleSeeder.createObjects()
// Seed Projects
projectSeeder.createObjects()
// Seed Events
eventSeeder.createObjects()

logger.info("Finished seeding activities (events and projects)")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package pt.up.fe.ni.website.backend.model.seeders

import jakarta.persistence.EntityManager
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.stereotype.Component
import pt.up.fe.ni.website.backend.service.database.DatabaseService

@Component
class ApplicationSeeder(
private val accountSeeder: AccountSeeder,
private val activitySeeder: ActivitySeeder,
private val postSeeder: PostSeeder,
private val generationSeeder: GenerationSeeder,
private val roleSeeder: RoleSeeder,
private val customWebsiteSeeder: CustomWebsiteSeeder,
@Autowired
private val appContext: ApplicationContext,
private val databaseService: DatabaseService
) {
fun seedDatabase() {
val dataSource = appContext.getBean(DataSource::class.java)
databaseService.cleanDataSource(dataSource)

val entityManager = appContext.getBean(EntityManager::class.java)
databaseService.cleanEntityManager(entityManager)

roleSeeder.createObjects()
customWebsiteSeeder.createObjects()
accountSeeder.createObjects()
activitySeeder.createObjects()
postSeeder.createObjects()
generationSeeder.createObjects()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package pt.up.fe.ni.website.backend.model.seeders

import org.springframework.stereotype.Component
import pt.up.fe.ni.website.backend.config.Logging
import pt.up.fe.ni.website.backend.model.CustomWebsite
import pt.up.fe.ni.website.backend.repository.CustomWebsiteRepository

@Component
class CustomWebsiteSeeder() : AbstractSeeder<CustomWebsiteRepository, CustomWebsite, Long>(), Logging {

override fun createObjects() {
if (repository.count() == 0L) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this if needed?

logger.info("Running CustomWebsite seeder...")

val customWebsites = (1..15).map {
CustomWebsite(
url = faker.internet().url(),
iconPath = faker.internet().image(),
label = faker.lorem().sentence(3, 5)
)
}

repository.saveAll(customWebsites)
logger.info("CustomWebsite data seeded successfully.")
} else {
logger.info("CustomWebsite data already exists, skipping seeding.")
}
}
}
Loading
Loading