Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tasks.register("run", JavaExec::class.java) {
classpath(configurations.getByName("runtimeClasspath"))
classpath(kotlin.target.compilations.getByName("main").output.classesDirs)
mainClass.set("androidmakers.service.MainKt")
debug = true
}

configureDeploy("service", "androidmakers.service.MainKt")
18 changes: 13 additions & 5 deletions service/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ schema {
mutation: RootMutation
}

"""
A type representing a formatted kotlinx.datetime.Instant
"""
scalar GraphQLInstant

scalar GraphQLLocalDate

scalar GraphQLLocalDateTime
Expand Down Expand Up @@ -57,12 +62,17 @@ type Conference {
type FeedItem {
id: ID!

createdAt: GraphQLInstant!

title: String!

markdown: Markdown!
}

type FeedItemFailure
type FeedItemFailure {
message: String!
}

type FeedItemSuccess {
feedItem: FeedItem!
}
Expand Down Expand Up @@ -260,8 +270,6 @@ type SpeakerConnection {
type User {
id: String!

email: String!

isAdmin: Boolean!
}

Expand Down Expand Up @@ -308,9 +316,9 @@ input ConferenceOrderBy {
}

input FeedItemInput {
title: String!
title: String

markdown: Markdown!
markdown: Markdown
}

input SessionOrderBy {
Expand Down
7 changes: 7 additions & 0 deletions service/index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
indexes:

- kind: FeedItems
ancestor: yes
properties:
- name: createdAt
direction: desc
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
package androidmakers.service.context

import androidmakers.service.firebaseEmail
import com.apollographql.apollo.api.ExecutionContext

internal class AuthenticationContext(val uid: String?) : ExecutionContext.Element {
override val key: ExecutionContext.Key<*>
get() = Key

companion object Key : ExecutionContext.Key<AuthenticationContext>

val email: String? by lazy {
uid?.let {
firebaseEmail(it)
}
}
}

internal fun ExecutionContext.uid() = this.get(AuthenticationContext)?.uid
Expand Down
4 changes: 0 additions & 4 deletions service/src/main/kotlin/androidmakers/service/firebase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ private fun ensureInitialized() {
}
}
}
fun firebaseEmail(uid: String): String {
ensureInitialized()
return FirebaseAuth.getInstance().getUser(uid).email
}

fun String.firebaseUid(): FirebaseUidResult {
if (this == "testToken") {
Expand Down
91 changes: 65 additions & 26 deletions service/src/main/kotlin/androidmakers/service/graphql/model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
package androidmakers.service.graphql

import androidmakers.service.Sessionize
import androidmakers.service.context.AuthenticationContext
import androidmakers.service.context.bookmarksKeyFactory
import androidmakers.service.context.datastore
import androidmakers.service.context.uid
import androidmakers.service.context.updateMaxAge
import com.apollographql.apollo.annotations.*
import androidmakers.service.context.*
import com.apollographql.apollo.annotations.ApolloExperimental
import com.apollographql.apollo.api.ExecutionContext
import com.apollographql.apollo.api.Optional
import com.apollographql.apollo.execution.StringCoercing
import com.apollographql.execution.annotation.GraphQLDefault
import com.apollographql.execution.annotation.GraphQLMutation
Expand All @@ -19,8 +16,11 @@ import com.google.cloud.datastore.BooleanValue
import com.google.cloud.datastore.Cursor
import com.google.cloud.datastore.Entity
import com.google.cloud.datastore.Query
import com.google.rpc.context.AttributeContext
import com.google.cloud.datastore.StructuredQuery
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toInstant
import kotlin.time.Clock

const val KIND_BOOKMARKS = "Bookmarks"
const val KIND_FEED_ITEMS = "FeedItems"
Expand All @@ -35,11 +35,11 @@ typealias Markdown = String
typealias ID = String

class FeedItemInput(
val title: String,
val markdown: Markdown
val title: Optional<String?>,
val markdown: Optional<Markdown?>,
)

internal val adminEmails = setOf("martinbonninandroid@gmail.com", "reno.mathieu@gmail.com")
internal val adminUids = setOf("AY7jNYS4EpNhRHBxW89LjomGCtl1")

sealed interface FeedItemResult

Expand All @@ -50,32 +50,69 @@ data class FeedItemSuccess(

class FeedItem(
val id: ID,
val createdAt: GraphQLInstant,
val title: String,
val markdown: Markdown
)

@GraphQLMutation
class RootMutation {
fun updateFeedItem(executionContext: ExecutionContext, id: ID, feedItem: FeedItemInput): FeedItemResult {
TODO()
}
fun addFeedItem(executionContext: ExecutionContext, feedItem: FeedItemInput): FeedItemResult {
val authenticationContext = executionContext.get(AuthenticationContext)!!
val email = authenticationContext.email
check(email != null) {
"Adding to the feed requires authentication"
if (authenticationContext.uid !in adminUids) {
throw Error("Only admins can add feed items")
}

if (email !in adminEmails) {
val datastore = executionContext.datastore()
val key = datastore.newKeyFactory().setKind(KIND_FEED_ITEMS).newKey(id.toLong())
val entity = datastore.get(key)
if (entity == null) {
return FeedItemFailure("No item found for id $id")
}
val updatedEntity = Entity.newBuilder(entity)
.apply {
if (feedItem.title.getOrNull() != null) {
set("title", feedItem.title.getOrThrow())
}
if (feedItem.markdown.getOrNull() != null) {
set("markdown", feedItem.markdown.getOrThrow())
}
}
.build()

val newEntity = datastore.put(updatedEntity)

return FeedItemSuccess(
FeedItem(
id = id,
title = newEntity.getString("title"),
markdown = newEntity.getString("markdown"),
createdAt = Instant.parse(newEntity.getString("createdAt"))
)
)
}

fun addFeedItem(executionContext: ExecutionContext, feedItem: FeedItemInput): FeedItemResult {
val authenticationContext = executionContext.get(AuthenticationContext)!!
if (authenticationContext.uid !in adminUids) {
throw Error("Only admins can add feed items")
}

val datastore = executionContext.datastore()

check(feedItem.title.getOrNull() != null) {
"title is required"
}
check(feedItem.markdown.getOrNull() != null) {
"markdown is required"
}
val key = datastore.newKeyFactory().setKind(KIND_FEED_ITEMS).newKey()
val now = Clock.System.now()

val entity = Entity.newBuilder(key)!!
.set("title", feedItem.title)
.set("markdown", feedItem.markdown)
.set("title", feedItem.title.getOrThrow())
.set("markdown", feedItem.markdown.getOrThrow())
.set("createdAt", now.toString())
.build()

val result = datastore.runInTransaction {
Expand All @@ -84,9 +121,10 @@ class RootMutation {

return FeedItemSuccess(
FeedItem(
id = result.key.toString(),
title = feedItem.title,
markdown = feedItem.markdown
id = result.key.id.toString(),
title = result.getString("title"),
markdown = result.getString("markdown"),
createdAt = result.getString("createdAt").toInstant()
)
)
}
Expand Down Expand Up @@ -298,6 +336,7 @@ class RootQuery {

val query = Query.newEntityQueryBuilder()
.setKind(KIND_FEED_ITEMS)
.addOrderBy(StructuredQuery.OrderBy.desc("createdAt"))
.setLimit(first)
.apply {
if (after != null) {
Expand All @@ -312,9 +351,10 @@ class RootQuery {
results.forEach { entity ->
allItems.add(
FeedItem(
id = entity.key.toString(),
id = entity.key.id.toString(),
title = entity.getString("title"),
markdown = entity.getString("markdown")
markdown = entity.getString("markdown"),
createdAt = entity.getString("createdAt").toInstant(),
)
)
}
Expand All @@ -333,13 +373,12 @@ class RootQuery {
if (authenticationContext.uid == null) {
return null
}
return User(id = authenticationContext.uid, email = authenticationContext.email!!, isAdmin = authenticationContext.email in adminEmails)
return User(id = authenticationContext.uid, isAdmin = authenticationContext.uid in adminUids)
}
}

class User(
val id: String,
val email: String,
val isAdmin: Boolean
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ object GraphQLLocalDateTimeCoercing : Coercing<LocalDateTime> {
return internalValue.toString()
}
}