diff --git a/changelog.md b/changelog.md index 565be87b1..2244aa326 100644 --- a/changelog.md +++ b/changelog.md @@ -37,6 +37,7 @@ Changelog
  • 1.21.4 - 06.02.2016
  • 1.21.5 - 11.02.2016
  • 1.22.0 - 14.02.2016
  • +
  • 1.22.1 - 24.02.2016
  • @@ -354,4 +355,11 @@ removed `session.newSimpleStatement`. - PHANTOM-194: Fixed serialization of ```Table.select.distinct``` queries. - Added support for `LocalDate` columns. -- Added `driver-extras` dependency from the Datastax set. \ No newline at end of file +- Added `driver-extras` dependency from the Datastax set. + +1.22.1 +================================ + +- Added a `DateTime` augmenter in the default package capable of producing `TimeUUID` values from a `DateTime`. +- Fixed serialization of `minTimeuuid` and `maxTimeuuid` clauses to use dates in ISO formats. +- Added tests to test `timeuuid` based date range selection with `minTimeuuid` and `maxTimeuuid`. \ No newline at end of file diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala index dad8cae24..5af2ebd7c 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala @@ -32,12 +32,11 @@ package com.websudos.phantom.builder.ops import java.util.Date import com.datastax.driver.core.Session -import com.datastax.driver.core.utils.UUIDs import com.websudos.phantom.builder.QueryBuilder import com.websudos.phantom.builder.clauses.OperatorClause.Condition import com.websudos.phantom.builder.clauses.{OperatorClause, TypedClause, WhereClause} import com.websudos.phantom.builder.primitives.{DefaultPrimitives, Primitive} -import com.websudos.phantom.builder.query.SessionAugmenter +import com.websudos.phantom.builder.query.{CQLQuery, SessionAugmenter} import com.websudos.phantom.builder.syntax.CQLSyntax import com.websudos.phantom.column.{AbstractColumn, TimeUUIDColumn} import org.joda.time.DateTime @@ -91,27 +90,29 @@ sealed class NowCqlFunction extends CqlFunction { sealed class MaxTimeUUID extends CqlFunction with DefaultPrimitives { - private[this] val datePrimitive = implicitly[Primitive[Date]] - private[this] val dateTimePrimitive = implicitly[Primitive[DateTime]] - def apply(date: Date): OperatorClause.Condition = { - new Condition(QueryBuilder.Select.maxTimeuuid(UUIDPrimitive.asCql(UUIDs.endOf(date.getTime)))) + new Condition( + QueryBuilder.Select.maxTimeuuid( + CQLQuery.escape(new DateTime(date).toString()) + ) + ) } def apply(date: DateTime): OperatorClause.Condition = { - new Condition(QueryBuilder.Select.maxTimeuuid(UUIDPrimitive.asCql(UUIDs.endOf(date.getMillis)))) + new Condition( + QueryBuilder.Select.maxTimeuuid( + CQLQuery.escape(new DateTime(date).toString()) + ) + ) } } sealed class MinTimeUUID extends CqlFunction with DefaultPrimitives { - private[this] val datePrimitive = implicitly[Primitive[Date]] - private[this] val dateTimePrimitive = implicitly[Primitive[DateTime]] - def apply(date: Date): OperatorClause.Condition = { new Condition( QueryBuilder.Select.minTimeuuid( - UUIDPrimitive.asCql(UUIDs.startOf(date.getTime)) + CQLQuery.escape(new DateTime(date).toString()) ) ) } @@ -119,7 +120,7 @@ sealed class MinTimeUUID extends CqlFunction with DefaultPrimitives { def apply(date: DateTime): OperatorClause.Condition = { new Condition( QueryBuilder.Select.minTimeuuid( - UUIDPrimitive.asCql(UUIDs.startOf(date.getMillis)) + CQLQuery.escape(date.toString()) ) ) } @@ -151,6 +152,7 @@ sealed class TokenConstructor[P <: HList, TP <: TokenTypes.Root](val mapper : Se /** * An equals comparison clause between token definitions. + * * @param tk The token constructor to compare against. * @tparam VL * @return diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala index a1e7681fa..100f4bc14 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala @@ -31,18 +31,19 @@ package com.websudos.phantom import java.net.InetAddress import java.nio.ByteBuffer -import java.util.Date +import java.util.{Date, Random} +import com.datastax.driver.core.utils.UUIDs import com.datastax.driver.core.{ConsistencyLevel => CLevel, VersionNumber} import com.websudos.phantom.batch.Batcher import com.websudos.phantom.builder.QueryBuilder -import com.websudos.phantom.builder.clauses.{WhereClause, UpdateClause} +import com.websudos.phantom.builder.clauses.{UpdateClause, WhereClause} import com.websudos.phantom.builder.ops._ import com.websudos.phantom.builder.primitives.{DefaultPrimitives, Primitive} -import com.websudos.phantom.builder.query.{DeleteImplicits, CQLQuery, CreateImplicits, SelectImplicits} +import com.websudos.phantom.builder.query.{CQLQuery, CreateImplicits, DeleteImplicits, SelectImplicits} import com.websudos.phantom.builder.syntax.CQLSyntax import com.websudos.phantom.util.ByteString -import shapeless.{HNil, ::} +import shapeless.{::, HNil} import scala.util.Try @@ -246,6 +247,7 @@ package object dsl extends ImplicitMechanism with CreateImplicits /** * Augments Cassandra VersionNumber descriptors to support simple comparison of versions. * This allows for operations that can differ based on the Cassandra version used by the session. + * * @param version The Cassandra version number. */ implicit class VersionAugmenter(val version: VersionNumber) extends AnyVal { @@ -253,4 +255,12 @@ package object dsl extends ImplicitMechanism with CreateImplicits def ===(other: VersionNumber): Boolean = version.compareTo(other) == 0 def > (other: VersionNumber): Boolean = version.compareTo(other) == 1 } + + implicit class DateTimeAugmenter(val date: DateTime) extends AnyVal { + def timeuuid(): UUID = { + val random = new Random() + new UUID(UUIDs.startOf(date.getMillis).getMostSignificantBits, random.nextLong()) + } + } + } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/ordering/TimeSeriesTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/ordering/TimeSeriesTest.scala index 784b3e76c..3faa865fd 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/ordering/TimeSeriesTest.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/ordering/TimeSeriesTest.scala @@ -48,17 +48,20 @@ class TimeSeriesTest extends PhantomSuite { TestDatabase.timeSeriesTable.insertSchema() } + protected[this] final val durationOffset = 1000 + it should "allow using naturally fetch the records in descending order for a descending clustering order" in { var i = 0 - val testUUID = gen[UUID] - val number = 5 val recordList = genList[TimeSeriesRecord](number).map( item => { i += 1 - item.copy(id = TestDatabase.timeSeriesTable.testUUID, timestamp = item.timestamp.withDurationAdded(1000, i)) + item.copy( + id = TestDatabase.timeSeriesTable.testUUID, + timestamp = item.timestamp.withDurationAdded(durationOffset, i) + ) }) val ts = recordList.map(_.timestamp.getSecondOfDay) @@ -88,14 +91,15 @@ class TimeSeriesTest extends PhantomSuite { it should "allow using naturally fetch the records in descending order for a descending clustering order with Twitter Futures" in { var i = 0 - val testUUID = gen[UUID] - val number = 5 val recordList = genList[TimeSeriesRecord](number).map( item => { i += 1 - item.copy(id = TestDatabase.timeSeriesTable.testUUID, timestamp = item.timestamp.withDurationAdded(1000, i)) + item.copy( + id = TestDatabase.timeSeriesTable.testUUID, + timestamp = item.timestamp.withDurationAdded(durationOffset, i) + ) }) val ts = recordList.map(_.timestamp.getSecondOfDay) @@ -124,13 +128,15 @@ class TimeSeriesTest extends PhantomSuite { it should "allow fetching the records in ascending order for a descending clustering order using order by clause" in { var i = 0 - val testUUID = gen[UUID] val number = 5 val recordList = genList[TimeSeriesRecord](number).map( item => { i += 1 - item.copy(id = TestDatabase.timeSeriesTable.testUUID, timestamp = item.timestamp.withDurationAdded(500, i)) + item.copy( + id = TestDatabase.timeSeriesTable.testUUID, + timestamp = item.timestamp.withDurationAdded(durationOffset / 2, i) + ) }) val batch = recordList.foldLeft(Batch.unlogged) { @@ -162,13 +168,15 @@ class TimeSeriesTest extends PhantomSuite { it should "allow fetching the records in ascending order for a descending clustering order using order by clause with Twitter Futures" in { var i = 0 - val testUUID = gen[UUID] val number = 5 val recordList = genList[TimeSeriesRecord](number).map( item => { i += 1 - item.copy(id = TestDatabase.timeSeriesTable.testUUID, timestamp = item.timestamp.withDurationAdded(500, i)) + item.copy( + id = TestDatabase.timeSeriesTable.testUUID, + timestamp = item.timestamp.withDurationAdded(durationOffset, i) + ) }) val batch = recordList.foldLeft(Batch.unlogged) { @@ -182,7 +190,11 @@ class TimeSeriesTest extends PhantomSuite { val chain = for { truncate <- TestDatabase.timeSeriesTable.truncate.execute() insert <- batch.execute() - chunks <- TestDatabase.timeSeriesTable.select.where(_.id eqs TestDatabase.timeSeriesTable.testUUID).orderBy(_.timestamp.asc).limit(number).collect() + chunks <- TestDatabase.timeSeriesTable + .select + .where(_.id eqs TestDatabase.timeSeriesTable.testUUID) + .orderBy(_.timestamp.asc).limit(number) + .collect() } yield chunks chain.successful { @@ -194,13 +206,15 @@ class TimeSeriesTest extends PhantomSuite { it should "allow fetching the records in descending order for a descending clustering order using order by clause" in { var i = 0 - val testUUID = gen[UUID] val number = 5 val recordList = genList[TimeSeriesRecord](number).map( item => { i += 1 - item.copy(id = TestDatabase.timeSeriesTable.testUUID, timestamp = item.timestamp.withDurationAdded(500, i)) + item.copy( + id = TestDatabase.timeSeriesTable.testUUID, + timestamp = item.timestamp.withDurationAdded(durationOffset / 2, i) + ) }) val batch = recordList.foldLeft(Batch.unlogged) { @@ -213,7 +227,12 @@ class TimeSeriesTest extends PhantomSuite { val chain = for { truncate <- TestDatabase.timeSeriesTable.truncate.future() insert <- batch.future() - chunks <- TestDatabase.timeSeriesTable.select.where(_.id eqs TestDatabase.timeSeriesTable.testUUID).orderBy(_.timestamp.descending).limit(number).fetch() + chunks <- TestDatabase.timeSeriesTable + .select + .where(_.id eqs TestDatabase.timeSeriesTable.testUUID) + .orderBy(_.timestamp.descending) + .limit(number) + .fetch() } yield chunks chain.successful { @@ -225,13 +244,15 @@ class TimeSeriesTest extends PhantomSuite { it should "allow fetching the records in descending order for a descending clustering order using order by clause with Twitter Futures" in { var i = 0 - val testUUID = gen[UUID] val number = 5 val recordList = genList[TimeSeriesRecord](number).map( item => { i += 1 - item.copy(id = TestDatabase.timeSeriesTable.testUUID, timestamp = item.timestamp.withDurationAdded(500, i)) + item.copy( + id = TestDatabase.timeSeriesTable.testUUID, + timestamp = item.timestamp.withDurationAdded(durationOffset, i) + ) }) val batch = recordList.foldLeft(Batch.unlogged) { @@ -244,7 +265,10 @@ class TimeSeriesTest extends PhantomSuite { val chain = for { truncate <- TestDatabase.timeSeriesTable.truncate.execute() insert <- batch.execute() - chunks <- TestDatabase.timeSeriesTable.select.where(_.id eqs TestDatabase.timeSeriesTable.testUUID).orderBy(_.timestamp.desc).limit(number).collect() + chunks <- TestDatabase.timeSeriesTable.select + .where(_.id eqs TestDatabase.timeSeriesTable.testUUID) + .orderBy(_.timestamp.desc).limit(number) + .collect() } yield chunks chain.successful { diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/SelectFunctionsTesting.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/SelectFunctionsTesting.scala index 4c91dc2fd..5af307b20 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/SelectFunctionsTesting.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/SelectFunctionsTesting.scala @@ -69,8 +69,8 @@ class SelectFunctionsTesting extends PhantomSuite { val chain = for { store <- database.timeuuidTable.store(record).future() - timestamp <- database.timeuuidTable.select.function(t => dateOf(t.id)) - .where(_.id eqs record.id).one() + timestamp <- database.timeuuidTable.select.function(t => dateOf(t.id)).where(_.user eqs record.user) + .and(_.id eqs record.id).one() } yield timestamp whenReady(chain) { @@ -88,8 +88,8 @@ class SelectFunctionsTesting extends PhantomSuite { val chain = for { store <- database.timeuuidTable.store(record).future() - timestamp <- database.timeuuidTable.select.function(t => unixTimestampOf(t.id)) - .where(_.id eqs record.id).one() + timestamp <- database.timeuuidTable.select.function(t => unixTimestampOf(t.id)).where(_.user eqs record.user) + .and(_.id eqs record.id).one() } yield timestamp whenReady(chain) { diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/TimeUuidTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/TimeUuidTest.scala new file mode 100644 index 000000000..f18aa91b7 --- /dev/null +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/TimeUuidTest.scala @@ -0,0 +1,164 @@ +/* + * Copyright 2013-2015 Websudos, Limited. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Explicit consent must be obtained from the copyright owner, Websudos Limited before any redistribution is made. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.websudos.phantom.builder.query.db.specialized + +import com.datastax.driver.core.utils.UUIDs +import com.websudos.phantom.{dsl, PhantomSuite} +import com.websudos.phantom.tables.{TimeUUIDRecord, TestDatabase} +import org.joda.time.DateTime +import com.websudos.util.testing._ +import com.websudos.phantom.dsl._ + +class TimeUuidTest extends PhantomSuite { + + override def beforeAll(): Unit = { + super.beforeAll() + TestDatabase.timeuuidTable.insertSchema() + } + + it should "be able to store and retrieve a time slice of records based on a combination of minTimeuuid and maxTimeuuid" in { + + val start = new dsl.DateTime().plusMinutes(-60) + val end = new dsl.DateTime().plusMinutes(60) + + val id = UUIDs.timeBased() + val user = UUIDs.random() + + val record = TimeUUIDRecord( + user, + UUIDs.timeBased(), + gen[String], + new DateTime(UUIDs.unixTimestamp(id)) + ) + + val minuteOffset = start.plusMinutes(-1).timeuuid() + val secondOffset = start.plusSeconds(-15).timeuuid() + + val record1 = TimeUUIDRecord( + user, + minuteOffset, + gen[String], + new DateTime(UUIDs.unixTimestamp(minuteOffset)) + ) + + val record2 = TimeUUIDRecord( + user, + secondOffset, + gen[String], + new DateTime(UUIDs.unixTimestamp(secondOffset)) + ) + + val chain = for { + empty <- TestDatabase.timeuuidTable.truncate().future() + store <- TestDatabase.timeuuidTable.store(record).future() + store2 <- TestDatabase.timeuuidTable.store(record1).future() + store3 <- TestDatabase.timeuuidTable.store(record2).future() + get <- TestDatabase.timeuuidTable.select + .where(_.user eqs record.user) + .and(_.id <= maxTimeuuid(end)) + .and(_.id >= minTimeuuid(start)) + .fetch() + + get2 <- TestDatabase.timeuuidTable.select + .where(_.user eqs record.user) + .and(_.id <= maxTimeuuid(end)) + .and(_.id >= minTimeuuid(start.plusMinutes(-2))) + .fetch() + } yield (get, get2) + + whenReady(chain) { + case (res, res2) => { + + res should contain(record) + + info("Should not contain record with a timestamp 1 minute before the selection window") + res should not contain record1 + + info("Should not contain record with a timestamp 15 seconds before the selection window") + res should not contain record2 + + info("Should contain all elements if we expand the selection window by 1 minute") + res2 should contain (record) + res2 should contain (record1) + res2 should contain (record2) + } + } + } + + ignore should "not retrieve anything for a mismatched selection time window" in { + + val start = new dsl.DateTime().plusMinutes(-60) + val end = new dsl.DateTime().plusMinutes(60) + + val id = UUIDs.timeBased() + val user = UUIDs.random() + + val record = TimeUUIDRecord( + user, + UUIDs.timeBased(), + gen[String], + new DateTime(UUIDs.unixTimestamp(id)) + ) + + val minuteOffset = start.plusMinutes(-1).timeuuid() + val secondOffset = start.plusSeconds(-15).timeuuid() + + val record1 = TimeUUIDRecord( + user, + minuteOffset, + gen[String], + new DateTime(UUIDs.unixTimestamp(minuteOffset)) + ) + + val record2 = TimeUUIDRecord( + user, + secondOffset, + gen[String], + new DateTime(UUIDs.unixTimestamp(secondOffset)) + ) + + val chain = for { + empty <- TestDatabase.timeuuidTable.truncate().future() + store <- TestDatabase.timeuuidTable.store(record).future() + store2 <- TestDatabase.timeuuidTable.store(record1).future() + store3 <- TestDatabase.timeuuidTable.store(record2).future() + get <- TestDatabase.timeuuidTable.select + .where(_.user eqs record.user) + .and(_.id >= minTimeuuid(start.plusMinutes(-3))) + .and(_.id <= maxTimeuuid(end.plusMinutes(-2))) + .fetch() + } yield get + + whenReady(chain) { + case res => res.size shouldEqual 0 + } + } + +} diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TimeSeriesTable.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TimeSeriesTable.scala index 8d9a79994..03f8e76be 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TimeSeriesTable.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TimeSeriesTable.scala @@ -41,6 +41,7 @@ case class TimeSeriesRecord( ) case class TimeUUIDRecord( + user: UUID, id: UUID, name: String, timestamp: DateTime @@ -65,11 +66,14 @@ sealed class TimeSeriesTable extends CassandraTable[ConcreteTimeSeriesTable, Tim abstract class ConcreteTimeSeriesTable extends TimeSeriesTable with RootConnector sealed class TimeUUIDTable extends CassandraTable[ConcreteTimeUUIDTable, TimeUUIDRecord] { - object id extends TimeUUIDColumn(this) with PartitionKey[UUID] + + object user extends UUIDColumn(this) with PartitionKey[UUID] + object id extends TimeUUIDColumn(this) with ClusteringOrder[UUID] with Descending object name extends StringColumn(this) def fromRow(row: Row): TimeUUIDRecord = { TimeUUIDRecord( + user(row), id(row), name(row), new DateTime(UUIDs.unixTimestamp(id(row))) @@ -81,6 +85,7 @@ abstract class ConcreteTimeUUIDTable extends TimeUUIDTable with RootConnector { def store(rec: TimeUUIDRecord): InsertQuery.Default[ConcreteTimeUUIDTable, TimeUUIDRecord] = { insert + .value(_.user, rec.user) .value(_.id, rec.id) .value(_.name, rec.name) } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/package.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/package.scala index eab9f3733..65c56007e 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/package.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/package.scala @@ -181,6 +181,7 @@ package object tables { val id = UUIDs.timeBased() TimeUUIDRecord( + user = gen[UUID], id = id, name = gen[ShortString].value, timestamp = new DateTime(UUIDs.unixTimestamp(id)) diff --git a/project/Build.scala b/project/Build.scala index 8d9aa39cb..f3155c99c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -108,12 +108,12 @@ object Build extends Build { bintray.BintrayKeys.bintrayReleaseOnPublish in ThisBuild := true, publishArtifact in Test := false, pomIncludeRepository := { _ => true}, - licenses += ("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0")) + licenses += ("Apache-2.0", url("https://github.com/websudos/phantom/blob/develop/LICENSE.txt")) ) val sharedSettings: Seq[Def.Setting[_]] = Defaults.coreDefaultSettings ++ Seq( organization := "com.websudos", - version := "1.22.0", + version := "1.22.1", scalaVersion := "2.11.7", crossScalaVersions := Seq("2.10.5", "2.11.7"), resolvers ++= Seq( @@ -155,7 +155,7 @@ object Build extends Build { testOptions in PerformanceTest := Seq(Tests.Filter(x => performanceFilter(x))), fork in PerformanceTest := false, parallelExecution in ThisBuild := false - ) ++ net.virtualvoid.sbt.graph.Plugin.graphSettings ++ mavenPublishSettings ++ VersionManagement.newSettings + ) ++ net.virtualvoid.sbt.graph.Plugin.graphSettings ++ publishSettings ++ VersionManagement.newSettings lazy val phantom = Project( id = "phantom",