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

Create report mutation #186

Merged
merged 22 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e7e7351
Send Date with milliseconds for getUploads.
mtammineni Sep 11, 2024
eb1a528
Create Report Mutation
mtammineni Sep 11, 2024
6444ed9
Promoting event-sink-reader from spikes into development
mtammineni Sep 11, 2024
ff51355
Add test configuration for running the unit tests from gradle
mtammineni Sep 12, 2024
2caf485
Update graphql-service-ci.yml
uek3-cdc Sep 12, 2024
b3365ed
Update graphql-service-ci.yml
mtammineni Sep 12, 2024
95dec1b
Add source directories to gradle build
mtammineni Sep 12, 2024
6796c6f
Get latest
mtammineni Sep 12, 2024
33edf6a
Update graphql-service-ci.yml
mtammineni Sep 12, 2024
ad6ef39
Merge branch 'develop' of https://github.com/CDCgov/data-exchange-pro…
mtammineni Sep 12, 2024
ced0dc0
Merge branch 'develop' into create-report-mutation
mtammineni Sep 12, 2024
a500d7c
Handle different types of content. Add class headers. Add function he…
mtammineni Sep 20, 2024
9fd37df
Add description headers. Create enums for the different content types…
mtammineni Sep 20, 2024
28fd0c6
Remove event-sink-reader from this commit
mtammineni Sep 20, 2024
d787262
Updated source sets for tests
mtammineni Sep 20, 2024
950fe39
Update graphql-service-ci.yml
mtammineni Sep 20, 2024
5a92c90
Code refactoring
mtammineni Sep 23, 2024
4fba376
Merge remote-tracking branch 'origin/create-report-mutation' into cre…
mtammineni Sep 23, 2024
3fc8b5a
Code refactoring
mtammineni Sep 23, 2024
5f35a43
Exception handling
mtammineni Sep 24, 2024
46a224f
Revert the changes for exception handling at the top level.
mtammineni Sep 24, 2024
8737e4d
Refactor Report Mutations
mtammineni Sep 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/graphql-service-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Run Gradle Test
run: ./gradlew test
run: ./gradlew test --info
27 changes: 23 additions & 4 deletions pstatus-graphql-ktor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ dependencies {
testImplementation "io.ktor:ktor-server-tests-jvm:$ktor_version"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"

// testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
// testImplementation 'org.mockito:mockito-core:4.6.1'
// testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1'

testImplementation "org.junit.jupiter:junit-jupiter-api:5.8.1"
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.8.1"
testImplementation "org.mockito:mockito-core:4.5.1"
Expand Down Expand Up @@ -108,4 +104,27 @@ jib {
}
}

test {

// Discover and execute JUnit Platform-based (JUnit 5, JUnit Jupiter) tests
// JUnit 5 has the ability to execute JUnit 4 tests as well
useJUnitPlatform()

//Change this to "true" if we want to execute unit tests
systemProperty("isTestEnvironment", "false")

// Set the test classpath, if required
}

