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
55 changes: 55 additions & 0 deletions service/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ scalar GraphQLLocalDate

scalar GraphQLLocalDateTime

scalar ID

"""
A Markdown string as described by https://spec.commonmark.org
"""
scalar Markdown

enum ConferenceField {
DAYS
}
Expand Down Expand Up @@ -47,6 +54,25 @@ type Conference {
themeColor: String
}

type FeedItem {
id: ID!

title: String!

markdown: Markdown!
}

type FeedItemFailure
type FeedItemSuccess {
feedItem: FeedItem!
}

type FeedItemsConnection {
nodes: [FeedItem!]!

pageInfo: PageInfo!
}

type Link {
type: LinkType!

Expand All @@ -55,6 +81,8 @@ type Link {

type PageInfo {
endCursor: String

hasNextPage: Boolean!
}

type Partner {
Expand Down Expand Up @@ -83,6 +111,10 @@ type Room {
}

type RootMutation {
updateFeedItem(id: ID!, feedItem: FeedItemInput!): FeedItemResult!

addFeedItem(feedItem: FeedItemInput!): FeedItemResult!

addBookmark(sessionId: String!): BookmarkConnection!

removeBookmark(sessionId: String!): BookmarkConnection!
Expand Down Expand Up @@ -121,6 +153,13 @@ type RootQuery {
bookmarkConnection: BookmarkConnection!

conferences(orderBy: ConferenceOrderBy = null): [Conference!]!

feedItemsConnection(first: Int! = 10, after: String = null): FeedItemsConnection!

"""
The current logged in user or null if the user is not logged in
"""
user: User
}

"""
Expand Down Expand Up @@ -218,6 +257,14 @@ type SpeakerConnection {
pageInfo: PageInfo!
}

type User {
id: String!

email: String!

isAdmin: Boolean!
}

"""
@property floorPlanUrl the url to an image containing the floor plan
"""
Expand Down Expand Up @@ -252,12 +299,20 @@ interface Node {
id: String!
}

union FeedItemResult = FeedItemFailure|FeedItemSuccess

input ConferenceOrderBy {
field: ConferenceField!

direction: OrderByDirection!
}

input FeedItemInput {
title: String!

markdown: Markdown!
}

input SessionOrderBy {
field: SessionField!

Expand Down
2 changes: 1 addition & 1 deletion service/src/main/kotlin/androidmakers/service/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private fun Any?.toJsonElement(): JsonElement {

private suspend fun RoutingCall.respondGraphQL2(executableSchema: ExecutableSchema) {
val authResult = firebaseUid()
var uid: String? =null
var uid: String? = null
when (authResult) {
is FirebaseUidResult.Error -> {
respondText(ContentType.parse("application/json"), HttpStatusCode.Unauthorized) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package androidmakers.service.context

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

class AuthenticationContext(val uid: String?) : ExecutionContext.Element {
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
internal fun ExecutionContext.uid() = this.get(AuthenticationContext)?.uid

18 changes: 13 additions & 5 deletions service/src/main/kotlin/androidmakers/service/firebase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,26 @@ sealed interface FirebaseUidResult {
object SignedOut: FirebaseUidResult
}

fun String.firebaseUid(): FirebaseUidResult {
if (this == "testToken") {
return FirebaseUidResult.SignedIn("testUser")
}

private fun ensureInitialized() {
synchronized(lock) {
if (!_isInitialized) {
val options = FirebaseOptions.builder().setCredentials(GoogleCredentials.getApplicationDefault()).build()
FirebaseApp.initializeApp(options)
_isInitialized = true
}
}
}
fun firebaseEmail(uid: String): String {
ensureInitialized()
return FirebaseAuth.getInstance().getUser(uid).email
}

fun String.firebaseUid(): FirebaseUidResult {
if (this == "testToken") {
return FirebaseUidResult.SignedIn("testUser")
}

ensureInitialized()

return try {
FirebaseUidResult.SignedIn(FirebaseAuth.getInstance().verifyIdToken(this).uid)
Expand Down
134 changes: 132 additions & 2 deletions service/src/main/kotlin/androidmakers/service/graphql/model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,94 @@
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 com.apollographql.apollo.api.ExecutionContext
import com.apollographql.apollo.execution.StringCoercing
import com.apollographql.execution.annotation.GraphQLDefault
import com.apollographql.execution.annotation.GraphQLMutation
import com.apollographql.execution.annotation.GraphQLQuery
import com.apollographql.execution.annotation.GraphQLScalar
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 kotlinx.datetime.LocalDateTime

const val KIND_BOOKMARKS = "Bookmarks"
const val KIND_FEED_ITEMS = "FeedItems"

/**
* A Markdown string as described by https://spec.commonmark.org
*/
@GraphQLScalar(StringCoercing::class)
typealias Markdown = String

@GraphQLScalar(StringCoercing::class)
typealias ID = String

class FeedItemInput(
val title: String,
val markdown: Markdown
)

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

sealed interface FeedItemResult

data object FeedItemFailure: FeedItemResult
data class FeedItemSuccess(
val feedItem: FeedItem
): FeedItemResult

class FeedItem(
val id: ID,
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 (email !in adminEmails) {
throw Error("Only admins can add feed items")
}

val datastore = executionContext.datastore()

val key = datastore.newKeyFactory().setKind(KIND_FEED_ITEMS).newKey()
val entity = Entity.newBuilder(key)!!
.set("title", feedItem.title)
.set("markdown", feedItem.markdown)
.build()

val result = datastore.runInTransaction {
it.put(entity)
}

return FeedItemSuccess(
FeedItem(
id = result.key.toString(),
title = feedItem.title,
markdown = feedItem.markdown
)
)
}

fun addBookmark(executionContext: ExecutionContext, sessionId: String): BookmarkConnection {
val uid = executionContext.uid()
check(uid != null) {
Expand Down Expand Up @@ -150,10 +221,10 @@ class RootQuery {
i += 1
}
}
var count = minOf(size - i, first)
val count = minOf(size - i, first)
return block(
this.subList(i, i + count),
PageInfo(get(i + count - 1).id)
PageInfo(get(i + count - 1).id, i + count < size)
)
}

Expand Down Expand Up @@ -217,8 +288,66 @@ class RootQuery {
fun conferences(@GraphQLDefault("null") orderBy: ConferenceOrderBy?): List<Conference> {
return listOf(Sessionize.data().conference)
}

fun feedItemsConnection(
executionContext: ExecutionContext,
@GraphQLDefault("10") first: Int,
@GraphQLDefault("null") after: String?
): FeedItemsConnection {
val datastore = executionContext.datastore()

val query = Query.newEntityQueryBuilder()
.setKind(KIND_FEED_ITEMS)
.setLimit(first)
.apply {
if (after != null) {
setStartCursor(Cursor.fromUrlSafe(after))
}
}
.build()

val results = datastore.run(query)

val allItems = mutableListOf<FeedItem>()
results.forEach { entity ->
allItems.add(
FeedItem(
id = entity.key.toString(),
title = entity.getString("title"),
markdown = entity.getString("markdown")
)
)
}

return FeedItemsConnection(
nodes = allItems,
pageInfo = PageInfo(allItems.lastOrNull()?.id, results.hasNext())
)
}

/**
* The current logged in user or null if the user is not logged in
*/
fun user(executionContext: ExecutionContext): User? {
val authenticationContext = executionContext[AuthenticationContext]!!
if (authenticationContext.uid == null) {
return null
}
return User(id = authenticationContext.uid, email = authenticationContext.email!!, isAdmin = authenticationContext.email in adminEmails)
}
}

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

class FeedItemsConnection(
val nodes: List<FeedItem>,
val pageInfo: PageInfo
)

class BookmarkConnection(
val nodes: List<Session>
)
Expand Down Expand Up @@ -270,6 +399,7 @@ data class SessionConnection(

data class PageInfo(
val endCursor: String?,
val hasNextPage: Boolean = true
)

enum class LinkType {
Expand Down