-
Notifications
You must be signed in to change notification settings - Fork 1
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
Improvements #1
base: master
Are you sure you want to change the base?
Improvements #1
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,4 @@ project/plugins/project/ | |
.history | ||
.cache | ||
.lib/ | ||
.idea |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version=0.13.13 | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,8 +37,8 @@ | |
copy them directly into your environment, or perhaps include them in your | ||
favored resource bundler. | ||
--> | ||
<link rel="stylesheet" href="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.css" /> | ||
<script src="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.min.js"></script> | ||
<link rel="stylesheet" href="//cdn.jsdelivr.net/graphiql/0.9.3/graphiql.css" /> | ||
<script src="//cdn.jsdelivr.net/graphiql/0.9.3/graphiql.min.js"></script> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the rest of the HTML need to be updated as well or just the JS/CSS? As a side note, I'll probably end up embedding these files in the repo instead of relying on a CDN in case I lose Internet connection during the live demo 😅. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, the rest should work just fine with the new version of GraphiQL There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sweet, done in 762db15. Thanks! |
||
|
||
</head> | ||
<body> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,38 @@ | ||
import spray.json._ | ||
import DefaultJsonProtocol._ | ||
import sangria.execution.deferred.HasId | ||
import sangria.schema._ | ||
import sangria.macros.derive._ | ||
|
||
case class Category( | ||
id: Int, | ||
name: String | ||
) { | ||
def styles(allStyles: List[Style]): List[Style] = | ||
allStyles.filter(_.category_id == id) | ||
name: String) | ||
|
||
object Category { | ||
implicit val categoryHasId = HasId[Category, Int](_.id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
implicit val categoryFormat = jsonFormat2(Category.apply) | ||
|
||
implicit val graphqlType: ObjectType[Repository, Category] = deriveObjectType( | ||
ObjectTypeDescription("A category"), | ||
AddFields(Field("styles", ListType(Style.graphqlType), | ||
resolve = c => c.ctx.styleFetcher.deferRelSeq(c.ctx.styleByCategory, c.value.id)))) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was trying to keep my models GraphQL-free, separating layers sort of, so that case classes contain the business logic and the schema file focuses on the GraphQL integration. Was that a stupid idea? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The domain models in this case still completely unaware of JSON serialization or GraphQL. I guess it's just very convenient way to pack all type-class instances for particular case class in its companion object. They are indeed co-located in a single file, but from application's perspective, they are sill loosely coupled. |
||
|
||
case class Style( | ||
id: Int, | ||
category_id: Int, | ||
name: String | ||
) { | ||
def beers(allBeers: List[Beer]): List[Beer] = | ||
allBeers.filter(_.style_id == id) | ||
name: String) | ||
|
||
object Style { | ||
implicit val styleHasId = HasId[Style, Int](_.id) | ||
implicit val styleFormat = jsonFormat3(Style.apply) | ||
|
||
def category(allCategories: List[Category]): Category = | ||
allCategories.find(_.id == category_id).get | ||
implicit val graphqlType: ObjectType[Repository, Style] = deriveObjectType( | ||
ObjectTypeDescription("A style"), | ||
ReplaceField("category_id", Field("category", Category.graphqlType, | ||
resolve = c => c.ctx.categoryFetcher.defer(c.value.category_id))), | ||
AddFields(Field("beers", ListType(Beer.graphqlType), | ||
resolve = c => c.ctx.beerFetcher.deferRelSeq(c.ctx.beerByStyle, c.value.id)))) | ||
} | ||
|
||
case class Brewery( | ||
|
@@ -28,10 +45,16 @@ case class Brewery( | |
country: String, | ||
phone: String, | ||
website: String, | ||
description: String | ||
) { | ||
def beers(allBeers: List[Beer]): List[Beer] = | ||
allBeers.filter(_.brewery_id == id) | ||
description: String) | ||
|
||
object Brewery { | ||
implicit val breweryHasId = HasId[Brewery, Int](_.id) | ||
implicit val breweryFormat = jsonFormat10(Brewery.apply) | ||
|
||
implicit val graphqlType: ObjectType[Repository, Brewery] = deriveObjectType( | ||
ObjectTypeDescription("A brewery"), | ||
AddFields(Field("beers", ListType(Beer.graphqlType), | ||
resolve = c => c.ctx.beerFetcher.deferRelSeq(c.ctx.beerByBrewery, c.value.id)))) | ||
} | ||
|
||
case class Beer( | ||
|
@@ -41,10 +64,16 @@ case class Beer( | |
name: String, | ||
abv: Double, | ||
description: String | ||
) { | ||
def brewery(allBreweries: List[Brewery]): Brewery = | ||
allBreweries.find(_.id == brewery_id).get | ||
) | ||
|
||
object Beer { | ||
implicit val beerHasId = HasId[Beer, Int](_.id) | ||
implicit val beerFormat = jsonFormat6(Beer.apply) | ||
|
||
def style(allStyles: List[Style]): Style = | ||
allStyles.find(_.id == style_id).get | ||
implicit val graphqlType: ObjectType[Repository, Beer] = deriveObjectType( | ||
ObjectTypeDescription("A beer"), | ||
ReplaceField("brewery_id", Field("brewery", Brewery.graphqlType, | ||
resolve = c => c.ctx.breweryFetcher.defer(c.value.brewery_id))), | ||
ReplaceField("style_id", Field("style", Style.graphqlType, | ||
resolve = c => c.ctx.styleFetcher.defer(c.value.style_id)))) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,92 +1,38 @@ | ||
import sangria.macros.derive._ | ||
import sangria.schema._ | ||
|
||
object ProjectSchema { | ||
val Id = Argument("id", IntType) | ||
|
||
val CategoryType: ObjectType[Repository, Category] = deriveObjectType( | ||
ObjectTypeDescription("A category"), | ||
AddFields( | ||
Field("styles", ListType(StyleType), | ||
resolve = c => c.value.styles(c.ctx.styles) | ||
) | ||
) | ||
) | ||
|
||
val StyleType: ObjectType[Repository, Style] = deriveObjectType( | ||
ObjectTypeDescription("A style"), | ||
ExcludeFields("category_id"), | ||
AddFields( | ||
Field("beers", ListType(BeerType), | ||
resolve = c => c.value.beers(c.ctx.beers) | ||
), | ||
Field("category", CategoryType, | ||
resolve = c => c.value.category(c.ctx.categories) | ||
) | ||
) | ||
) | ||
|
||
val BreweryType: ObjectType[Repository, Brewery] = deriveObjectType( | ||
ObjectTypeDescription("A brewery"), | ||
AddFields( | ||
Field("beers", ListType(BeerType), | ||
resolve = c => c.value.beers(c.ctx.beers) | ||
) | ||
) | ||
) | ||
|
||
// Type must be specified because of recursion: beer > brewery > beers > ... | ||
val BeerType: ObjectType[Repository, Beer] = deriveObjectType( | ||
ObjectTypeDescription("A beer"), | ||
ExcludeFields("brewery_id", "style_id"), | ||
AddFields( | ||
Field("style", StyleType, | ||
resolve = c => c.value.style(c.ctx.styles) | ||
), | ||
Field("brewery", BreweryType, | ||
resolve = c => c.value.brewery(c.ctx.breweries) | ||
) | ||
) | ||
) | ||
|
||
|
||
val QueryType = ObjectType("Query", fields[Repository, Unit]( | ||
Field("categories", ListType(CategoryType), | ||
Field("categories", ListType(Category.graphqlType), | ||
description = Some("Returns a list of all categories"), | ||
resolve = _.ctx.categories | ||
), | ||
Field("styles", ListType(StyleType), | ||
resolve = _.ctx.categories), | ||
Field("styles", ListType(Style.graphqlType), | ||
description = Some("Returns a list of all styles"), | ||
resolve = _.ctx.styles | ||
), | ||
Field("beers", ListType(BeerType), | ||
resolve = _.ctx.styles), | ||
Field("beers", ListType(Beer.graphqlType), | ||
description = Some("Returns a list of all beers"), | ||
resolve = _.ctx.beers | ||
), | ||
Field("breweries", ListType(BreweryType), | ||
resolve = _.ctx.beers), | ||
Field("breweries", ListType(Brewery.graphqlType), | ||
description = Some("Returns a list of all breweries"), | ||
resolve = _.ctx.breweries | ||
), | ||
Field("category", OptionType(CategoryType), | ||
resolve = _.ctx.breweries), | ||
|
||
Field("category", OptionType(Category.graphqlType), | ||
description = Some("Returns a category"), | ||
arguments = Id :: Nil, | ||
resolve = c => c.ctx.category(c.arg(Id)) | ||
), | ||
Field("style", OptionType(StyleType), | ||
resolve = c => c.ctx.categoryFetcher.deferOpt(c.arg(Id))), | ||
Field("style", OptionType(Style.graphqlType), | ||
description = Some("Returns a style"), | ||
arguments = Id :: Nil, | ||
resolve = c => c.ctx.style(c.arg(Id)) | ||
), | ||
Field("brewery", OptionType(BreweryType), | ||
resolve = c => c.ctx.styleFetcher.deferOpt(c.arg(Id))), | ||
Field("brewery", OptionType(Brewery.graphqlType), | ||
description = Some("Returns a brewery"), | ||
arguments = Id :: Nil, | ||
resolve = c => c.ctx.brewery(c.arg(Id)) | ||
), | ||
Field("beer", OptionType(BeerType), | ||
resolve = c => c.ctx.breweryFetcher.deferOpt(c.arg(Id))), | ||
Field("beer", OptionType(Beer.graphqlType), | ||
description = Some("Returns a beer"), | ||
arguments = Id :: Nil, | ||
resolve = c => c.ctx.beer(c.arg(Id)) | ||
) | ||
)) | ||
resolve = c => c.ctx.beerFetcher.deferOpt(c.arg(Id))))) | ||
|
||
val schema = Schema(QueryType) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,49 @@ | ||
import spray.json._ | ||
import DefaultJsonProtocol._ | ||
import sangria.execution.deferred.{DeferredResolver, Fetcher, Relation, RelationIds} | ||
|
||
object Repository { | ||
println("Data loaded:") | ||
import scala.concurrent.Future.successful | ||
|
||
val categorySource = io.Source.fromFile("src/main/resources/categories.json")("UTF-8").mkString.parseJson | ||
implicit val categoryFormat = jsonFormat2(Category.apply) | ||
val categories = categorySource.convertTo[List[Category]] | ||
class Repository extends Fetchers { | ||
val categories = loadFile[Category]("categories.json") | ||
val styles = loadFile[Style]("styles.json") | ||
val breweries = loadFile[Brewery]("breweries.json") | ||
val beers = loadFile[Beer]("beers.json") | ||
|
||
println(s" - ${categories.length} categories") | ||
|
||
val styleSource = io.Source.fromFile("src/main/resources/styles.json")("UTF-8").mkString.parseJson | ||
implicit val styleFormat = jsonFormat3(Style.apply) | ||
val styles = styleSource.convertTo[List[Style]] | ||
private def loadFile[T : JsonFormat](fileName: String): List[T] = | ||
io.Source.fromResource(fileName)("UTF-8").mkString.parseJson.convertTo[List[T]] | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah! I did try exactly that but I got confused with the required |
||
|
||
println(s" - ${styles.length} styles") | ||
trait Fetchers { | ||
val categoryFetcher = Fetcher.caching( | ||
(repo: Repository, ids: Seq[Int]) => | ||
successful(repo.categories.filter(c => ids contains c.id))) | ||
|
||
val brewerySource = io.Source.fromFile("src/main/resources/breweries.json")("UTF-8").mkString.parseJson | ||
implicit val breweryFormat = jsonFormat10(Brewery.apply) | ||
val breweries = brewerySource.convertTo[List[Brewery]] | ||
val styleByCategory = Relation("byCategory", (s: Style) => Seq(s.category_id)) | ||
|
||
println(s" - ${breweries.length} breweries") | ||
val styleFetcher = Fetcher.relCaching( | ||
(repo: Repository, ids: Seq[Int]) => | ||
successful(repo.styles.filter(s => ids contains s.id)), | ||
(repo: Repository, ids: RelationIds[Style]) => | ||
successful(repo.styles.filter(s => ids(styleByCategory) contains s.category_id))) | ||
|
||
val beerSource = io.Source.fromFile("src/main/resources/beers.json")("UTF-8").mkString.parseJson | ||
implicit val beerFormat = jsonFormat6(Beer.apply) | ||
val beers = beerSource.convertTo[List[Beer]] | ||
val breweryFetcher = Fetcher.caching( | ||
(repo: Repository, ids: Seq[Int]) => | ||
successful(repo.breweries.filter(b => ids contains b.id))) | ||
|
||
println(s" - ${beers.length} beers") | ||
} | ||
val beerByBrewery = Relation("byBrewery", (b: Beer) => Seq(b.brewery_id)) | ||
val beerByStyle = Relation("byStyle", (b: Beer) => Seq(b.style_id)) | ||
|
||
class Repository { | ||
import Repository._ | ||
val beerFetcher = Fetcher.relCaching( | ||
(repo: Repository, ids: Seq[Int]) => | ||
successful(repo.beers.filter(b => ids contains b.id)), | ||
(repo: Repository, ids: RelationIds[Beer]) => { | ||
val breweryIds = ids(beerByBrewery) | ||
val styleIds = ids(beerByStyle) | ||
|
||
val categories = Repository.categories | ||
val styles = Repository.styles | ||
val beers = Repository.beers | ||
val breweries = Repository.breweries | ||
successful(repo.beers.filter(b => breweryIds.contains(b.brewery_id) || styleIds.contains(b.style_id))) | ||
}) | ||
|
||
def style(id: Int): Option[Style] = Repository.styles.find(_.id == id) | ||
def category(id: Int): Option[Category] = Repository.categories.find(_.id == id) | ||
def beer(id: Int): Option[Beer] = Repository.beers.find(_.id == id) | ||
def brewery(id: Int): Option[Brewery] = Repository.breweries.find(_.id == id) | ||
val deferredResolver = DeferredResolver.fetchers( | ||
categoryFetcher, styleFetcher, breweryFetcher, beerFetcher) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm very much unaware of Scala tooling as I'm a complete beginner. Any specific reason why you added this + the
.idea
in the.gitignore
file?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sbt.version
locks down the SBT version. If you don't have this defines, SBT launcher will take whatever is installed in your system and it is quite unreliable. I, for instance, haven't updated SBT setup for a long time and project failed to start because of this.build.properties
fixed it..idea
folder is just an intellij idea project files. You don't really want them in a git repo, so i ignored them :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re:
sbt.version
, gotcha, added in 4bfd1d2.Re:
.idea
, I prefer to keep OS-specific and IDE-specific files out of my projects and use a global ignore list instead. For example, see this and that. That's a personal preference but I prefer that over having to duplicate/maintain these in all project gitignore files :)