sourceSets {
main {
java {
srcDir 'src/kotlin'
}
resources {
srcDir 'src/resources'
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fun KoinApplication.loadKoinModules(environment: ApplicationEnvironment): KoinAp
// Create a CosmosDB config that can be dependency injected (for health checks)
single(createdAtStart = true) { CosmosConfiguration(uri, authKey) }
}

return modules(listOf(cosmosModule))
}

Expand All @@ -34,15 +35,20 @@ fun main(args: Array<String>) {
}

fun Application.module() {
graphQLModule()
configureRouting()

install(Koin) {
loadKoinModules(environment)
}
graphQLModule()
configureRouting()

install(ContentNegotiation) {
jackson()
}




// See https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/writing-schemas/scalars
RuntimeWiring.newRuntimeWiring().scalar(ExtendedScalars.Date)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package gov.cdc.ocio.processingstatusapi.models.reports

import com.expediagroup.graphql.generator.annotations.GraphQLDescription
manu-govind marked this conversation as resolved.
Show resolved Hide resolved
import gov.cdc.ocio.processingstatusapi.models.submission.Aggregation
import gov.cdc.ocio.processingstatusapi.models.submission.Status
import java.time.OffsetDateTime

@GraphQLDescription("Input type for a key-value pair")
data class KeyValueInput(
manu-govind marked this conversation as resolved.
Show resolved Hide resolved
@GraphQLDescription("Key of the key-value pair")
val key: String,

@GraphQLDescription("Value of the key-value pair")
val value: String
)

@GraphQLDescription("Input type for creating or updating a report")
data class ReportInput(
@GraphQLDescription("Identifier of the report recorded by the database")
val id: String? = null,

@GraphQLDescription("Upload identifier this report belongs to")
val uploadId: String? = null,

@GraphQLDescription("Unique report identifier")
val reportId: String? = null,

@GraphQLDescription("Data stream ID")
val dataStreamId: String? = null,

@GraphQLDescription("Data stream route")
val dataStreamRoute: String? = null,

@GraphQLDescription("Date/time of when the upload was first ingested into the data-exchange")
val dexIngestDateTime: OffsetDateTime? = null,

@GraphQLDescription("Message metadata")
val messageMetadata: MessageMetadataInput? = null,

@GraphQLDescription("Stage info")
val stageInfo: StageInfoInput? = null,

@GraphQLDescription("Tags associated with the report")
val tags: List<KeyValueInput>? = null,

@GraphQLDescription("Data associated with the report")
val data: List<KeyValueInput>? = null,

@GraphQLDescription("Indicates the content type of the content; e.g. JSON, XML")
val contentType: String? = null,

@GraphQLDescription("Jurisdiction report belongs to; set to null if not applicable")
val jurisdiction: String? = null,

@GraphQLDescription("Sender ID this report belongs to; set to null if not applicable")
val senderId: String? = null,

@GraphQLDescription("Data Producer ID stated in the report; set to null if not applicable")
val dataProducerId: String? = null,

@GraphQLDescription("Content of the report. If the report is JSON then the content will be shown as JSON. Otherwise, the content is a base64 encoded string.")
val content: List<KeyValueInput>? = null,
manu-govind marked this conversation as resolved.
Show resolved Hide resolved

@GraphQLDescription("Timestamp when the report was recorded in the database")
val timestamp: OffsetDateTime? = null
)

@GraphQLDescription("Input type for message metadata")
data class MessageMetadataInput(
@GraphQLDescription("Unique Identifier for that message")
val messageUUID: String? = null,

@GraphQLDescription("MessageHash value")
val messageHash: String? = null,

@GraphQLDescription("Single or Batch message")
val aggregation: Aggregation? = null,

@GraphQLDescription("Message Index")
val messageIndex: Int? = null
)

@GraphQLDescription("Input type for stage info")
data class StageInfoInput(
@GraphQLDescription("Service")
val service: String? = null,

@GraphQLDescription("Stage name a.k.a action")
val action: String? = null,

@GraphQLDescription("Version")
val version: String? = null,

@GraphQLDescription("Status- SUCCESS OR FAILURE")
val status: Status? = null,

@GraphQLDescription("Issues array")
val issues: List<IssueInput>? = null,

@GraphQLDescription("Start processing time")
val startProcessingTime: OffsetDateTime? = null,

@GraphQLDescription("End processing time")
val endProcessingTime: OffsetDateTime? = null
)

@GraphQLDescription("Input type for issues")
data class IssueInput(
@GraphQLDescription("Issue code")
val code: String? = null,

@GraphQLDescription("Issue description")
val description: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package gov.cdc.ocio.processingstatusapi.mutations

import com.azure.cosmos.models.CosmosItemRequestOptions
import com.azure.cosmos.models.CosmosItemResponse
import com.azure.cosmos.models.PartitionKey
import gov.cdc.ocio.processingstatusapi.exceptions.BadRequestException
import gov.cdc.ocio.processingstatusapi.exceptions.BadStateException
import gov.cdc.ocio.processingstatusapi.exceptions.ContentException
import gov.cdc.ocio.processingstatusapi.loaders.CosmosLoader
import gov.cdc.ocio.processingstatusapi.models.Report
import gov.cdc.ocio.processingstatusapi.models.reports.IssueInput
import gov.cdc.ocio.processingstatusapi.models.reports.MessageMetadataInput
import gov.cdc.ocio.processingstatusapi.models.reports.ReportInput
import gov.cdc.ocio.processingstatusapi.models.reports.StageInfoInput
import gov.cdc.ocio.processingstatusapi.models.submission.Issue
import gov.cdc.ocio.processingstatusapi.models.submission.Level
import gov.cdc.ocio.processingstatusapi.models.submission.MessageMetadata
import gov.cdc.ocio.processingstatusapi.models.submission.StageInfo

class ReportMutation() : CosmosLoader() {

/**
* Mutation for creating or replacing a report based on the provided action.
*
* @param input ReportInput
* @param action Specifies whether to create or replace the report. Can be "create" or "replace".
* @return Report
* @throws BadRequestException
* @throws ContentException
* @throws BadStateException
*/
fun upsertReport(input: ReportInput, action: String): Report? {
logger.info("ReportId, id = ${input.id}, action = $action")

return try {
performUpsert(input, action)
} catch (e: Exception) {
logger.error("Error upserting report: ${e.message}", e)
null
}
}

private fun performUpsert(input: ReportInput, action: String): Report? {
manu-govind marked this conversation as resolved.
Show resolved Hide resolved
// Validate action parameter
if (action !in listOf("create", "replace")) {
throw BadRequestException("Invalid action specified: $action. Must be 'create' or 'replace'.")
}

val report = mapInputToReport(input)
manu-govind marked this conversation as resolved.
Show resolved Hide resolved

when (action) {
"create" -> {
if (!report.id.isNullOrBlank()) {
throw BadRequestException("ID should not be provided for create action.")
}

// Generate a new ID if not provided
report.id = generateNewId()
val options = CosmosItemRequestOptions()
val createResponse: CosmosItemResponse<Report>? = reportsContainer?.createItem(report, PartitionKey(report.uploadId!!), options)
return createResponse?.item
}
"replace" -> {
if (report.id.isNullOrBlank()) {
throw BadRequestException("ID must be provided for replace action.")
}

// Attempt to read the existing item
val readResponse: CosmosItemResponse<Report>? = reportsContainer?.readItem(report.id!!, PartitionKey(report.uploadId!!), Report::class.java)

if (readResponse != null) {
// Replace the existing item
val options = CosmosItemRequestOptions()
val replaceResponse: CosmosItemResponse<Report>? = reportsContainer?.replaceItem(report, report.id!!, PartitionKey(report.uploadId!!), options)
return replaceResponse?.item
} else {
throw BadRequestException("Report with ID ${report.id} not found for replacement.")
}
}
else -> {
throw BadRequestException("Unexpected action: $action")
}
}
}

private fun mapInputToReport(input: ReportInput): Report {
manu-govind marked this conversation as resolved.
Show resolved Hide resolved
return Report(
id = input.id,
uploadId = input.uploadId,
reportId = input.reportId,
dataStreamId = input.dataStreamId,
dataStreamRoute = input.dataStreamRoute,
dexIngestDateTime = input.dexIngestDateTime,
messageMetadata = input.messageMetadata?.let { mapInputToMessageMetadata(it) },
stageInfo = input.stageInfo?.let { mapInputToStageInfo(it) },
tags = input.tags?.associate { it.key to it.value },
data = input.data?.associate { it.key to it.value },
contentType = input.contentType,
jurisdiction = input.jurisdiction,
senderId = input.senderId,
dataProducerId = input.dataProducerId,
content = input.content?.associate { it.key to it.value },
timestamp = input.timestamp
)
}

private fun mapInputToMessageMetadata(input: MessageMetadataInput): MessageMetadata {
return MessageMetadata(
messageUUID = input.messageUUID,
messageHash = input.messageHash,
aggregation = input.aggregation,
messageIndex = input.messageIndex
)
}

private fun mapInputToStageInfo(input: StageInfoInput): StageInfo {
return StageInfo(
service = input.service,
action = input.action,
version = input.version,
status = input.status,
issues = input.issues?.map { mapInputToIssue(it) },
startProcessingTime = input.startProcessingTime,
endProcessingTime = input.endProcessingTime
)
}

private fun mapInputToIssue(input: IssueInput): Issue {
return Issue(
level = input.code?.let { Level.valueOf(it) },
manu-govind marked this conversation as resolved.
Show resolved Hide resolved
message = input.description
)
}

private fun generateNewId(): String {
// Implement your logic to generate a new unique ID
return java.util.UUID.randomUUID().toString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gov.cdc.ocio.processingstatusapi.mutations

import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.server.operations.Mutation
import gov.cdc.ocio.processingstatusapi.models.reports.ReportInput

@GraphQLDescription("A Mutation Service to either create a new report or replace an existing report")
class ReportMutationService() : Mutation {

@GraphQLDescription("Create upload")
@Suppress("unused")
fun upsertReport(
@GraphQLDescription(
"*Report Input* to be created or updated:\n"
)
input: ReportInput,
@GraphQLDescription(
"*Action*: Can be one of the following values\n"
+ "`create`: Create new report\n"
+ "`replace`: Replace existing report\n"
)
action: String,
) = ReportMutation().upsertReport(input, action)


}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.expediagroup.graphql.server.ktor.*
import gov.cdc.ocio.processingstatusapi.dataloaders.ReportDataLoader
import gov.cdc.ocio.processingstatusapi.dataloaders.ReportDeadLetterDataLoader
import gov.cdc.ocio.processingstatusapi.mutations.NotificationsMutationService
import gov.cdc.ocio.processingstatusapi.mutations.ReportMutation
import gov.cdc.ocio.processingstatusapi.mutations.ReportMutationService
import gov.cdc.ocio.processingstatusapi.queries.*
import io.ktor.http.*
import io.ktor.serialization.jackson.*
Expand All @@ -21,6 +23,7 @@ import io.ktor.server.websocket.*
import mu.KotlinLogging
import java.time.Duration
import java.util.*
import org.koin.ktor.ext.inject


/**
Expand Down Expand Up @@ -80,6 +83,9 @@ fun Application.graphQLModule() {
// install(CORS) {
// anyHost()
// }

// val reportMutation by inject<ReportMutation>() // Inject ReportMutation from Koin

install(GraphQL) {
schema {
packages = listOf("gov.cdc.ocio.processingstatusapi")
Expand All @@ -92,7 +98,8 @@ fun Application.graphQLModule() {

)
mutations= listOf(
NotificationsMutationService()
NotificationsMutationService(),
ReportMutationService()
)
// subscriptions = listOf(
// ErrorSubscriptionService()
Expand Down
Loading
Loading