├── .drone.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── PGP-PUBLIC-KEY ├── README.md ├── bitbucket-pipelines.yml ├── build.sbt ├── core-test └── src │ └── test │ ├── resources │ └── logback-test.xml │ └── scala │ └── org │ └── thp │ └── scalligraph │ ├── AppBuilder.scala │ ├── FPathTest.scala │ ├── RetryTest.scala │ ├── ScalligraphApplicationTest.scala │ ├── controllers │ ├── ControllerTest.scala │ ├── FieldsParserMacroTest.scala │ ├── FieldsParserTestSamples.scala │ ├── FieldsTest.scala │ ├── TestAuthSrv.scala │ ├── TestUtils.scala │ └── UpdateFieldsParserMacroTest.scala │ ├── models │ ├── CallbackTest.scala │ ├── CardinalityTest.scala │ ├── DatabaseProviders.scala │ ├── DummyUserSrv.scala │ ├── IndexTest.scala │ ├── Mesh.scala │ ├── ModelSamples.scala │ ├── Modern.scala │ ├── ModernQuery.scala │ ├── ModernTest.scala │ ├── PerformanceTest.scala │ ├── QueryTest.scala │ ├── SimpleEntityTest.scala │ └── StreamTransactionTest.scala │ └── services │ ├── IntegrityCheckTest.scala │ └── StorageSrvTest.scala ├── core ├── logback.xml └── src │ └── main │ ├── resources │ ├── play │ │ └── reference-overrides.conf │ └── reference.conf │ └── scala │ └── org │ └── thp │ └── scalligraph │ ├── AccessLogFilter.scala │ ├── Annotations.scala │ ├── ContextPropagatingDisptacher.scala │ ├── EntityId.scala │ ├── ErrorHandler.scala │ ├── Errors.scala │ ├── ScalligraphApplication.scala │ ├── ScalligraphRouter.scala │ ├── SingleInstance.scala │ ├── auth │ ├── ADAuthSrv.scala │ ├── AuthSrv.scala │ ├── BasicAuthSrv.scala │ ├── HeaderAuthenticateSrv.scala │ ├── KeyAuthSrv.scala │ ├── LdapAuthSrv.scala │ ├── MultiAuthSrv.scala │ ├── OAuth2Srv.scala │ ├── Permission.scala │ ├── PkiAuthSrv.scala │ ├── SessionAuthSrv.scala │ ├── UserSrv.scala │ └── package.scala │ ├── controllers │ ├── Annotations.scala │ ├── AuthenticatedRequest.scala │ ├── Entrypoint.scala │ ├── FPath.scala │ ├── Fields.scala │ ├── FieldsParser.scala │ ├── Output.scala │ └── UpdateFieldsParser.scala │ ├── macro │ ├── AnnotationMacro.scala │ ├── FieldsParserMacro.scala │ ├── IndexMacro.scala │ ├── MacroError.scala │ ├── MacroLogger.scala │ ├── MacroUtil.scala │ ├── MappingMacroHelper.scala │ ├── ModelMacro.scala │ └── TraversalMacro.scala │ ├── models │ ├── Database.scala │ ├── Mapping.scala │ ├── Model.scala │ ├── NoValue.scala │ ├── Operation.scala │ ├── Schema.scala │ └── TransactionHandler.scala │ ├── package.scala │ ├── query │ ├── Aggregation.scala │ ├── Filter.scala │ ├── InputSort.scala │ ├── PredicateOps.scala │ ├── PropertyBuilder.scala │ ├── PublicProperty.scala │ ├── Query.scala │ ├── QueryExecutor.scala │ └── Utils.scala │ ├── record │ ├── Record.scala │ └── RecordMacro.scala │ ├── services │ ├── EdgeSrv.scala │ ├── ElementSrv.scala │ ├── EventSrv.scala │ ├── IntegrityCheckOps.scala │ ├── ModelSrv.scala │ ├── StorageSrv.scala │ ├── VertexSrv.scala │ └── config │ │ ├── ApplicationConfig.scala │ │ ├── ConfigActor.scala │ │ ├── ConfigItem.scala │ │ ├── ConfigSerializer.scala │ │ └── ContextConfigItem.scala │ ├── traversal │ ├── BranchSelector.scala │ ├── Converter.scala │ ├── Graph.scala │ ├── IteratorOutput.scala │ ├── MatchElement.scala │ ├── ProjectionBuilder.scala │ ├── Selectors.scala │ ├── StepLabel.scala │ ├── Traversal.scala │ ├── TraversalOps.scala │ ├── TraversalPrinter.scala │ └── ValueSelector.scala │ └── utils │ ├── Config.scala │ ├── FunctionalCondition.scala │ ├── Hash.scala │ ├── Instance.scala │ ├── ProcessStats.scala │ ├── Retry.scala │ ├── RichType.scala │ └── UnthreadedExecutionContext.scala ├── database ├── arangodb │ └── src │ │ └── main │ │ └── scala │ │ └── org │ │ └── thp │ │ └── scalligraph │ │ └── arangodb │ │ └── ArangoDatabase.scala ├── janusgraph │ └── src │ │ └── main │ │ ├── java │ │ └── org │ │ │ └── thp │ │ │ └── scalligraph │ │ │ └── janus │ │ │ └── strategies │ │ │ ├── ElementValueComparatorAcceptNull.java │ │ │ ├── IndexOptimizerStrategy.java │ │ │ ├── JanusGraphAcceptNullStrategy.java │ │ │ ├── JanusGraphStepAcceptNull.java │ │ │ ├── LimitedIterator.java │ │ │ ├── MultiComparatorAcceptNull.java │ │ │ ├── MultiDistinctOrderedIteratorAcceptNull.java │ │ │ ├── OrderAcceptNullStrategy.java │ │ │ ├── OrderGlobalStepAcceptNull.java │ │ │ └── RewriteOrderGlobalStepStrategy.java │ │ ├── resources │ │ ├── play │ │ │ └── reference-overrides.conf │ │ └── reference.conf │ │ └── scala │ │ └── org │ │ └── thp │ │ └── scalligraph │ │ └── janus │ │ ├── ImmenseTermProcessor.scala │ │ ├── IndexOps.scala │ │ ├── JanusClusterManagerActor.scala │ │ ├── JanusClusterSerializer.scala │ │ ├── JanusDatabase.scala │ │ └── JanusDatabaseProvider.scala ├── neo4j │ └── src │ │ └── main │ │ ├── resources │ │ └── reference.conf │ │ └── scala │ │ └── org │ │ └── thp │ │ └── scalligraph │ │ └── neo4j │ │ └── Neo4jDatabase.scala └── orientdb │ └── src │ └── main │ ├── resources │ └── reference.conf │ └── scala │ └── org │ └── thp │ └── scalligraph │ └── orientdb │ ├── OrientDatabase.scala │ └── OrientDatabaseStorageSrv.scala ├── graphql └── src │ ├── main │ └── scala │ │ └── org │ │ └── thp │ │ └── scalligraph │ │ └── graphql │ │ ├── Order.scala │ │ ├── SchemaGenerator.scala │ │ └── package.scala │ └── test │ ├── resources │ └── graphql │ │ ├── complexQuery.expected.json │ │ ├── complexQuery.graphql │ │ ├── modernSchema.graphqls │ │ ├── queryWithBooleanOperators.expected.json │ │ ├── queryWithBooleanOperators.graphql │ │ ├── queryWithFilterObject.expected.json │ │ ├── queryWithFilterObject.graphql │ │ ├── queryWithSeveralAttributes.expected.json │ │ ├── queryWithSeveralAttributes.graphql │ │ ├── schema.graphqls │ │ ├── simpleQuery.expected.json │ │ └── simpleQuery.graphql │ └── scala │ └── org │ └── thp │ └── scalligraph │ └── graphql │ └── SangriaTest.scala ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt └── sbt /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: default 4 | type: docker 5 | 6 | steps: 7 | # Restore cache of downloaded dependencies 8 | - name: restore-cache 9 | image: drillster/drone-volume-cache 10 | settings: 11 | restore: true 12 | backend: "filesystem" 13 | mount: 14 | - .sbt 15 | - .ivy2 16 | - .cache 17 | volumes: [{name: cache, path: /cache}] 18 | 19 | # Run project tests 20 | - name: run-tests 21 | image: thehiveproject/drone-scala-node 22 | commands: 23 | - sbt -Duser.home=$PWD test:compile test 24 | 25 | # Save external libraries in cache 26 | - name: save-cache 27 | image: drillster/drone-volume-cache 28 | settings: 29 | rebuild: true 30 | backend: "filesystem" 31 | mount: 32 | - .sbt 33 | - .ivy2 34 | - .cache 35 | volumes: [{name: cache, path: /cache}] 36 | 37 | - name: send message 38 | image: thehiveproject/drone_keybase 39 | settings: 40 | username: {from_secret: keybase_username} 41 | paperkey: {from_secret: keybase_paperkey} 42 | channel: {from_secret: keybase_channel} 43 | commands: 44 | - | 45 | keybase oneshot -u "$PLUGIN_USERNAME" --paperkey "$PLUGIN_PAPERKEY" 46 | URL="$DRONE_SYSTEM_PROTO://$DRONE_SYSTEM_HOST/$DRONE_REPO/$DRONE_BUILD_NUMBER" 47 | if [ $DRONE_BUILD_STATUS = "success" ] 48 | then 49 | keybase chat send "$PLUGIN_CHANNEL" ":white_check_mark: $DRONE_REPO: build succeeded $URL" 50 | else 51 | keybase chat send "$PLUGIN_CHANNEL" ":x: $DRONE_REPO: build failed $URL" 52 | fi 53 | when: 54 | status: 55 | - success 56 | - failure 57 | 58 | volumes: 59 | - name: cache 60 | host: 61 | path: /opt/drone/cache 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | bin 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | RUNNING_PID 15 | .cache-main 16 | .cache-tests 17 | sbt-launch.jar 18 | .bsp/ 19 | 20 | # Eclipse 21 | .project 22 | .target 23 | .settings 24 | tmp 25 | .classpath 26 | 27 | # IntelliJ IDEA 28 | /*.iml 29 | /out 30 | /.idea_modules 31 | /.idea/** 32 | !/.idea/runConfigurations/ 33 | !/.idea/runConfigurations/* 34 | 35 | # VSCode 36 | .vscode/ 37 | .bloop/ 38 | .metals/ 39 | metals.sbt 40 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.6.4 2 | project.git = true 3 | align = more # For pretty alignment. 4 | assumeStandardLibraryStripMargin = true 5 | style = defaultWithAlign 6 | maxColumn = 150 7 | 8 | align.openParenCallSite = false 9 | align.openParenDefnSite = false 10 | newlines.alwaysBeforeTopLevelStatements = false 11 | rewrite.rules = [ 12 | RedundantBraces 13 | RedundantParens 14 | SortModifiers 15 | PreferCurlyFors 16 | SortImports 17 | ] 18 | 19 | includeCurlyBraceInSelectChains = true 20 | includeNoParensInSelectChains = true 21 | 22 | rewriteTokens { 23 | "⇒" : "=>" 24 | "←" : "<-" 25 | "→" : "->" 26 | } 27 | -------------------------------------------------------------------------------- /PGP-PUBLIC-KEY: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1 3 | 4 | mQINBFkRxeYBEAC8N672/5USJztb1R4pn8zB2/fujg71uAh1ZEERbiMFHC0IJbuh 5 | 8aF0F+tn1YIbMuI8B1byMOLhSRnnuw630StbnUsVqRbLXg3plLDAg50I1v90pXfO 6 | FiOOG1XApnb8NhS+huGwIXXLk65NE1MPwhPC9jxK1Bx/RJZNKg9UDz5+YPPEydhh 7 | ZJ+35UKb94DuY/YsqMCSYJwlHL3ZnqENcQvylVD9TSdJCJ51xu655kbbZks1Amaz 8 | j7aWJQs8ur9nhEw/au221rUdOrS0XW5m4XCfE7hTqjREjDEGsUHilbsO+RH2f2+h 9 | PWalG/9aN2f1maiAezAuT/agFBWbuyYqBfSvuZOMCuLdXsiQNxdPJPcEQUIcO1Qf 10 | g4RTWJCoXrlL2QQSSstswPrlZxloHsGvl0AtAlard2+rr6Fz1QG3hokOhFav8LS+ 11 | 0lbwQpR1K3RY2HUrof1r4HCQHqEJ76q0bwk9vhd8cAA9tc5uTh+oBbKXaPyO/8Be 12 | SUiBU94kJd12RR81FIF4bAcqgrTi8JDj+EaXeGH5b6eUiRmEWCSzQpq6nOYMYBQw 13 | 4r62v4zYYq0NgM6f8rZ+EeuvCiGxSGAzvtLtKRYZRYHZanasO/pyVy6cpRxiWVRR 14 | 5+smEarOU8ul1o8D19si/sjFAs9nQMs20R+O76wHDb32aAKTjiRWkTUAVQARAQAB 15 | tENUaGVIaXZlIFByb2plY3QgKFRoZUhpdmUgcmVsZWFzZSBrZXkpIDxzdXBwb3J0 16 | QHRoZWhpdmUtcHJvamVjdC5vcmc+iQI4BBMBAgAiBQJZEcXmAhsDBgsJCAcDAgYV 17 | CAIJCgsEFgIDAQIeAQIXgAAKCRA9mbsYViy8HHcvD/9yZvUvNGDJ//8frjwuRvWa 18 | 3jUxoX6m+KcUSHOf1wc6SyVZ/E0AJKDXMkYBJAjv5FvJOu//XGcMNwiNtR8gtZnd 19 | razokSbUcg8osijPWsNFQ5te7wMqo34M3GP2rpXz+QC6UMd2/UDk6mdWtgqzTIzi 20 | MsXm4A9p5So54Twkl32ZIfCG952rpQdr5lTq4FM5usZhtQzpOtm240ChOozU8SBE 21 | CKH49I4iNBiFUNCHNivmGEA8CpP6ouCJaAmMF6cruyxRsZnwImU1SvjXUJYGdBQ8 22 | u5pEvSQLhCwo12o7dATUmBMLNW05ksewDYVOj52lMg29TAZ+e0dBKCzMyz6+HpIk 23 | 6XJ0NZFWwLgzex863KI7S/ytErUKSjUhwHfoWhEkLvpUtuhHRbv5Ryyg7da7bp+B 24 | 4Q3MJbAzgOvZMXPlJpu4luIB5eEwRYyYQ3W7f1dSSklkaFuKh1rc0EACREJ7Y2e4 25 | Vt8++zRbp1oFGQ5+0s64HlEBiaujc7F2/BRFR1MKE9Fd6WH2YljkQ0DidY7IWdTK 26 | FxN7YPM9jeF51FP+W0I7PvK415W66UF60CDBtPL0wE7cQuujB856IDwZKPKi5Uro 27 | ivsVewKw/nN4T92Xk87sdnYDBvQ++nQmg5SDTET+IJ6cP9OzDQ3ecmgSSAPw02jT 28 | pl/GX01j04rUUOOpEaXttrkCDQRZEcXmARAAqgHpWyaPlD0mdVLnXMg9cxVQyR8X 29 | VTQ7dmszgsRNtGdr60T6FZ5ORb5EowkBHKe3vfyUtrYmDhyFr0sikitYxIjHAIdn 30 | EFgHWRHBPcc/fwt8omPyVtnNcVTJ/p27t01hwBzGNVvDbI6Lqj1X6MmWY8tphJu6 31 | Ox0GozJZU8J3FCor0BqI/yeR4X3rFRfiZfz2Ejc/6tktfyKlSyPS8tNkPn0CulNa 32 | TpYAhZ3S4coGlaC1MQcRRbGjuMRXZozLxYVDF3AvGXuwAtH53nly+skIG9Exk9UW 33 | jfy6SHlKjq9UW5kSeFE4uRJkJBQeqXg/rO337rquJkUobiizHnepRtnjHxnWpHNs 34 | jiZbEYgbHJBqNhuf1qaTVdup8mTOSVzUynr1aNHrQCmHkVR/P/fWOF09R9p+lRCp 35 | I3yZwXbACvFeScidMtm1T8aFItmcQ+QXtyrRPvOvy8jCO/gkDpKm/NE5f3nT4Tg6 36 | DOKtEQlo657FyG3t7STZ6H7uD2dd2TTWxelCaO9GaR743ybJYb5H02V9Dm3qIYFb 37 | 1cafz8Q43kAqcq29/Sgpx8yHv2SmsFilAfuW2ic0Mi6DbRUiF7p1qKLIj+ZsP3Ax 38 | Mo1FwZI4zHlKKwm2Uapyqmd+xF9y7Y2pFgNYfDuj6/nd0VPFZ1T+6+9RjPWT7TPB 39 | 1+Eaf/o3/v31YZ8AEQEAAYkCHwQYAQIACQUCWRHF5gIbDAAKCRA9mbsYViy8HBEZ 40 | EACFmsm9jPNFkjw7w6XC1+V4lY9HUQagwwf41XcYWt5gYlJww2oO+PzG9MIfj+25 41 | mrmkKoVbNZhJmjH1ZgME5aIDbw+gSn3tsuBKriuryPS9aKjfDDpN2SmxO3N+m+uR 42 | OxwoFvzzQUeba8ItdED4ATUj5qBDcwdTBZWrPDlC/Pr1ASY4NrG6f5uSUyTNwaR+ 43 | l1kKAGZxDm3/8tkI9UAmvSC2i0yxuOyHj9rpuz57aAKAvMu3vNGd7eU4bLPCuZFu 44 | FhZvU6wAd/1+oLIVYUVjF1Nh3RgF+mn2MzH5AvShIpZzOkajY91ebQYAZ55AU2iU 45 | y3C1sjSRa1lWzxbThJBT5Sm1B35vTABaM1m25HcjumKeU2XtfN4EArlxuxZRHSbQ 46 | 2ceWDPm5TuvjMyZdb5u90xpJ/VGmFYxqjfR1nwku9mY1JKDvZgI9i3Gm+JgrDI4c 47 | ldFFLTSdSASnOfW1P+flMKUkDOBIIOYJtZgZ9BU9U/lU5/ZssPoCWkfwy022anqu 48 | wR6Z0Ao/cKE+l+XoXFOvtcRvJyfarXs9EON0s/HNyPRpy20KPyv2lCwm11bhGoI3 49 | LGYf9udLuLm2Dex9U4+Hs2KWDT9QeVmS7OQUz0Yg184CkVWGLqaVIReYv9q7qPlI 50 | CykvrXDiG+C5OTn1wvPRUX1wA1f7dRsYXOK3AHxz65zMSw== 51 | =f0Hb 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | image: bitbucketpipelines/scala-sbt:scala-2.12 2 | 3 | pipelines: 4 | custom: 5 | build_on_demand: 6 | - step: 7 | caches: 8 | - sbt 9 | - ivy2 10 | script: 11 | - sbt test 12 | -------------------------------------------------------------------------------- /core-test/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ${application.home:-.}/logs/application.log 6 | 7 | ${application.home:-.}/logs/application.%i.log.zip 8 | 1 9 | 10 10 | 11 | 12 | 10MB 13 | 14 | 15 | %date [%level] from %logger in %thread - %message%n%xException 16 | 17 | 18 | 19 | 20 | 21 | %logger{15} - %message%n%xException{10} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/FPathTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import org.specs2.mutable.Specification 4 | import org.thp.scalligraph.controllers._ 5 | 6 | class FPathTest extends Specification { 7 | 8 | "path" should { 9 | "parse elements" in { 10 | FPath("a__.b__.c__") must_=== FPathElem("a__", FPathElem("b__", FPathElem("c__", FPathEmpty))) 11 | } 12 | 13 | "parse indexed seq elements" in { 14 | FPath("a[3].b.c[2]") must_=== FPathElemInSeq("a", 3, FPathElem("b", FPathElemInSeq("c", 2, FPathEmpty))) 15 | } 16 | 17 | "parse seq elements" in { 18 | FPath("a[].b.c[]") must_=== FPathSeq("a", FPathElem("b", FPathSeq("c", FPathEmpty))) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/RetryTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import org.thp.scalligraph.utils.Retry 4 | import play.api.test.PlaySpecification 5 | 6 | import scala.util.Success 7 | 8 | class RetryTest extends PlaySpecification { 9 | 10 | "Retry" should { 11 | "catch direct exception" in { 12 | var count = 0 13 | Retry(4) 14 | .on[ArithmeticException] 15 | .withTry { 16 | count += 1 17 | Success(12 / (count - 1)) 18 | } 19 | count must_=== 2 20 | } 21 | 22 | "catch origin exception" in { 23 | var count = 0 24 | Retry(4) 25 | .on[ArithmeticException] 26 | .withTry { 27 | count += 1 28 | try { 29 | Success(12 / (count - 1)) 30 | } catch { case t: Throwable => throw new RuntimeException("wrap", t) } 31 | } 32 | count must_=== 2 33 | } 34 | 35 | "retry until limit is reached" in { 36 | var count = 0 37 | Retry(4) 38 | .on[ArithmeticException] 39 | .withTry { 40 | count += 1 41 | Success(12 / (count - count)) 42 | } must beFailedTry 43 | count must_=== 4 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/ScalligraphApplicationTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import com.google.inject.Inject 4 | import net.codingwell.scalaguice.{ScalaModule, ScalaMultibinder} 5 | import org.specs2.mock.Mockito 6 | import org.thp.scalligraph.auth.{AuthSrv, UserSrv} 7 | import org.thp.scalligraph.models.{Database, Schema, UpdatableSchema} 8 | import org.thp.scalligraph.query.QueryExecutor 9 | import play.api.cache.caffeine.CaffeineCacheModule 10 | import play.api.i18n.{I18nModule => PlayI18nModule} 11 | import play.api.inject.guice.GuiceApplicationBuilder 12 | import play.api.inject.{BuiltinModule => PlayBuiltinModule} 13 | import play.api.libs.logback.LogbackLoggerConfigurator 14 | import play.api.mvc.{CookiesModule => PlayCookiesModule} 15 | import play.api.routing.{Router => PlayRouter} 16 | import play.api.test.PlaySpecification 17 | import play.api.{Configuration, Environment} 18 | 19 | trait TestService { 20 | def id: String 21 | } 22 | 23 | class TestService1 @Inject() (parentTestService: ParentProvider[TestService]) extends TestService { 24 | lazy val parentServiceId: String = parentTestService.get().fold("**")(_.id) 25 | def id: String = s"$parentServiceId" 26 | } 27 | 28 | class TestService2 @Inject() (parentTestService: ParentProvider[TestService]) extends TestService { 29 | lazy val parentServiceId: String = parentTestService.get().fold("**")(_.id) 30 | def id: String = s"$parentServiceId" 31 | } 32 | 33 | class TestService3 @Inject() (parentTestService: ParentProvider[TestService]) extends TestService { 34 | lazy val parentServiceId: String = parentTestService.get().fold("**")(_.id) 35 | def id: String = s"$parentServiceId" 36 | } 37 | 38 | class TestServiceModule[TestServiceImpl <: TestService: Manifest] extends ScalaModule { 39 | override def configure(): Unit = { 40 | bind[TestService].to[TestServiceImpl] 41 | () 42 | } 43 | } 44 | 45 | object TestModule extends ScalaModule with Mockito { 46 | override def configure(): Unit = { 47 | bind[AuthSrv].toInstance(mock[AuthSrv]) 48 | bind[UserSrv].toInstance(mock[UserSrv]) 49 | bind[Database].toInstance(mock[Database]) 50 | ScalaMultibinder.newSetBinder[Schema](binder) 51 | ScalaMultibinder.newSetBinder[QueryExecutor](binder) 52 | ScalaMultibinder.newSetBinder[PlayRouter](binder) 53 | ScalaMultibinder.newSetBinder[UpdatableSchema](binder) 54 | () 55 | } 56 | } 57 | 58 | class ScalligraphApplicationTest extends PlaySpecification { 59 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 60 | 61 | "create an application with overridden module" in { 62 | val applicationBuilder = GuiceApplicationBuilder() 63 | .load( 64 | new PlayBuiltinModule, 65 | new PlayI18nModule, 66 | new PlayCookiesModule, 67 | new CaffeineCacheModule, 68 | new TestServiceModule[TestService1], 69 | new TestServiceModule[TestService2], 70 | new TestServiceModule[TestService3], 71 | TestModule 72 | ) 73 | 74 | val application = applicationBuilder 75 | .load(ScalligraphApplicationLoader.loadModules(applicationBuilder.loadModules)) 76 | .build 77 | val injector = application.injector 78 | 79 | injector.instanceOf[TestService].id must_=== "**" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/controllers/ControllerTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import org.specs2.concurrent.ExecutionEnv 4 | import org.specs2.mock.Mockito 5 | import org.thp.scalligraph.ErrorHandler 6 | import org.thp.scalligraph.auth.AuthSrv 7 | import play.api.inject.guice.GuiceApplicationBuilder 8 | import play.api.libs.json.Json 9 | import play.api.libs.logback.LogbackLoggerConfigurator 10 | import play.api.mvc.{AnyContentAsJson, DefaultActionBuilder, Results} 11 | import play.api.test.{FakeRequest, Helpers, PlaySpecification} 12 | import play.api.{Application, Configuration, Environment} 13 | 14 | import scala.util.Success 15 | 16 | class ControllerTest(implicit executionEnv: ExecutionEnv) extends PlaySpecification with Mockito { 17 | lazy val app: Application = new GuiceApplicationBuilder().build() 18 | 19 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 20 | 21 | "controller" should { 22 | 23 | "extract simple class from HTTP request" in { 24 | 25 | val actionBuilder = DefaultActionBuilder(Helpers.stubBodyParser()) 26 | val entrypoint = new Entrypoint(mock[AuthSrv], actionBuilder, new ErrorHandler, executionEnv.ec) 27 | 28 | val action = entrypoint("model extraction") 29 | .extract("simpleClass", FieldsParser[SimpleClassForFieldsParserMacroTest]) { req => 30 | val simpleClass = req.body("simpleClass") 31 | simpleClass must_=== SimpleClassForFieldsParserMacroTest("myName", 44) 32 | Success(Results.Ok("ok")) 33 | } 34 | 35 | val request = FakeRequest("POST", "/api/simple_class").withBody(AnyContentAsJson(Json.obj("name" -> "myName", "value" -> 44))) 36 | val result = action(request) 37 | val bodyText = contentAsString(result) 38 | bodyText must be equalTo "ok" 39 | } 40 | 41 | // "render stream with total number of element in header" in { 42 | // 43 | // val actionBuilder = DefaultActionBuilder(Helpers.stubBodyParser()) 44 | // val entrypoint = new EntryPoint(mock[AuthenticateSrv], actionBuilder, new ErrorHandler, ee.ec, mat) 45 | // 46 | // val action = entrypoint("find entity") 47 | // .chunked(_ => Source(0 to 3).mapMaterializedValue(_ => 10)) 48 | // val request = FakeRequest("GET", "/") 49 | // val result = Await.result(action(request), 1.second) 50 | // result.header.headers("X-Total") must_=== "10" 51 | // result.body.contentType must beSome("application/json") 52 | // Await.result(result.body.consumeData.map(_.decodeString("utf-8")), 1.second) must_=== Json.arr(0, 1, 2, 3).toString 53 | // } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/controllers/FieldsParserTestSamples.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import org.scalactic.Good 4 | 5 | case class SimpleClassForFieldsParserMacroTest(name: String, value: Int) 6 | 7 | case class SubClassForFieldsParserMacroTest(name: String, option: Option[Int]) 8 | 9 | case class ComplexClassForFieldsParserMacroTest(name: String, value: Int, subClasses: Seq[SubClassForFieldsParserMacroTest]) 10 | 11 | //case class SubMultiAttachClassForFieldsParserMacroTest(name: String, mainAttach: Attachment, otherAttach: Seq[Attachment]) 12 | //case class MultiAttachClassForFieldsParserMacroTest(name: String, attachments: Seq[SubMultiAttachClassForFieldsParserMacroTest]) 13 | 14 | @WithParser(CustomFieldsParsers.englishIntFieldsParser) 15 | @WithUpdateParser(CustomFieldsParsers.englishUpdateFieldsParser) 16 | case class LocaleInt(value: Int) 17 | 18 | object CustomFieldsParsers { 19 | 20 | val englishIntFieldsParser: FieldsParser[LocaleInt] = FieldsParser[LocaleInt]("englishInt", Set("one", "two", "three")) { 21 | case (_, FString("one")) => Good(LocaleInt(1)) 22 | case (_, FString("two")) => Good(LocaleInt(2)) 23 | case (_, FString("three")) => Good(LocaleInt(3)) 24 | } 25 | 26 | val frenchIntFieldsParser: FieldsParser[LocaleInt] = FieldsParser[LocaleInt]("frenchInt", Set("un", "deux", "trois")) { 27 | case (_, FString("un")) => Good(LocaleInt(1)) 28 | case (_, FString("deux")) => Good(LocaleInt(2)) 29 | case (_, FString("trois")) => Good(LocaleInt(3)) 30 | } 31 | 32 | val englishUpdateFieldsParser: UpdateFieldsParser[LocaleInt] = 33 | UpdateFieldsParser[LocaleInt]("englishLocalInt", Seq(FPath.empty -> englishIntFieldsParser)) 34 | 35 | val frenchUpdateFieldsParser: UpdateFieldsParser[LocaleInt] = 36 | UpdateFieldsParser[LocaleInt]("frenchLocalInt", Seq(FPath.empty -> frenchIntFieldsParser)) 37 | } 38 | 39 | case class ClassWithAnnotation( 40 | name: String, 41 | @WithParser(CustomFieldsParsers.frenchIntFieldsParser) 42 | @WithUpdateParser(CustomFieldsParsers.frenchUpdateFieldsParser) 43 | valueFr: LocaleInt, 44 | valueEn: LocaleInt 45 | ) 46 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/controllers/TestAuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import javax.inject.Inject 4 | import org.thp.scalligraph.auth.{HeaderAuthSrv, RequestOrganisation, UserSrv} 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | class TestAuthSrv @Inject() (userSrv: UserSrv, ec: ExecutionContext) 9 | extends HeaderAuthSrv("user", new RequestOrganisation(Some("X-Organisation"), None, None, None), None, userSrv, ec) 10 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/controllers/TestUtils.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import org.thp.scalligraph.`macro`.FieldsParserMacro 4 | 5 | import scala.language.experimental.macros 6 | 7 | trait TestUtils { 8 | def getFieldsParser[T]: FieldsParser[T] = macro FieldsParserMacro.getOrBuildFieldsParser[T] 9 | def getUpdateFieldsParser[T]: UpdateFieldsParser[T] = macro FieldsParserMacro.getOrBuildUpdateFieldsParser[T] 10 | } 11 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/controllers/UpdateFieldsParserMacroTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import org.scalactic.{Bad, Good, One} 4 | import org.specs2.mutable.Specification 5 | import org.thp.scalligraph.InvalidFormatAttributeError 6 | 7 | class UpdateFieldsParserMacroTest extends Specification with TestUtils { 8 | 9 | "UpdateFieldParser macro" should { 10 | 11 | "parse a simple class" in { 12 | val fieldsParser = getUpdateFieldsParser[SimpleClassForFieldsParserMacroTest] 13 | val fields = FObject("name" -> FString("simpleClass")) 14 | val updates = Seq(FPath("name") -> "simpleClass") 15 | fieldsParser(fields) must_=== Good(updates) 16 | } 17 | 18 | "make all fields of complex class updatable" in { 19 | val fieldsParser = getUpdateFieldsParser[ComplexClassForFieldsParserMacroTest] 20 | fieldsParser.parsers.map(_._1.toString) must contain(exactly("", "name", "value", "subClasses[]", "subClasses[].name", "subClasses[].option")) 21 | } 22 | 23 | "parse complex class" in { 24 | val fieldsParser = getUpdateFieldsParser[ComplexClassForFieldsParserMacroTest] 25 | val fields = FObject("subClasses[0].name" -> FString("sc1"), "subClasses[1].option" -> FNull) 26 | val updates = Seq(FPath("subClasses[0].name") -> "sc1", FPath("subClasses[1].option") -> None) 27 | fieldsParser(fields) must_=== Good(updates) 28 | } 29 | 30 | "parse class with annotation" in { 31 | val fieldsParser = getUpdateFieldsParser[ClassWithAnnotation] 32 | val fields = FObject("valueFr" -> FString("un"), "valueEn" -> FString("three")) 33 | val updates = Seq(FPath("valueFr") -> LocaleInt(1), FPath("valueEn") -> LocaleInt(3)) 34 | fieldsParser(fields) must_=== Good(updates) 35 | } 36 | 37 | "parse class with implicit" in { 38 | implicit val subClassFieldsParser: UpdateFieldsParser[SubClassForFieldsParserMacroTest] = 39 | UpdateFieldsParser[SubClassForFieldsParserMacroTest]( 40 | "SubClassForFieldsParserMacroTest", 41 | Seq(FPath("option") -> FieldsParser.build[Option[Int]], FPath("name") -> FieldsParser.build[String]) 42 | ) 43 | val fieldsParser = getUpdateFieldsParser[ComplexClassForFieldsParserMacroTest] 44 | 45 | val fields = FObject("subClasses[0].option" -> FNumber(3), "subClasses[1].option" -> FNull) 46 | val updates = Seq(FPath("subClasses[0].option") -> Some(3), FPath("subClasses[1].option") -> None) 47 | fieldsParser(fields) must_=== Good(updates) 48 | } 49 | 50 | "return an error if provided fields is not correct" in { 51 | val fieldsParser = getUpdateFieldsParser[SimpleClassForFieldsParserMacroTest] 52 | val fields = FObject("name" -> FNumber(12)) // invalid format 53 | 54 | fieldsParser(fields) must_=== Bad(One(InvalidFormatAttributeError("name", "string", Set("string"), FNumber(12)))) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/CallbackTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Transaction.Status 4 | import org.specs2.specification.core.Fragments 5 | import play.api.libs.logback.LogbackLoggerConfigurator 6 | import play.api.test.PlaySpecification 7 | import play.api.{Configuration, Environment} 8 | 9 | class CallbackTest extends PlaySpecification { 10 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 11 | 12 | Fragments.foreach(new DatabaseProviders().list) { dbProvider => 13 | val db: Database = dbProvider.get() 14 | 15 | s"[${dbProvider.name}] entity" should { 16 | "execute transaction callbacks when readonly transaction is committed" in { 17 | var commitFlag = 0 18 | var rollbackFlag = 0 19 | db.roTransaction { implicit graph => 20 | db.addTransactionListener({ 21 | case Status.COMMIT => commitFlag += 1 22 | case Status.ROLLBACK => rollbackFlag += 1 23 | }) 24 | } 25 | (commitFlag, rollbackFlag) must beEqualTo((1, 0)) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/CardinalityTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.specs2.specification.core.Fragments 4 | import org.thp.scalligraph.BuildVertexEntity 5 | import org.thp.scalligraph.auth.{AuthContext, UserSrv} 6 | import org.thp.scalligraph.services.VertexSrv 7 | import org.thp.scalligraph.traversal.Graph 8 | import org.thp.scalligraph.traversal.TraversalOps._ 9 | import play.api.libs.logback.LogbackLoggerConfigurator 10 | import play.api.test.PlaySpecification 11 | import play.api.{Configuration, Environment} 12 | 13 | import scala.util.{Success, Try} 14 | 15 | @BuildVertexEntity 16 | case class EntityWithSeq(name: String, valueList: Seq[String], valueSet: Set[String]) 17 | 18 | class EntityWithSeqSrv extends VertexSrv[EntityWithSeq] { 19 | def create(e: EntityWithSeq)(implicit graph: Graph, authContext: AuthContext): Try[EntityWithSeq with Entity] = createEntity(e) 20 | } 21 | 22 | class CardinalityTest extends PlaySpecification { 23 | 24 | val userSrv: UserSrv = DummyUserSrv() 25 | implicit val authContext: AuthContext = userSrv.getSystemAuthContext 26 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 27 | 28 | Fragments.foreach(new DatabaseProviders().list) { dbProvider => 29 | val db: Database = dbProvider.get() 30 | db.createSchema(Model.vertex[EntityWithSeq]) 31 | db.addSchemaIndexes(Model.vertex[EntityWithSeq]) 32 | val entityWithSeqSrv: EntityWithSeqSrv = new EntityWithSeqSrv 33 | 34 | s"[${dbProvider.name}] entity" should { 35 | "create with empty list and set" in db.transaction { implicit graph => 36 | val initialEntity = EntityWithSeq("The answer", Seq.empty, Set.empty) 37 | entityWithSeqSrv.create(initialEntity) must beSuccessfulTry.which { createdEntity => 38 | createdEntity._id must_!== null 39 | initialEntity must_=== createdEntity 40 | entityWithSeqSrv.getOrFail(createdEntity._id) must beSuccessfulTry(createdEntity) 41 | } 42 | } 43 | 44 | "create and get entities with list property" in db.transaction { implicit graph => 45 | val initialEntity = EntityWithSeq("list", Seq("1", "2", "3"), Set.empty) 46 | entityWithSeqSrv.create(initialEntity) must beSuccessfulTry.which { createdEntity => 47 | initialEntity must_=== createdEntity 48 | entityWithSeqSrv.getOrFail(createdEntity._id) must beSuccessfulTry(createdEntity) 49 | } 50 | } 51 | 52 | "create and get entities with set property" in db.transaction { implicit graph => 53 | val initialEntity = EntityWithSeq("list", Seq.empty, Set("a", "b", "c")) 54 | entityWithSeqSrv.create(initialEntity) must beSuccessfulTry.which { createdEntity => 55 | initialEntity must_=== createdEntity 56 | entityWithSeqSrv.getOrFail(createdEntity._id) must_=== Success(createdEntity) 57 | } 58 | } 59 | 60 | "be searchable from its list property" in db.transaction { implicit graph => 61 | val initialEntity = EntityWithSeq("list", Seq("1", "2", "3"), Set.empty) 62 | entityWithSeqSrv.create(initialEntity) must beSuccessfulTry.which { createdEntity => 63 | entityWithSeqSrv.startTraversal.has(_.valueList, "1").getOrFail("EntityWithSeq") must beSuccessfulTry(createdEntity) 64 | // This test fails with OrientDB : https://github.com/orientechnologies/orientdb-gremlin/issues/120 65 | } 66 | } 67 | 68 | // "update an entity" in db.transaction { implicit graph => 69 | // val id = entityWithSeqSrv.create(EntityWithSeq("super", 7))._id 70 | // entityWithSeqSrv.update(id, "value", 8) 71 | // 72 | // entityWithSeqSrv.getOrFail(id).value must_=== 8 73 | // } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/DatabaseProviders.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import akka.actor.ActorSystem 4 | import com.typesafe.config.ConfigFactory 5 | import javax.inject.{Inject, Provider} 6 | import org.thp.scalligraph.janus.JanusDatabase 7 | import play.api.{Configuration, Environment, Logger} 8 | //import org.thp.scalligraph.neo4j.Neo4jDatabase 9 | //import org.thp.scalligraph.orientdb.OrientDatabase 10 | 11 | class DatabaseProviders @Inject() (config: Configuration, system: ActorSystem) { 12 | 13 | def this(system: ActorSystem) = 14 | this( 15 | Configuration(ConfigFactory.parseString(s""" 16 | |db.janusgraph.storage.directory = target/janusgraph-test-database-${math.random}.db 17 | |db.janusgraph.index.search.backend = lucene 18 | |db.janusgraph.index.search.directory = target/janusgraph-test-database-${math.random}.idx 19 | |""".stripMargin)) withFallback 20 | Configuration.load(Environment.simple()), 21 | system 22 | ) 23 | 24 | def this() = this(ActorSystem("DatabaseProviders")) 25 | 26 | lazy val logger: Logger = Logger(getClass) 27 | 28 | lazy val janus: DatabaseProvider = new DatabaseProvider("janus", new JanusDatabase(config, system, fullTextIndexAvailable = false)) 29 | 30 | // lazy val orientdb: DatabaseProvider = new DatabaseProvider("orientdb", new OrientDatabase(config, system)) 31 | // 32 | // lazy val neo4j: DatabaseProvider = new DatabaseProvider("neo4j", new Neo4jDatabase(config, system)) 33 | 34 | lazy val list: Seq[DatabaseProvider] = janus /* :: orientdb :: neo4j*/ :: Nil 35 | } 36 | 37 | class DatabaseProvider(val name: String, db: => Database) extends Provider[Database] { 38 | private lazy val _db = db 39 | 40 | override def get(): Database = _db 41 | } 42 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/DummyUserSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.thp.scalligraph.{EntityIdOrName, EntityName} 4 | import org.thp.scalligraph.auth._ 5 | import play.api.libs.json.JsObject 6 | import play.api.mvc.RequestHeader 7 | 8 | import scala.util.{Success, Try} 9 | 10 | case class DummyUserSrv( 11 | userId: String = "admin", 12 | userName: String = "default admin user", 13 | organisation: String = "admin", 14 | permissions: Set[Permission] = Set.empty, 15 | requestId: String = "testRequest" 16 | ) extends UserSrv { userSrv => 17 | 18 | val authContext: AuthContext = 19 | AuthContextImpl(userSrv.userId, userSrv.userName, EntityName(userSrv.organisation), userSrv.requestId, userSrv.permissions) 20 | override def getAuthContext(request: RequestHeader, userId: String, organisationName: Option[EntityIdOrName]): Try[AuthContext] = 21 | Success(authContext) 22 | 23 | override def getSystemAuthContext: AuthContext = authContext 24 | 25 | override def createUser(userId: String, userInfo: JsObject): Try[User] = ??? 26 | } 27 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/IndexTest.scala: -------------------------------------------------------------------------------- 1 | //package org.thp.scalligraph.models 2 | // 3 | //import org.specs2.specification.core.Fragments 4 | //import org.thp.scalligraph.{BuildVertexEntity, EntityName} 5 | //import org.thp.scalligraph.auth.{AuthContext, AuthContextImpl} 6 | //import play.api.libs.logback.LogbackLoggerConfigurator 7 | //import play.api.test.PlaySpecification 8 | //import play.api.{Configuration, Environment} 9 | // 10 | //@DefineIndex(IndexType.unique, "name") 11 | //@BuildVertexEntity 12 | //case class EntityWithUniqueName(name: String, value: Int) 13 | // 14 | //class IndexTest extends PlaySpecification { 15 | // (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 16 | // val authContext: AuthContext = AuthContextImpl("me", "", EntityName(""), "", Set.empty) 17 | // 18 | // Fragments.foreach(new DatabaseProviders().list) { dbProvider => 19 | // implicit val db: Database = dbProvider.get() 20 | // val model = Model.vertex[EntityWithUniqueName] 21 | // db.createSchema(model) 22 | // db.addSchemaIndexes(model) 23 | // 24 | // s"[${dbProvider.name}] Creating duplicate entries on unique index constraint" should { 25 | // "throw an exception in the same transaction" in { 26 | // db.transaction { implicit graph => 27 | // db.createVertex(graph, authContext, model, EntityWithUniqueName("singleTransaction", 1)) 28 | // db.createVertex(graph, authContext, model, EntityWithUniqueName("singleTransaction", 2)) 29 | // } must throwA[Exception] 30 | // } 31 | // 32 | // "throw an exception in the different transactions" in { 33 | // { 34 | // db.transaction { implicit graph => 35 | // db.createVertex(graph, authContext, model, EntityWithUniqueName("singleTransaction", 1)) 36 | // } 37 | // db.transaction { implicit graph => 38 | // db.createVertex(graph, authContext, model, EntityWithUniqueName("singleTransaction", 2)) 39 | // } 40 | // } must throwA[Exception] 41 | // } 42 | // 43 | //// "throw an exception in overlapped transactions" in { 44 | //// def synchronizedElementCreation(name: String, waitBeforeCreate: Future[Unit], waitBeforeCommit: Future[Unit]): Future[Unit] = 45 | //// Future { 46 | //// db.transaction { implicit graph => 47 | //// Await.result(waitBeforeCreate, 2.seconds) 48 | //// db.createVertex(graph, authContext, model, EntityWithUniqueName(name, 1)) 49 | //// Await.result(waitBeforeCommit, 2.seconds) 50 | //// } 51 | //// } 52 | //// 53 | //// val waitBeforeCreate = Promise[Unit] 54 | //// val waitBeforeCommit = Promise[Unit] 55 | //// val f1 = synchronizedElementCreation("overlappedTransaction", waitBeforeCreate.future, waitBeforeCommit.future) 56 | //// val f2 = synchronizedElementCreation("overlappedTransaction", waitBeforeCreate.future, waitBeforeCommit.future) 57 | //// waitBeforeCreate.success(()) 58 | //// waitBeforeCommit.success(()) 59 | //// Await.result(f1.flatMap(_ => f2), 5.seconds) must throwA[Exception] 60 | //// } 61 | // } 62 | // } 63 | //} 64 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/Mesh.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.thp.scalligraph.auth.AuthContext 4 | import org.thp.scalligraph.services.{EdgeSrv, VertexSrv} 5 | import org.thp.scalligraph.traversal.{Graph, Traversal} 6 | import org.thp.scalligraph.traversal.TraversalOps._ 7 | import org.thp.scalligraph.{BuildEdgeEntity, BuildVertexEntity, EntityId} 8 | 9 | import scala.util.Try 10 | 11 | @BuildVertexEntity 12 | case class A(name: String) 13 | 14 | @BuildVertexEntity 15 | case class B( 16 | name: String, 17 | oneA: EntityId, 18 | maybeA: Option[EntityId], 19 | aName: String, 20 | maybeAName: Option[String], 21 | someA: Seq[EntityId], 22 | someAName: Seq[String] 23 | ) 24 | 25 | @BuildEdgeEntity[B, A] 26 | case class BAOne() 27 | @BuildEdgeEntity[B, A] 28 | case class BAMaybe() 29 | @BuildEdgeEntity[B, A] 30 | case class BAName() 31 | @BuildEdgeEntity[B, A] 32 | case class BAMaybeName() 33 | @BuildEdgeEntity[B, A] 34 | case class BASome() 35 | @BuildEdgeEntity[B, A] 36 | case class BASomeName() 37 | 38 | class ASrv extends VertexSrv[A] { 39 | def create(a: A)(implicit graph: Graph, authContext: AuthContext): Try[A with Entity] = createEntity(a) 40 | override def getByName(name: String)(implicit graph: Graph): Traversal.V[A] = startTraversal.has(_.name, name) 41 | } 42 | 43 | class BSrv extends VertexSrv[B] { 44 | def create(b: B)(implicit graph: Graph, authContext: AuthContext): Try[B with Entity] = createEntity(b) 45 | override def getByName(name: String)(implicit graph: Graph): Traversal.V[B] = startTraversal.has(_.name, name) 46 | } 47 | 48 | class MeshSchema extends Schema { 49 | val aSrv = new ASrv 50 | val bSrv = new BSrv 51 | val baOneSrv = new EdgeSrv[BAOne, B, A] 52 | val baMaybeSrv = new EdgeSrv[BAMaybe, B, A] 53 | val baNameSrv = new EdgeSrv[BAName, B, A] 54 | val baMaybeNameSrv = new EdgeSrv[BAMaybeName, B, A] 55 | val baSomeSrv = new EdgeSrv[BASome, B, A] 56 | val baSomeNameSrv = new EdgeSrv[BASomeName, B, A] 57 | 58 | override def modelList: Seq[Model] = Seq(aSrv.model, bSrv.model) 59 | } 60 | 61 | object MeshDatabaseBuilder { 62 | def build(schema: MeshSchema)(implicit db: Database, authContext: AuthContext): Try[Unit] = 63 | db.createSchemaFrom(schema) 64 | .flatMap(_ => db.addSchemaIndexes(schema)) 65 | .flatMap { _ => 66 | db.tryTransaction { implicit graph => 67 | for { 68 | a <- schema.aSrv.create(A("a")) 69 | _ = Thread.sleep(1) 70 | b <- schema.aSrv.create(A("b")) 71 | _ = Thread.sleep(1) 72 | c <- schema.aSrv.create(A("c")) 73 | _ = Thread.sleep(1) 74 | d <- schema.aSrv.create(A("d")) 75 | _ = Thread.sleep(1) 76 | e <- schema.aSrv.create(A("e")) 77 | _ = Thread.sleep(1) 78 | f <- schema.aSrv.create(A("f")) 79 | _ = Thread.sleep(1) 80 | g <- schema.aSrv.create(A("g")) 81 | _ = Thread.sleep(1) 82 | h <- schema.aSrv.create(A("h")) 83 | _ = Thread.sleep(1) 84 | i <- schema.aSrv.create(A("i")) 85 | b1 <- schema.bSrv.create(B("b1", a._id, Some(b._id), "c", Some("d"), Seq(e._id, f._id), Seq("g", "h", "i"))) 86 | _ <- schema.baOneSrv.create(new BAOne, b1, a) 87 | _ <- schema.baMaybeSrv.create(new BAMaybe, b1, b) 88 | _ <- schema.baNameSrv.create(new BAName, b1, c) 89 | _ <- schema.baMaybeNameSrv.create(new BAMaybeName, b1, d) 90 | _ <- schema.baSomeSrv.create(new BASome, b1, e) 91 | _ <- schema.baSomeSrv.create(new BASome, b1, f) 92 | _ <- schema.baSomeNameSrv.create(new BASomeName, b1, g) 93 | _ <- schema.baSomeNameSrv.create(new BASomeName, b1, h) 94 | _ <- schema.baSomeNameSrv.create(new BASomeName, b1, i) 95 | } yield () 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/ModelSamples.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.scalactic.Good 4 | import org.thp.scalligraph.controllers.{FString, FieldsParser} 5 | import play.api.libs.json.{JsString, Reads, Writes} 6 | 7 | object ModelSamples { 8 | 9 | val hobbiesParser: FieldsParser[Seq[String]] = FieldsParser("hobbies") { 10 | case (_, FString(s)) => Good(s.split(",").toSeq) 11 | } 12 | val hobbiesDatabaseReads: Reads[Seq[String]] = Reads[Seq[String]](json => json.validate[String].map(_.split(",").toSeq)) 13 | val hobbiesDatabaseWrites: Writes[Seq[String]] = Writes[Seq[String]](h => JsString(h.mkString(","))) 14 | //val hobbiesDatabaseFormat: Format[Seq[String]] = Format[Seq[String]](hobbiesDatabaseReads, hobbiesDatabaseWrites) 15 | } 16 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/Modern.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import org.apache.tinkerpop.gremlin.process.traversal.P 5 | import org.thp.scalligraph._ 6 | import org.thp.scalligraph.auth.AuthContext 7 | import org.thp.scalligraph.services._ 8 | import org.thp.scalligraph.traversal.TraversalOps._ 9 | import org.thp.scalligraph.traversal.{Converter, Graph, Traversal} 10 | 11 | import scala.util.Try 12 | 13 | @BuildVertexEntity 14 | case class Person(name: String, age: Int) 15 | 16 | object Person { 17 | val initialValues = Seq(Person("marc", 34), Person("franck", 28)) 18 | } 19 | 20 | @BuildVertexEntity 21 | case class Software(name: String, lang: String) 22 | 23 | @BuildEdgeEntity[Person, Person] 24 | case class Knows(weight: Double) 25 | 26 | @BuildEdgeEntity[Person, Software] 27 | case class Created(weight: Double) 28 | 29 | object ModernOps { 30 | implicit class PersonOpsDefs(traversal: Traversal.V[Person]) { 31 | def created: Traversal.V[Software] = traversal.out[Created].v[Software] 32 | def getByName(name: String): Traversal.V[Person] = traversal.has(_.name, name) 33 | def created(predicate: P[Double]): Traversal.V[Software] = 34 | traversal 35 | .outE[Created] 36 | .has(_.weight, predicate) 37 | .inV 38 | .v[Software] 39 | def connectedEdge: Seq[String] = traversal.outE().label.toSeq 40 | def knownLevels: Seq[Double] = traversal.outE[Knows].property("weight", Converter.double).toSeq 41 | def knows: Traversal.V[Person] = traversal.out[Knows].v[Person] 42 | def friends(threshold: Double = 0.8): Traversal.V[Person] = traversal.outE[Knows].has(_.weight, P.gte(threshold)).inV.v[Person] 43 | } 44 | 45 | implicit class SoftwareOpsDefs(traversal: Traversal.V[Software]) { 46 | def createdBy: Traversal.V[Person] = traversal.in("Created").v[Person] 47 | def isRipple: Traversal.V[Software] = traversal.has(_.name, "ripple") 48 | 49 | } 50 | } 51 | 52 | import org.thp.scalligraph.models.ModernOps._ 53 | 54 | @Singleton 55 | class PersonSrv @Inject() extends VertexSrv[Person] { 56 | def create(e: Person)(implicit graph: Graph, authContext: AuthContext): Try[Person with Entity] = createEntity(e) 57 | 58 | override def getByName(name: String)(implicit graph: Graph): Traversal.V[Person] = 59 | startTraversal.getByName(name) 60 | } 61 | 62 | @Singleton 63 | class SoftwareSrv @Inject() extends VertexSrv[Software] { 64 | def create(e: Software)(implicit graph: Graph, authContext: AuthContext): Try[Software with Entity] = createEntity(e) 65 | } 66 | 67 | @Singleton 68 | class ModernSchema @Inject() extends Schema { 69 | val personSrv = new PersonSrv 70 | val softwareSrv = new SoftwareSrv 71 | val knowsSrv = new EdgeSrv[Knows, Person, Person] 72 | val createdSrv = new EdgeSrv[Created, Person, Software] 73 | val vertexServices: Seq[VertexSrv[_]] = Seq(personSrv, softwareSrv) 74 | override def modelList: Seq[Model] = (vertexServices :+ knowsSrv :+ createdSrv).map(_.model) 75 | } 76 | 77 | object ModernDatabaseBuilder { 78 | 79 | def build(schema: ModernSchema)(implicit db: Database, authContext: AuthContext): Try[Unit] = 80 | db.createSchemaFrom(schema) 81 | .flatMap(_ => db.addSchemaIndexes(schema)) 82 | .flatMap { _ => 83 | db.tryTransaction { implicit graph => 84 | for { 85 | vadas <- schema.personSrv.create(Person("vadas", 27)) 86 | marko <- schema.personSrv.create(Person("marko", 29)) 87 | josh <- schema.personSrv.create(Person("josh", 32)) 88 | peter <- schema.personSrv.create(Person("peter", 35)) 89 | lop <- schema.softwareSrv.create(Software("lop", "java")) 90 | ripple <- schema.softwareSrv.create(Software("ripple", "java")) 91 | _ <- schema.knowsSrv.create(Knows(0.5), marko, vadas) 92 | _ <- schema.knowsSrv.create(Knows(1), marko, josh) 93 | _ <- schema.createdSrv.create(Created(0.4), marko, lop) 94 | _ <- schema.createdSrv.create(Created(1), josh, ripple) 95 | _ <- schema.createdSrv.create(Created(0.4), josh, lop) 96 | _ <- schema.createdSrv.create(Created(0.2), peter, lop) 97 | } yield () 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/ModernQuery.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.P 4 | import org.thp.scalligraph.controllers.Renderer 5 | import org.thp.scalligraph.query._ 6 | import org.thp.scalligraph.traversal.Traversal 7 | import org.thp.scalligraph.traversal.TraversalOps._ 8 | import play.api.libs.json.{Json, OWrites} 9 | 10 | case class OutputPerson(createdBy: String, label: String, name: String, age: Int) 11 | 12 | object OutputPerson { 13 | implicit val writes: OWrites[OutputPerson] = Json.writes[OutputPerson] 14 | } 15 | 16 | case class OutputSoftware(createdBy: String, name: String, lang: String) 17 | 18 | object OutputSoftware { 19 | implicit val writes: OWrites[OutputSoftware] = Json.writes[OutputSoftware] 20 | } 21 | 22 | object ModernOutputs { 23 | implicit val personOutput: Renderer[Person with Entity] = 24 | Renderer.toJson[Person with Entity, OutputPerson](person => 25 | new OutputPerson(person._createdBy, s"Mister ${person.name}", person.name, person.age) 26 | ) 27 | implicit val softwareOutput: Renderer[Software with Entity] = 28 | Renderer.toJson[Software with Entity, OutputSoftware](software => new OutputSoftware(software._createdBy, software.name, software.lang)) 29 | } 30 | 31 | case class SeniorAgeThreshold(age: Int) 32 | case class FriendLevel(level: Double) 33 | 34 | class ModernQueryExecutor(implicit val db: Database) extends QueryExecutor { 35 | import ModernOps._ 36 | import ModernOutputs._ 37 | 38 | override val limitedCountThreshold: Long = 1000 39 | val personSrv = new PersonSrv 40 | val softwareSrv = new SoftwareSrv 41 | 42 | override val version: (Int, Int) = 1 -> 1 43 | 44 | override lazy val publicProperties: PublicProperties = { 45 | val labelMapping = SingleMapping[String, String]( 46 | toGraph = { 47 | case d if d startsWith "Mister " => d.drop(7) 48 | case d => d 49 | }, 50 | toDomain = (g: String) => "Mister " + g 51 | ) 52 | PublicPropertyListBuilder[Person] 53 | .property("createdBy", UMapping.string)(_.rename("_createdBy").readonly) 54 | .property("label", labelMapping)(_.rename("name").updatable) 55 | .property("name", UMapping.string)(_.field.updatable) 56 | .property("age", UMapping.int)(_.field.updatable) 57 | .build ++ 58 | PublicPropertyListBuilder[Software] 59 | .property("createdBy", UMapping.string)(_.rename("_createdBy").readonly) 60 | .property("name", UMapping.string)(_.field.updatable) 61 | .property("lang", UMapping.string)(_.field.updatable) 62 | // .property("any", UMapping.string)( 63 | // _.select( 64 | // _.property[String, String]("_createdBy", UMapping.string), 65 | // _.property("name", UMapping.string), 66 | // _.property("lang", UMapping.string) 67 | // ).readonly 68 | // ) 69 | .build 70 | } 71 | 72 | override lazy val queries: Seq[ParamQuery[_]] = Seq( 73 | Query.init[Traversal.V[Person]]("allPeople", (graph, _) => personSrv.startTraversal(graph)), 74 | Query.init[Traversal.V[Software]]("allSoftware", (graph, _) => softwareSrv.startTraversal(graph)), 75 | Query.initWithParam[SeniorAgeThreshold, Traversal.V[Person]]( 76 | "seniorPeople", 77 | (seniorAgeThreshold, graph, _) => personSrv.startTraversal(graph).has(_.age, P.gte(seniorAgeThreshold.age)) 78 | ), 79 | Query[Traversal.V[Person], Traversal.V[Software]]("created", (personSteps, _) => personSteps.created), 80 | Query.withParam[FriendLevel, Traversal.V[Person], Traversal.V[Person]]( 81 | "friends", 82 | (friendLevel, personSteps, _) => personSteps.friends(friendLevel.level) 83 | ), 84 | Query.output[Person with Entity, Traversal.V[Person]], 85 | Query.output[Software with Entity, Traversal.V[Software]] 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/ModernTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.specs2.specification.core.{Fragment, Fragments} 4 | import org.thp.scalligraph.{AppBuilder, EntityName} 5 | import org.thp.scalligraph.auth.{AuthContext, AuthContextImpl} 6 | import org.thp.scalligraph.traversal.TraversalOps._ 7 | import play.api.test.PlaySpecification 8 | 9 | import scala.util.Try 10 | 11 | class ModernTest extends PlaySpecification { 12 | 13 | implicit val authContext: AuthContext = AuthContextImpl("me", "", EntityName(""), "", Set.empty) 14 | 15 | Fragments.foreach(new DatabaseProviders().list) { dbProvider => 16 | val app: AppBuilder = new AppBuilder() 17 | .bindToProvider(dbProvider) 18 | step(setupDatabase(app)) ^ specs(dbProvider.name, app) ^ step(teardownDatabase(app)) 19 | } 20 | 21 | def setupDatabase(app: AppBuilder): Try[Unit] = 22 | ModernDatabaseBuilder.build(app.apply[ModernSchema])(app.apply[Database], authContext) 23 | 24 | def teardownDatabase(app: AppBuilder): Unit = app.apply[Database].drop() 25 | 26 | def specs(name: String, app: AppBuilder): Fragment = { 27 | implicit val db: Database = app.apply[Database] 28 | val personSrv = app.apply[PersonSrv] 29 | 30 | s"[$name] graph" should { 31 | // "remove connected edge when a vertex is removed" in db.transaction { implicit graph => 32 | // // Check that marko is connected to two other people, with known level 0.5 and 1.0 33 | // personSrv.get("marko").knownLevels must contain(exactly(0.5, 1.0)) 34 | // // Remove vadas who is connected to marko 35 | // personSrv.get("vadas").remove() 36 | // // Check that marko is connected to only one person 37 | // personSrv.get("marko").knownLevels must contain(exactly(1.0)) 38 | // } 39 | 40 | "create initial values" in db.roTransaction { implicit graph => 41 | personSrv.startTraversal.toSeq.map(_.name) must contain(exactly("marko", "vadas", "franck", "marc", "josh", "peter")) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/SimpleEntityTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.specs2.specification.core.Fragments 4 | import org.thp.scalligraph.BuildVertexEntity 5 | import org.thp.scalligraph.auth.{AuthContext, UserSrv} 6 | import org.thp.scalligraph.services.VertexSrv 7 | import org.thp.scalligraph.traversal.Graph 8 | import org.thp.scalligraph.traversal.TraversalOps._ 9 | import play.api.libs.logback.LogbackLoggerConfigurator 10 | import play.api.test.PlaySpecification 11 | import play.api.{Configuration, Environment} 12 | 13 | import scala.util.Try 14 | 15 | @BuildVertexEntity 16 | case class MyEntity(name: String, value: Int) 17 | 18 | object MyEntity { 19 | val initialValues: Seq[MyEntity] = Seq(MyEntity("ini1", 1), MyEntity("ini1", 2)) 20 | } 21 | 22 | class MyEntitySrv extends VertexSrv[MyEntity] { 23 | def create(e: MyEntity)(implicit graph: Graph, authContext: AuthContext): Try[MyEntity with Entity] = createEntity(e) 24 | } 25 | 26 | class SimpleEntityTest extends PlaySpecification { 27 | 28 | val userSrv: UserSrv = DummyUserSrv() 29 | implicit val authContext: AuthContext = userSrv.getSystemAuthContext 30 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 31 | 32 | Fragments.foreach(new DatabaseProviders().list) { dbProvider => 33 | val db: Database = dbProvider.get() 34 | db.createSchema(Model.vertex[MyEntity]) 35 | db.addSchemaIndexes(Model.vertex[MyEntity]) 36 | val myEntitySrv: MyEntitySrv = new MyEntitySrv 37 | 38 | s"[${dbProvider.name}] simple entity" should { 39 | "create" in db.transaction { implicit graph => 40 | myEntitySrv.create(MyEntity("The answer", 42)) must beSuccessfulTry.which { createdEntity => 41 | createdEntity._id must_!== null 42 | } 43 | } 44 | 45 | "create and get entities" in db.transaction { implicit graph => 46 | myEntitySrv 47 | .create(MyEntity("e^π", -1)) 48 | .flatMap { createdEntity => 49 | myEntitySrv.getOrFail(createdEntity._id) 50 | } must beSuccessfulTry.which { e: MyEntity with Entity => 51 | e.name must_=== "e^π" 52 | e.value must_=== -1 53 | e._createdBy must_=== "admin" 54 | } 55 | } 56 | 57 | "update an entity" in db.transaction { implicit graph => 58 | myEntitySrv.create(MyEntity("super", 7)) must beASuccessfulTry.which { entity => 59 | myEntitySrv.get(entity).update(_.value, 8).iterate() 60 | myEntitySrv.get(entity).getOrFail("MyEntity") must beSuccessfulTry.which((_: MyEntity with Entity).value must_=== 8) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/models/StreamTransactionTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.Materializer 5 | import akka.stream.scaladsl.{Flow, Keep, Sink, Source} 6 | import play.api.libs.logback.LogbackLoggerConfigurator 7 | import play.api.test.PlaySpecification 8 | import play.api.{Configuration, Environment} 9 | 10 | import scala.concurrent.duration.DurationInt 11 | import scala.util.Try 12 | 13 | class StreamTransactionTest extends PlaySpecification { 14 | val system: ActorSystem = ActorSystem("test") 15 | implicit val mat: Materializer = Materializer(system) 16 | 17 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 18 | class Tx { 19 | var isCommitted = false 20 | var isRollback = false 21 | 22 | def commit(): Unit = 23 | if (isCommitted || isRollback) throw new IllegalStateException("Transaction can't be committed, it is already closed") 24 | else 25 | isCommitted = true 26 | 27 | def rollback(): Unit = 28 | if (isCommitted || isRollback) throw new IllegalStateException("Transaction can't be rolled back, it is already closed") 29 | else isRollback = true 30 | } 31 | 32 | object Tx { 33 | def apply[E, M](flow: Flow[Tx, E, M]): (Tx, Source[E, M]) = { 34 | val tx = new Tx 35 | tx -> TransactionHandler[Tx, E, M](() => tx, _.commit(), _.rollback(), flow) 36 | } 37 | } 38 | 39 | "transactional stream" should { 40 | "commit the transaction if sink consume all elements" in { 41 | val flow = Flow[Tx].mapConcat(_ => 1 to 10) 42 | val (tx, src) = Tx(flow) 43 | await(src.runWith(Sink.seq)) must beEqualTo(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) 44 | tx.isCommitted must beTrue.setMessage(s"$tx is not committed").eventually(5, 10.milliseconds) 45 | } 46 | 47 | "commit the transaction if sink consume part of elements" in { 48 | val flow = Flow[Tx].mapConcat(_ => 1 to 10) 49 | val (tx, src) = Tx(flow) 50 | await(src.runWith(Sink.head)) must beEqualTo(1) 51 | println(s"$tx ${tx.isCommitted}") 52 | tx.isCommitted must beTrue.setMessage(s"$tx is not committed").eventually(5, 10.milliseconds) 53 | } 54 | 55 | "rollback the transaction if flow fails" in { 56 | val flow = Flow[Tx].mapConcat(_ => 1 to 10).map(v => 1 / (v - 3)) 57 | val (tx, src) = Tx(flow) 58 | Try(await(src.runWith(Sink.seq))) must beAFailedTry 59 | tx.isRollback must beTrue.setMessage(s"$tx is not rolled back").eventually(5, 10.milliseconds) 60 | } 61 | 62 | "rollback the transaction if sink fails" in { 63 | val flow = Flow[Tx].mapConcat(_ => 1 to 10) 64 | val (tx, src) = Tx(flow) 65 | Try(await(src.runWith(Flow[Int].map(v => 1 / (v - 3)).toMat(Sink.ignore)(Keep.right)))) must beAFailedTry 66 | tx.isRollback must beTrue.setMessage(s"$tx is not rolled back").eventually(5, 10.milliseconds) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /core-test/src/test/scala/org/thp/scalligraph/services/StorageSrvTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services 2 | 3 | import java.io.InputStream 4 | import java.nio.file.{Files, Path, Paths} 5 | 6 | import akka.actor.ActorSystem 7 | import org.specs2.mock.Mockito 8 | import org.specs2.mutable.Specification 9 | import org.specs2.specification.core.{Fragment, Fragments} 10 | import org.thp.scalligraph.EntityName 11 | import org.thp.scalligraph.auth.{AuthContextImpl, UserSrv} 12 | import org.thp.scalligraph.models.{Database, DatabaseProvider, DatabaseProviders} 13 | import play.api.libs.logback.LogbackLoggerConfigurator 14 | import play.api.{Configuration, Environment} 15 | 16 | import scala.annotation.tailrec 17 | //import org.thp.scalligraph.orientdb.{OrientDatabase, OrientDatabaseStorageSrv} 18 | 19 | class StorageSrvTest extends Specification with Mockito { 20 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 21 | 22 | @tailrec 23 | private def streamCompare(is1: InputStream, is2: InputStream): Boolean = { 24 | val n1 = is1.read() 25 | val n2 = is2.read() 26 | // println(s"$n1 -- $n2") 27 | if (n1 == -1 || n2 == -1) n1 == n2 28 | else (n1 == n2) && streamCompare(is1, is2) 29 | } 30 | 31 | val storageDirectory: Path = Paths.get(s"target/AttachmentTest-${math.random()}") 32 | Files.createDirectory(storageDirectory) 33 | val actorSystem: ActorSystem = ActorSystem("AttachmentTest") 34 | val dbProviders = new DatabaseProviders(actorSystem) 35 | val userSrv: UserSrv = mock[UserSrv] 36 | userSrv.getSystemAuthContext returns AuthContextImpl("test", "Test user", EntityName("test"), "test-request-id", Set.empty) 37 | 38 | val dbProvStorageSrv: Seq[(DatabaseProvider, StorageSrv)] = dbProviders.list.map { db => 39 | db -> new DatabaseStorageSrv(32 * 1024, userSrv, db.get()) 40 | // case db if db.name == "orientdb" => db -> new OrientDatabaseStorageSrv(db.get().asInstanceOf[OrientDatabase], 32 * 1024) 41 | } // :+ (new DatabaseProvider("janus", new JanusDatabase(actorSystem)) -> new LocalFileSystemStorageSrv(storageDirectory)) 42 | 43 | Fragments.foreach(dbProvStorageSrv) { 44 | case (dbProvider, storageSrv) => 45 | val db = dbProvider.get() 46 | step(db.createSchema(Nil)) ^ specs(dbProvider.name, db, storageSrv) ^ step(db.drop()) 47 | } 48 | 49 | def specs(dbName: String, db: Database, storageSrv: StorageSrv): Fragment = 50 | s"[$dbName] attachment" should { 51 | 52 | "save and read stored data" in { 53 | val f1 = Paths.get("../build.sbt") 54 | lazy val f2 = Paths.get("build.sbt") 55 | val filePath = if (Files.exists(f1)) f1 else f2 56 | val is = Files.newInputStream(filePath) 57 | val fId = "build.sbt-custom-id" 58 | db.tryTransaction { implicit graph => 59 | storageSrv.saveBinary("test", fId, is) 60 | } must beSuccessfulTry(()) 61 | is.close() 62 | 63 | val is1 = storageSrv.loadBinary("test", fId) 64 | val is2 = Files.newInputStream(filePath) 65 | try { 66 | streamCompare(is1, is2) must beTrue 67 | storageSrv.exists("test", fId) must beTrue 68 | } finally { 69 | is1.close() 70 | is2.close() 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /core/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | ${application.home:-.}/logs/application.log 9 | 10 | %date [%level] from %logger in %thread - %message%n%xException 11 | 12 | 13 | 14 | 15 | 16 | %coloredLevel %logger{15} - %message%n%xException{10} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /core/src/main/resources/play/reference-overrides.conf: -------------------------------------------------------------------------------- 1 | play.http.errorHandler = org.thp.scalligraph.ErrorHandler 2 | play.application.loader = org.thp.scalligraph.ScalligraphApplicationLoader 3 | play.http.filters = org.thp.scalligraph.Filters 4 | 5 | akka { 6 | cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" 7 | extensions = ["akka.cluster.pubsub.DistributedPubSub"] 8 | actor { 9 | default-dispatcher.type = "org.thp.scalligraph.ContextPropagatingDispatcherConfigurator" 10 | provider = "cluster" 11 | serializers { 12 | config = "org.thp.scalligraph.services.config.ConfigSerializer" 13 | } 14 | 15 | serialization-bindings { 16 | "org.thp.scalligraph.services.config.ConfigMessage" = config 17 | } 18 | } 19 | remote.artery { 20 | canonical { 21 | hostname = "127.0.0.1" 22 | port = 0 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | ignoreDatabaseConfiguration = false 2 | 3 | storage { 4 | # provider = 5 | localfs.location = /tmp/scalligraph 6 | database.chunkSize = 32k 7 | s3 { 8 | bucket = "bucket" 9 | readTimeout = 1 minute 10 | writeTimeout = 1 minute 11 | chunkSize = 1 MB 12 | # endPoint = "" 13 | # accessKey = "" 14 | # secretKey = "" 15 | region = "us-east-1" 16 | } 17 | } 18 | session { 19 | timeout = 1h 20 | warning = 5m 21 | username = username 22 | } 23 | auth { 24 | organisationHeader = "X-Organisation" 25 | organisationCookie = "THEHIVE_ORGANISATION" 26 | defaults { 27 | ad { 28 | # dnsDomain = 29 | # winDomain = 30 | # hosts = 31 | useSSL = false 32 | } 33 | header { 34 | userHeader = "X-USERID" 35 | } 36 | ldap { 37 | # bindDN = 38 | # bindPW = 39 | # baseDN = 40 | # filter = 41 | # hosts = 42 | useSSL = false 43 | } 44 | pki { 45 | certificateField = cn 46 | } 47 | session { 48 | timeout = 1 hour 49 | warning = 5 minutes 50 | } 51 | 52 | oauth2 { 53 | #clientId = 54 | #clientSecret = 55 | #redirectUri = 56 | #responseType = 57 | #grantType = 58 | #authorizationUrl = 59 | #tokenUrl = 60 | #userUrl = 61 | #scope = [] 62 | #userIdField = 63 | #userOrganisationField = 64 | #defaultOrganisation = 65 | #authorizationHeader = 66 | } 67 | # Single-Sign On 68 | sso { 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/AccessLogFilter.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import akka.stream.Materializer 4 | import javax.inject.Inject 5 | import play.api.Logger 6 | import play.api.http.{DefaultHttpFilters, EnabledFilters} 7 | import play.api.mvc._ 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | class AccessLogFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext, errorHandler: ErrorHandler) extends EssentialFilter { 12 | 13 | val logger: Logger = Logger(getClass) 14 | 15 | override def apply(next: EssentialAction): EssentialAction = 16 | (requestHeader: RequestHeader) => { 17 | val startTime = System.currentTimeMillis 18 | DiagnosticContext 19 | .withRequest(requestHeader)(next(requestHeader)) 20 | .recoverWith { case error => errorHandler.onServerError(requestHeader, error) } 21 | .map { result => 22 | DiagnosticContext.withRequest(requestHeader) { 23 | val endTime = System.currentTimeMillis 24 | val requestTime = endTime - startTime 25 | 26 | logger.info( 27 | s"${requestHeader.remoteAddress} ${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status} ${result 28 | .body 29 | .contentLength 30 | .fold("")(_ + " bytes")}" 31 | ) 32 | 33 | result.withHeaders("Request-Time" -> requestTime.toString) 34 | } 35 | } 36 | } 37 | } 38 | 39 | class Filters @Inject() (enabledFilters: EnabledFilters, accessLogFilter: AccessLogFilter) 40 | extends DefaultHttpFilters(enabledFilters.filters :+ accessLogFilter: _*) 41 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/Annotations.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import org.thp.scalligraph.`macro`.AnnotationMacro 4 | 5 | import scala.annotation.{compileTimeOnly, StaticAnnotation} 6 | import scala.language.experimental.macros 7 | 8 | @compileTimeOnly("enable macro paradise to expand macro annotations") 9 | class BuildVertexEntity extends StaticAnnotation { 10 | 11 | def macroTransform(annottees: Any*): Any = 12 | macro AnnotationMacro.buildVertexModel 13 | } 14 | 15 | @compileTimeOnly("enable macro paradise to expand macro annotations") 16 | class BuildEdgeEntity[FROM <: Product, TO <: Product] extends StaticAnnotation { 17 | 18 | def macroTransform(annottees: Any*): Any = 19 | macro AnnotationMacro.buildEdgeModel 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/ContextPropagatingDisptacher.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import java.util.concurrent.TimeUnit 4 | import java.util.{Map => JMap} 5 | import akka.dispatch._ 6 | import com.typesafe.config.Config 7 | import org.slf4j.MDC 8 | import org.thp.scalligraph.auth.AuthContext 9 | import play.api.mvc.RequestHeader 10 | 11 | import scala.concurrent.ExecutionContext 12 | import scala.concurrent.duration.{Duration, FiniteDuration} 13 | 14 | /** 15 | * Configurator for a context propagating dispatcher. 16 | */ 17 | class ContextPropagatingDispatcherConfigurator(config: Config, prerequisites: DispatcherPrerequisites) 18 | extends MessageDispatcherConfigurator(config, prerequisites) { 19 | 20 | private val instance = new ContextPropagatingDispatcher( 21 | this, 22 | config.getString("id"), 23 | config.getInt("throughput"), 24 | FiniteDuration(config.getDuration("throughput-deadline-time", TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS), 25 | configureExecutor(), 26 | FiniteDuration(config.getDuration("shutdown-timeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) 27 | ) 28 | 29 | override def dispatcher(): MessageDispatcher = instance 30 | } 31 | 32 | /** 33 | * A context propagating dispatcher. 34 | * 35 | * This dispatcher propagates the current diagnostic context if it's set when it's executed. 36 | */ 37 | class ContextPropagatingDispatcher( 38 | _configurator: MessageDispatcherConfigurator, 39 | id: String, 40 | throughput: Int, 41 | throughputDeadlineTime: Duration, 42 | executorServiceFactoryProvider: ExecutorServiceFactoryProvider, 43 | shutdownTimeout: FiniteDuration 44 | ) extends Dispatcher( 45 | _configurator, 46 | id, 47 | throughput, 48 | throughputDeadlineTime, 49 | executorServiceFactoryProvider, 50 | shutdownTimeout 51 | ) { self => 52 | 53 | override def prepare(): ExecutionContext = 54 | new ExecutionContext { 55 | // capture the context 56 | val context: CapturedDiagnosticContext = DiagnosticContext.capture() 57 | def execute(r: Runnable): Unit = self.execute(() => context.withContext(r.run())) 58 | def reportFailure(t: Throwable): Unit = self.reportFailure(t) 59 | } 60 | } 61 | 62 | /** 63 | * The current diagnostic context. 64 | */ 65 | object DiagnosticContext { 66 | 67 | /** 68 | * Capture the current diagnostic context. 69 | */ 70 | def capture(): CapturedDiagnosticContext = 71 | new CapturedDiagnosticContext { 72 | val maybeMDC: Option[JMap[String, String]] = getDiagnosticContext 73 | 74 | def withContext[T](block: => T): T = 75 | maybeMDC match { 76 | case Some(mdc) => withDiagnosticContext(mdc)(block) 77 | case None => block 78 | } 79 | } 80 | 81 | /** 82 | * Get the current diagnostic context. 83 | */ 84 | def getDiagnosticContext: Option[JMap[String, String]] = Option(MDC.getCopyOfContextMap) 85 | 86 | /** 87 | * Execute the given block with the given diagnostic context. 88 | */ 89 | def withDiagnosticContext[T](mdc: JMap[String, String])(block: => T): T = { 90 | assert(mdc != null, "MDC must not be null") 91 | saveDiagnosticContext { 92 | MDC.setContextMap(mdc) 93 | block 94 | } 95 | } 96 | 97 | def withRequest[T](requestHeader: RequestHeader)(block: => T): T = { 98 | assert(requestHeader != null, "RequestHeader must not be null") 99 | saveDiagnosticContext { 100 | requestHeader match { 101 | case authContext: AuthContext => 102 | MDC.put("userId", authContext.userId) 103 | MDC.put("organisation", authContext.organisation.toString) 104 | case _ => () 105 | } 106 | MDC.put("request", f"${requestHeader.id}%08x") 107 | MDC.remove("tx") 108 | block 109 | } 110 | } 111 | 112 | def saveDiagnosticContext[T](block: => T): T = { 113 | val maybeOld = getDiagnosticContext 114 | try block 115 | finally maybeOld match { 116 | case Some(old) => MDC.setContextMap(old) 117 | case None => MDC.clear() 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * A captured context 124 | */ 125 | trait CapturedDiagnosticContext { 126 | 127 | /** 128 | * Execute the given block with the captured context. 129 | */ 130 | def withContext[T](block: => T): T 131 | } 132 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/EntityId.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import org.thp.scalligraph.controllers.FieldsParser 4 | import play.api.libs.json.{Format, Reads, Writes} 5 | 6 | sealed abstract class EntityIdOrName(val value: String) { 7 | def fold[A](ifIsId: EntityId => A, ifIsName: String => A): A 8 | } 9 | object EntityIdOrName { 10 | val prefixChar: Char = '~' 11 | def isId(value: String): Boolean = value.nonEmpty && value.charAt(0) == prefixChar 12 | def fold[A](value: String)(ifIsId: String => A, ifIsName: String => A): A = if (isId(value)) ifIsId(value.substring(1)) else ifIsName(value) 13 | def apply(value: String): EntityIdOrName = fold(value)(new EntityId(_), new EntityName(_)) 14 | implicit val fieldsParser: FieldsParser[EntityIdOrName] = FieldsParser.string.on("idOrName").map("EntityIdOrName")(EntityIdOrName.apply) 15 | } 16 | 17 | case class EntityId(override val value: String) extends EntityIdOrName(value) { 18 | override def fold[A](ifIsId: EntityId => A, ifIsName: String => A): A = ifIsId(this) 19 | override def toString: String = s"${EntityIdOrName.prefixChar}$value" 20 | def isDefined: Boolean = value.nonEmpty 21 | def isEmpty: Boolean = value.isEmpty 22 | def toOption: Option[EntityId] = if (isDefined) Some(this) else None 23 | } 24 | object EntityId { 25 | def apply(id: AnyRef): EntityId = read(id.toString) 26 | def read(id: String): EntityId = EntityIdOrName.fold(id)(new EntityId(_), new EntityId(_)) 27 | def empty = EntityId("") 28 | implicit val format: Format[EntityId] = Format(Reads.StringReads.map(EntityId.read), Writes.StringWrites.contramap(_.toString)) 29 | implicit val fieldsParser: FieldsParser[EntityId] = FieldsParser.string.map("EntityId")(EntityId.read) 30 | } 31 | 32 | case class EntityName(override val value: String) extends EntityIdOrName(value) { 33 | override def fold[A](ifIsId: EntityId => A, ifIsName: String => A): A = ifIsName(value) 34 | override def toString: String = value 35 | } 36 | object EntityName { 37 | implicit val format: Format[EntityName] = Format(Reads.StringReads.map(new EntityName(_)), Writes.StringWrites.contramap(_.toString)) 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/ErrorHandler.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import play.api.Logger 4 | import play.api.http.Status.{BAD_REQUEST, FORBIDDEN, NOT_FOUND} 5 | import play.api.http.{HttpErrorHandler, Status, Writeable} 6 | import play.api.libs.json.{JsObject, JsString, Json} 7 | import play.api.mvc.{RequestHeader, ResponseHeader, Result} 8 | 9 | import scala.concurrent.Future 10 | 11 | /** 12 | * This class handles errors. It traverses all causes of exception to find known error and shows the appropriate message 13 | */ 14 | class ErrorHandler extends HttpErrorHandler { 15 | lazy val logger: Logger = Logger(getClass) 16 | 17 | def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = { 18 | val tpe = statusCode match { 19 | case BAD_REQUEST => "BadRequest" 20 | case FORBIDDEN => "Forbidden" 21 | case NOT_FOUND => "NotFound" 22 | case _ => "Unknown" 23 | } 24 | Future.successful(toResult(statusCode, Json.obj("type" -> tpe, "message" -> message))) 25 | } 26 | 27 | def toErrorResult(ex: Throwable): (Int, JsObject) = 28 | ex match { 29 | case e: AuthenticationError => Status.UNAUTHORIZED -> e.toJson 30 | case e: AuthorizationError => Status.FORBIDDEN -> e.toJson 31 | case e: MultiFactorCodeRequired => Status.PAYMENT_REQUIRED -> e.toJson 32 | case e: CreateError => Status.BAD_REQUEST -> e.toJson 33 | case e: GetError => Status.INTERNAL_SERVER_ERROR -> e.toJson 34 | case e: SearchError => Status.BAD_REQUEST -> e.toJson 35 | case e: UpdateError => Status.INTERNAL_SERVER_ERROR -> e.toJson 36 | case e: NotFoundError => Status.NOT_FOUND -> e.toJson 37 | case e: BadRequestError => Status.BAD_REQUEST -> e.toJson 38 | case e: MultiError => Status.MULTI_STATUS -> e.toJson 39 | case e: AttributeCheckingError => Status.BAD_REQUEST -> e.toJson 40 | case e: InternalError => Status.INTERNAL_SERVER_ERROR -> e.toJson 41 | case e: BadConfigurationError => Status.BAD_REQUEST -> e.toJson 42 | case nfe: NumberFormatException => 43 | Status.BAD_REQUEST -> Json.obj("type" -> "NumberFormatException", "message" -> ("Invalid format " + nfe.getMessage)) 44 | case iae: IllegalArgumentException => Status.BAD_REQUEST -> Json.obj("type" -> "IllegalArgument", "message" -> iae.getMessage) 45 | case _ if Option(ex.getCause).isDefined => toErrorResult(ex.getCause) 46 | case _ => 47 | logger.error("Internal error", ex) 48 | val json = Json.obj("type" -> ex.getClass.getName, "message" -> ex.getMessage) 49 | Status.INTERNAL_SERVER_ERROR -> (if (ex.getCause == null) json else json + ("cause" -> JsString(ex.getCause.getMessage))) 50 | } 51 | 52 | def toResult[C](status: Int, c: C)(implicit writeable: Writeable[C]): Result = 53 | Result(header = ResponseHeader(status), body = writeable.toEntity(c)) 54 | 55 | def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = { 56 | val (status, body) = toErrorResult(exception) 57 | if (!exception.isInstanceOf[AuthenticationError]) 58 | if (logger.isDebugEnabled) logger.warn(s"${request.method} ${request.uri} returned $status", exception) 59 | else logger.warn(s"${request.method} ${request.uri} returned $status") 60 | Future.successful(toResult(status, body)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/ScalligraphRouter.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import com.google.inject.Provider 4 | import javax.inject.{Inject, Singleton} 5 | import org.thp.scalligraph.auth.AuthSrv 6 | import org.thp.scalligraph.controllers.{AuthenticatedRequest, Entrypoint} 7 | import org.thp.scalligraph.query.{Query, QueryExecutor} 8 | import play.api.Logger 9 | import play.api.cache.AsyncCacheApi 10 | import play.api.http.HttpConfiguration 11 | import play.api.mvc._ 12 | import play.api.routing.Router.Routes 13 | import play.api.routing.sird._ 14 | import play.api.routing.{Router, SimpleRouter} 15 | 16 | import scala.collection.immutable 17 | import scala.concurrent.{ExecutionContext, Future} 18 | 19 | object DebugRouter { 20 | lazy val logger: Logger = Logger(getClass) 21 | 22 | def apply(name: String, router: Router): Router = new Router { 23 | override def routes: Routes = new Routes { 24 | override def isDefinedAt(x: RequestHeader): Boolean = { 25 | val result = router.routes.isDefinedAt(x) 26 | logger.info(s"ROUTER $name $x => $result") 27 | result 28 | } 29 | override def apply(v1: RequestHeader): Handler = router.routes.apply(v1) 30 | } 31 | override def documentation: Seq[(String, String, String)] = router.documentation 32 | override def withPrefix(prefix: String): Router = DebugRouter(s"$name.in($prefix)", router.withPrefix(prefix)) 33 | override def toString: String = s"router($name)@$hashCode" 34 | } 35 | } 36 | 37 | @Singleton 38 | class GlobalQueryExecutor @Inject() (queryExecutors: immutable.Set[QueryExecutor], cache: AsyncCacheApi) { 39 | 40 | def get(version: Int): QueryExecutor = 41 | cache.sync.getOrElseUpdate(s"QueryExecutor.$version") { 42 | queryExecutors 43 | .filter(_.versionCheck(version)) 44 | .reduceOption(_ ++ _) 45 | .getOrElse(throw BadRequestError(s"No available query executor for version $version")) 46 | } 47 | 48 | def get: QueryExecutor = queryExecutors.reduce(_ ++ _) 49 | } 50 | 51 | @Singleton 52 | class ScalligraphRouter @Inject() ( 53 | httpConfig: HttpConfiguration, 54 | routers: immutable.Set[Router], 55 | entrypoint: Entrypoint, 56 | globalQueryExecutor: GlobalQueryExecutor, 57 | actionBuilder: DefaultActionBuilder, 58 | authSrv: AuthSrv, 59 | implicit val ec: ExecutionContext 60 | ) extends Provider[Router] { 61 | lazy val logger: Logger = Logger(getClass) 62 | lazy val routerList: List[Router] = routers.toList 63 | val prefix: String = httpConfig.context 64 | override lazy val get: Router = { 65 | 66 | routerList 67 | .reduceOption(_ orElse _) 68 | .getOrElse(Router.empty) 69 | .orElse(SimpleRouter(queryRoutes)) 70 | .orElse(SimpleRouter(authRoutes)) 71 | .withPrefix(prefix) 72 | } 73 | 74 | val queryRoutes: Routes = { 75 | case POST(p"/api/v${int(version)}/query") => 76 | val queryExecutor = globalQueryExecutor.get(version) 77 | entrypoint("query") 78 | .extract("query", queryExecutor.parser.on("query")) 79 | .auth { request => 80 | // macro can't be used because it is in the same module 81 | // val query: Query = request.body("query" 82 | val query: Query = request.body.list.head 83 | queryExecutor.execute(query, request) 84 | } 85 | } 86 | 87 | val defaultAction: ActionFunction[Request, AuthenticatedRequest] = new ActionFunction[Request, AuthenticatedRequest] { 88 | override def invokeBlock[A](request: Request[A], block: AuthenticatedRequest[A] => Future[Result]): Future[Result] = 89 | Future.failed(NotFoundError(request.path)) 90 | override protected def executionContext: ExecutionContext = ec 91 | } 92 | 93 | val authRoutes: Routes = { 94 | case _ => 95 | actionBuilder.async { request => 96 | authSrv 97 | .actionFunction(defaultAction) 98 | .invokeBlock( 99 | request, 100 | (_: AuthenticatedRequest[AnyContent]) => 101 | if (request.path.endsWith("/ssoLogin")) 102 | Future.successful(Results.Redirect(prefix)) 103 | else 104 | Future.failed(NotFoundError(request.path)) 105 | ) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/SingleInstance.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | class SingleInstance(val value: Boolean) { 4 | override lazy val toString: String = if (value) "single node" else "cluster" 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/AuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import org.thp.scalligraph.controllers.AuthenticatedRequest 4 | import org.thp.scalligraph.{BadConfigurationError, EntityIdOrName, NotSupportedError} 5 | import play.api.mvc.{ActionFunction, Request, RequestHeader, Result} 6 | import play.api.{ConfigLoader, Configuration} 7 | 8 | import javax.inject.{Inject, Singleton} 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.util.matching.Regex 11 | import scala.util.{Failure, Success, Try} 12 | 13 | object AuthCapability extends Enumeration { 14 | val changePassword, setPassword, authByKey, sso, mfa = Value 15 | } 16 | 17 | @Singleton 18 | class RequestOrganisation(header: Option[String], parameter: Option[String], pathSegment: Option[Regex], cookie: Option[String]) 19 | extends (Request[_] => Option[EntityIdOrName]) { 20 | @Inject() def this(configuration: Configuration) = 21 | this( 22 | configuration.getOptional[String]("auth.organisationHeader"), 23 | configuration.getOptional[String]("auth.organisationParameter"), 24 | configuration.getOptional[String]("auth.organisationPathExtractor").map(_.r), 25 | configuration.getOptional[String]("auth.organisationCookieName") 26 | ) 27 | override def apply(request: Request[_]): Option[EntityIdOrName] = 28 | (header.flatMap(request.headers.get(_)) orElse 29 | parameter.flatMap(request.queryString.getOrElse(_, Nil).headOption) orElse 30 | pathSegment.flatMap(r => r.findFirstMatchIn(request.path).flatMap(m => Option(m.group(0)))) orElse 31 | cookie.flatMap(request.cookies.get).map(_.value)).map(EntityIdOrName.apply) 32 | } 33 | 34 | trait AuthSrvProvider extends (Configuration => Try[AuthSrv]) { 35 | val name: String 36 | implicit class RichConfig(configuration: Configuration) { 37 | 38 | def getOrFail[A: ConfigLoader](path: String): Try[A] = 39 | configuration 40 | .getOptional[A](path) 41 | .fold[Try[A]](Failure(BadConfigurationError(s"Configuration $path is missing")))(Success(_)) 42 | } 43 | } 44 | 45 | trait AuthSrv { 46 | val name: String 47 | def capabilities = Set.empty[AuthCapability.Value] 48 | 49 | def actionFunction(nextFunction: ActionFunction[Request, AuthenticatedRequest]): ActionFunction[Request, AuthenticatedRequest] = 50 | nextFunction 51 | 52 | def authenticate(username: String, password: String, organisation: Option[EntityIdOrName], code: Option[String])(implicit 53 | request: RequestHeader 54 | ): Try[AuthContext] = 55 | Failure(NotSupportedError()) 56 | 57 | def authenticate(key: String, organisation: Option[EntityIdOrName])(implicit request: RequestHeader): Try[AuthContext] = 58 | Failure(NotSupportedError()) 59 | 60 | def setSessionUser(authContext: AuthContext): Result => Result = identity 61 | 62 | def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Try[Unit] = 63 | Failure(NotSupportedError()) 64 | 65 | def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Try[Unit] = 66 | Failure(NotSupportedError()) 67 | 68 | def renewKey(username: String)(implicit authContext: AuthContext): Try[String] = 69 | Failure(NotSupportedError()) 70 | 71 | def getKey(username: String)(implicit authContext: AuthContext): Try[String] = 72 | Failure(NotSupportedError()) 73 | 74 | def removeKey(username: String)(implicit authContext: AuthContext): Try[Unit] = 75 | Failure(NotSupportedError()) 76 | } 77 | 78 | trait AuthSrvWithActionFunction extends AuthSrv { 79 | 80 | protected def ec: ExecutionContext 81 | 82 | def getAuthContext[A](request: Request[A]): Option[AuthContext] 83 | 84 | def transformResult[A](request: Request[A], authContext: AuthContext): Result => Result = identity 85 | 86 | override def actionFunction(nextFunction: ActionFunction[Request, AuthenticatedRequest]): ActionFunction[Request, AuthenticatedRequest] = 87 | new ActionFunction[Request, AuthenticatedRequest] { 88 | override def invokeBlock[A](request: Request[A], block: AuthenticatedRequest[A] => Future[Result]): Future[Result] = 89 | getAuthContext(request) 90 | .fold(nextFunction.invokeBlock(request, block)) { authContext => 91 | block(new AuthenticatedRequest(authContext, request)) 92 | .map(transformResult(request, authContext))(ec) 93 | } 94 | 95 | override protected def executionContext: ExecutionContext = ec 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/BasicAuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import java.util.Base64 4 | 5 | import javax.inject.{Inject, Provider, Singleton} 6 | import org.thp.scalligraph.AuthenticationError 7 | import org.thp.scalligraph.controllers.AuthenticatedRequest 8 | import play.api.Configuration 9 | import play.api.http.{HeaderNames, Status} 10 | import play.api.mvc.{ActionFunction, Request, Result, Results} 11 | 12 | import scala.concurrent.{ExecutionContext, Future} 13 | import scala.util.{Failure, Success, Try} 14 | 15 | class BasicAuthSrv(realm: Option[String], authSrv: AuthSrv, requestOrganisation: RequestOrganisation, implicit val ec: ExecutionContext) 16 | extends AuthSrv { 17 | 18 | private val authHeader = realm.map(r => "WWW-Authenticate" -> s"""Basic realm="$r"""") 19 | 20 | override val name: String = "basic" 21 | 22 | def getAuthContext[A](request: Request[A]): Option[AuthContext] = 23 | request 24 | .headers 25 | .get(HeaderNames.AUTHORIZATION) 26 | .collect { 27 | case h if h.startsWith("Basic ") => 28 | val authWithoutBasic = h.substring(6) 29 | val decodedAuth = new String(Base64.getDecoder.decode(authWithoutBasic), "UTF-8") 30 | decodedAuth.split(":") 31 | } 32 | .flatMap { 33 | case Array(username, password) => 34 | authSrv.authenticate(username, password, requestOrganisation(request), None)(request).toOption 35 | case Array(username, password, code) => 36 | authSrv.authenticate(username, password, requestOrganisation(request), Some(code))(request).toOption 37 | case _ => None 38 | } 39 | 40 | def addAuthenticateHeader[A](request: Request[A], result: Future[Result]): Future[Result] = 41 | authHeader match { 42 | case Some(h) if !request.headers.hasHeader("Referer") => 43 | result.transform { 44 | case Success(result) if result.header.status == Status.UNAUTHORIZED => Success(result.withHeaders(h)) 45 | case Failure(error: AuthenticationError) => Success(Results.Unauthorized(error.toJson).withHeaders(h)) 46 | case other => other 47 | } 48 | case _ => result 49 | } 50 | 51 | override def actionFunction(nextFunction: ActionFunction[Request, AuthenticatedRequest]): ActionFunction[Request, AuthenticatedRequest] = 52 | new ActionFunction[Request, AuthenticatedRequest] { 53 | override def invokeBlock[A](request: Request[A], block: AuthenticatedRequest[A] => Future[Result]): Future[Result] = 54 | getAuthContext(request).fold(addAuthenticateHeader(request, nextFunction.invokeBlock(request, block))) { authContext => 55 | block(new AuthenticatedRequest(authContext, request)) 56 | } 57 | 58 | override protected def executionContext: ExecutionContext = ec 59 | } 60 | } 61 | 62 | @Singleton 63 | class BasicAuthProvider @Inject() (authSrvProvider: Provider[AuthSrv], requestOrganisation: RequestOrganisation, ec: ExecutionContext) 64 | extends AuthSrvProvider { 65 | lazy val authSrv: AuthSrv = authSrvProvider.get 66 | override val name: String = "basic" 67 | override def apply(config: Configuration): Try[AuthSrv] = { 68 | val realm = config.getOptional[String]("realm") 69 | Success(new BasicAuthSrv(realm, authSrv, requestOrganisation, ec)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/HeaderAuthenticateSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import javax.inject.Inject 4 | import org.thp.scalligraph.controllers.AuthenticatedRequest 5 | import play.api.Configuration 6 | import play.api.mvc.{ActionFunction, Cookie, Request, Result} 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | import scala.util.Try 10 | 11 | class HeaderAuthSrv( 12 | userHeader: String, 13 | requestOrganisation: RequestOrganisation, 14 | organisationCookie: Option[String], 15 | userSrv: UserSrv, 16 | ec: ExecutionContext 17 | ) extends AuthSrv { 18 | override val name: String = "header" 19 | override def actionFunction(nextFunction: ActionFunction[Request, AuthenticatedRequest]): ActionFunction[Request, AuthenticatedRequest] = 20 | new ActionFunction[Request, AuthenticatedRequest] { 21 | override def invokeBlock[A](request: Request[A], block: AuthenticatedRequest[A] => Future[Result]): Future[Result] = 22 | request 23 | .headers 24 | .get(userHeader) 25 | .flatMap(userSrv.getAuthContext(request, _, requestOrganisation(request)).toOption) 26 | .fold(nextFunction.invokeBlock(request, block)) { authContext => 27 | block(new AuthenticatedRequest[A](authContext, request)) 28 | .map { result => 29 | result 30 | .header 31 | .headers 32 | .get("X-Organisation") 33 | .fold(result) { organisation => 34 | organisationCookie.fold(result) { cookieName => 35 | result.withCookies(Cookie(cookieName, organisation, httpOnly = false)) 36 | } 37 | } 38 | }(ec) 39 | } 40 | override protected def executionContext: ExecutionContext = ec 41 | } 42 | } 43 | 44 | class HeaderAuthProvider @Inject() (configuration: Configuration, requestOrganisation: RequestOrganisation, userSrv: UserSrv, ec: ExecutionContext) 45 | extends AuthSrvProvider { 46 | override val name: String = "header" 47 | override def apply(config: Configuration): Try[AuthSrv] = 48 | for { 49 | userHeader <- config.getOrFail[String]("userHeader") 50 | } yield new HeaderAuthSrv(userHeader, requestOrganisation, configuration.getOptional[String]("auth.organisationCookieName"), userSrv, ec) 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/KeyAuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import javax.inject.Inject 4 | import play.api.Configuration 5 | import play.api.http.HeaderNames 6 | import play.api.mvc.Request 7 | 8 | import scala.concurrent.ExecutionContext 9 | import scala.util.{Success, Try} 10 | 11 | class KeyAuthSrv(authSrv: AuthSrv, requestOrganisation: RequestOrganisation, val ec: ExecutionContext) extends AuthSrvWithActionFunction { 12 | override val name: String = "key" 13 | 14 | override def getAuthContext[A](request: Request[A]): Option[AuthContext] = 15 | request 16 | .headers 17 | .get(HeaderNames.AUTHORIZATION) 18 | .collect { 19 | case h if h.startsWith("Bearer ") => h.substring(7) 20 | } 21 | .flatMap(key => authSrv.authenticate(key, requestOrganisation(request))(request).toOption) 22 | } 23 | 24 | class KeyAuthProvider @Inject() (authSrv: AuthSrv, requestOrganisation: RequestOrganisation, ec: ExecutionContext) extends AuthSrvProvider { 25 | override val name: String = "key" 26 | override def apply(config: Configuration): Try[AuthSrv] = Success(new KeyAuthSrv(authSrv, requestOrganisation, ec)) 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/Permission.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import play.api.Logger 4 | 5 | import scala.language.implicitConversions 6 | 7 | trait PermissionTag 8 | 9 | object Permission { 10 | def apply(name: String): Permission = shapeless.tag[PermissionTag][String](name) 11 | def apply(names: Set[String]): Set[Permission] = names.map(apply) 12 | } 13 | 14 | case class PermissionDesc(name: String, label: String, scope: String*) { 15 | val permission: Permission = Permission(name) 16 | } 17 | 18 | object PermissionDesc { 19 | implicit def PermissionDescToPermission(pd: PermissionDesc): Permission = pd.permission 20 | } 21 | 22 | trait Permissions { 23 | lazy val logger: Logger = Logger(getClass) 24 | 25 | val defaultScopes: Seq[String] 26 | val list: Set[PermissionDesc] 27 | 28 | lazy val all: Set[Permission] = list.map(_.permission) 29 | 30 | def forScope(scope: String): Set[Permission] = list.collect { 31 | case p if p.scope.contains(scope) => p.permission 32 | } 33 | 34 | def desc(permission: Permission): PermissionDesc = list.find(_.permission == permission).getOrElse { 35 | logger.error(s"Unknown permission: $permission") 36 | PermissionDesc(permission, permission, defaultScopes: _*) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/PkiAuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.security.cert.X509Certificate 5 | import java.util.{List => JList} 6 | 7 | import javax.inject.{Inject, Singleton} 8 | import javax.naming.ldap.LdapName 9 | import org.bouncycastle.asn1._ 10 | import play.api.Configuration 11 | import play.api.mvc.Request 12 | 13 | import scala.collection.JavaConverters._ 14 | import scala.concurrent.ExecutionContext 15 | import scala.util.Try 16 | 17 | class PkiAuthSrv(certificateField: String, requestOrganisation: RequestOrganisation, userSrv: UserSrv, val ec: ExecutionContext) 18 | extends AuthSrvWithActionFunction { 19 | override val name: String = "pki" 20 | 21 | @scala.annotation.tailrec 22 | final def asn1String(obj: ASN1Primitive): String = obj match { 23 | case ds: DERUTF8String => DERUTF8String.getInstance(ds).getString 24 | case to: ASN1TaggedObject => asn1String(ASN1TaggedObject.getInstance(to).getObject) 25 | case os: ASN1OctetString => new String(os.getOctets) 26 | case as: ASN1String => as.getString 27 | } 28 | 29 | object CertificateSAN { 30 | 31 | def unapply(l: JList[_]): Option[(String, String)] = { 32 | val typeValue = for { 33 | t <- Option(l.get(0)) 34 | v <- Option(l.get(1)) 35 | } yield t -> v 36 | typeValue 37 | .collect { case (t: Integer, v) => t.toInt -> v } 38 | .collect { 39 | case (0, value: Array[Byte]) => 40 | val asn1 = new ASN1InputStream(new ByteArrayInputStream(value)).readObject() 41 | val asn1Seq = ASN1Sequence.getInstance(asn1) 42 | val id = ASN1ObjectIdentifier.getInstance(asn1Seq.getObjectAt(0)).getId 43 | val valueStr = asn1String(asn1Seq.getObjectAt(1).toASN1Primitive) 44 | 45 | id match { 46 | case "1.3.6.1.4.1.311.20.2.3" => "upn" -> valueStr 47 | // Add other object id 48 | case other => other -> valueStr 49 | } 50 | case (1, value: String) => "rfc822Name" -> value 51 | case (2, value: String) => "dNSName" -> value 52 | case (3, value: String) => "x400Address" -> value 53 | case (4, value: String) => "directoryName" -> value 54 | case (5, value: String) => "ediPartyName" -> value 55 | case (6, value: String) => "uniformResourceIdentifier" -> value 56 | case (7, value: String) => "iPAddress" -> value 57 | case (8, value: String) => "registeredID" -> value 58 | } 59 | } 60 | } 61 | 62 | def extractFieldFromSubject(cert: X509Certificate): Option[String] = { 63 | val dn = cert.getSubjectX500Principal.getName 64 | val ldapName = new LdapName(dn) 65 | ldapName 66 | .getRdns 67 | .asScala 68 | .collectFirst { 69 | case rdn if rdn.getType == certificateField => rdn.getValue.toString 70 | } 71 | } 72 | 73 | def extractFieldFromSAN(cert: X509Certificate): Option[String] = 74 | for { 75 | san <- Option(cert.getSubjectAlternativeNames) 76 | fieldValue <- san.asScala.collectFirst { 77 | case CertificateSAN(`certificateField`, value) => value 78 | } 79 | } yield fieldValue 80 | 81 | override def getAuthContext[A](request: Request[A]): Option[AuthContext] = 82 | request 83 | .clientCertificateChain 84 | .flatMap(_.headOption) 85 | .flatMap { cert => 86 | extractFieldFromSubject(cert) 87 | .orElse(extractFieldFromSAN(cert)) 88 | .flatMap(userId => userSrv.getAuthContext(request, userId, requestOrganisation(request)).toOption) 89 | } 90 | } 91 | 92 | @Singleton 93 | class PkiAuthProvider @Inject() (requestOrganisation: RequestOrganisation, userSrv: UserSrv, ec: ExecutionContext) extends AuthSrvProvider { 94 | override val name: String = "pki" 95 | override def apply(config: Configuration): Try[AuthSrv] = 96 | for { 97 | certificateField <- config.getOrFail[String]("certificateField") 98 | } yield new PkiAuthSrv(certificateField, requestOrganisation, userSrv, ec) 99 | } 100 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/UserSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.auth 2 | 3 | import org.thp.scalligraph.EntityIdOrName 4 | import org.thp.scalligraph.utils.Instance 5 | import play.api.libs.functional.syntax._ 6 | import play.api.libs.json._ 7 | import play.api.mvc.RequestHeader 8 | 9 | import scala.util.Try 10 | 11 | trait AuthContext { 12 | def userId: String 13 | def userName: String 14 | def organisation: EntityIdOrName 15 | def requestId: String 16 | def permissions: Set[Permission] 17 | def changeOrganisation(newOrganisation: EntityIdOrName, newPermissions: Set[Permission]): AuthContext 18 | def isPermitted(requiredPermission: Permission): Boolean = permissions.contains(requiredPermission) 19 | } 20 | 21 | case class AuthContextImpl(userId: String, userName: String, organisation: EntityIdOrName, requestId: String, permissions: Set[Permission]) 22 | extends AuthContext { 23 | override def changeOrganisation(newOrganisation: EntityIdOrName, newPermissions: Set[Permission]): AuthContext = 24 | copy(organisation = newOrganisation, permissions = newPermissions) 25 | } 26 | 27 | object AuthContext { 28 | 29 | def fromJson(request: RequestHeader, json: String): Try[AuthContext] = 30 | Try { 31 | Json.parse(json).as(reads(Instance.getRequestId(request))) 32 | } 33 | 34 | def reads(requestId: String): Reads[AuthContext] = 35 | ((JsPath \ "userId").read[String] and 36 | (JsPath \ "userName").read[String] and 37 | (JsPath \ "organisation").read[String].map(EntityIdOrName.apply) and 38 | Reads.pure(requestId) and 39 | (JsPath \ "permissions").read[Set[String]].map(Permission.apply))(AuthContextImpl.apply _) 40 | 41 | implicit val writes: Writes[AuthContext] = Writes[AuthContext] { authContext => 42 | Json.obj( 43 | "userId" -> authContext.userId, 44 | "userName" -> authContext.userName, 45 | "organisation" -> authContext.organisation.toString, 46 | "permissions" -> authContext.permissions 47 | ) 48 | } 49 | } 50 | 51 | trait UserSrv { 52 | def getAuthContext(request: RequestHeader, userId: String, organisationName: Option[EntityIdOrName]): Try[AuthContext] 53 | def getSystemAuthContext: AuthContext 54 | def createUser(userId: String, userInfo: JsObject): Try[User] 55 | } 56 | 57 | trait User { 58 | val id: String 59 | def getUserName: String 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/auth/package.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import shapeless.tag.@@ 4 | 5 | package object auth { 6 | type Permission = String @@ PermissionTag 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/controllers/Annotations.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | class WithParser[A](fieldsParser: FieldsParser[A]) extends StaticAnnotation 6 | 7 | class WithUpdateParser[A](fieldsParsers: UpdateFieldsParser[A]) extends StaticAnnotation 8 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/controllers/AuthenticatedRequest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import org.thp.scalligraph.EntityIdOrName 4 | import org.thp.scalligraph.auth.{AuthContext, Permission} 5 | import org.thp.scalligraph.utils.Instance 6 | import play.api.mvc.{Request, WrappedRequest} 7 | 8 | /** 9 | * A request with authentication information 10 | * 11 | * @param authContext authentication information (which contains user name, permissions, ...) 12 | * @param request the request 13 | * @tparam A the body content type. 14 | */ 15 | class AuthenticatedRequest[A](val authContext: AuthContext, request: Request[A]) extends WrappedRequest[A](request) with AuthContext with Request[A] { 16 | override def userId: String = authContext.userId 17 | override def userName: String = authContext.userName 18 | override def organisation: EntityIdOrName = authContext.organisation 19 | override def requestId: String = Instance.getRequestId(request) 20 | override def permissions: Set[Permission] = authContext.permissions 21 | override def map[B](f: A => B): AuthenticatedRequest[B] = new AuthenticatedRequest(authContext, request.map(f)) 22 | override def changeOrganisation(newOrganisation: EntityIdOrName, newPermissions: Set[Permission]): AuthContext = 23 | authContext.changeOrganisation(newOrganisation, newPermissions) 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/controllers/UpdateFieldsParser.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.controllers 2 | 3 | import org.scalactic.Accumulation._ 4 | import org.scalactic.{Every, Or} 5 | import org.thp.scalligraph.AttributeError 6 | import org.thp.scalligraph.`macro`.FieldsParserMacro 7 | 8 | import scala.language.experimental.macros 9 | 10 | case class UpdateFieldsParser[T](formatName: String, parsers: Seq[(FPath, FieldsParser[_])]) { 11 | 12 | def ++(updateFieldsParser: UpdateFieldsParser[_]): UpdateFieldsParser[T] = 13 | new UpdateFieldsParser[T](formatName, parsers ++ updateFieldsParser.parsers) 14 | 15 | def forType[A](newFormatName: String) = new UpdateFieldsParser[A](newFormatName, parsers) 16 | 17 | def apply(field: FObject): Seq[(FPath, Any)] Or Every[AttributeError] = 18 | field 19 | .fields 20 | .toSeq 21 | .flatMap { 22 | case (key, value) => 23 | val path = FPath(key) 24 | parsers.collectFirst { 25 | case (p, parser) if p.matches(path) => 26 | parser(value) 27 | .map(path -> _) 28 | .badMap(x => x.map(_.withName(path.toString))) 29 | } 30 | } 31 | .combined 32 | 33 | def on(pathStr: String): UpdateFieldsParser[T] = 34 | new UpdateFieldsParser[T](formatName, parsers.map { case (path, parser) => FPathElem(pathStr, path) -> parser }) 35 | 36 | def seq(pathStr: String): UpdateFieldsParser[T] = 37 | new UpdateFieldsParser[T](formatName, parsers.map { case (path, parser) => FPathSeq(pathStr, path) -> parser }) 38 | } 39 | 40 | object UpdateFieldsParser { 41 | def empty[T](formatName: String): UpdateFieldsParser[T] = new UpdateFieldsParser[T](formatName, Nil) 42 | def apply[T]: UpdateFieldsParser[T] = macro FieldsParserMacro.getOrBuildUpdateFieldsParser[T] 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/macro/AnnotationMacro.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.`macro` 2 | 3 | import scala.reflect.macros.whitebox 4 | 5 | class AnnotationMacro(val c: whitebox.Context) extends MacroUtil with MappingMacroHelper with MacroLogger { 6 | 7 | import c.universe._ 8 | 9 | def buildVertexModel(annottees: Tree*): Tree = 10 | annottees.toList match { 11 | case (modelClass @ ClassDef(classMods, className, Nil, _)) :: tail if classMods.hasFlag(Flag.CASE) => 12 | val modelDef = Seq( 13 | q"val model: org.thp.scalligraph.models.Model.Vertex[$className] = org.thp.scalligraph.models.Model.buildVertexModel[$className]" 14 | ) 15 | 16 | val modelModule = tail match { 17 | case ModuleDef(moduleMods, moduleName, moduleTemplate) :: Nil => 18 | val parents = tq"org.thp.scalligraph.models.HasModel" :: moduleTemplate.parents.filterNot { 19 | case Select(_, TypeName("AnyRef")) => true 20 | case _ => false 21 | } 22 | 23 | ModuleDef( 24 | moduleMods, 25 | moduleName, 26 | Template(parents = parents, self = moduleTemplate.self, body = moduleTemplate.body ++ modelDef) 27 | ) 28 | case Nil => 29 | val moduleName = className.toTermName 30 | q"object $moduleName extends org.thp.scalligraph.models.HasModel { ..$modelDef }" 31 | } 32 | 33 | Block(modelClass :: modelModule :: Nil, Literal(Constant(()))) 34 | } 35 | 36 | def buildEdgeModel(annottees: Tree*): Tree = 37 | annottees.toList match { 38 | case (modelClass @ ClassDef(classMods, className, Nil, _)) :: tail if classMods.hasFlag(Flag.CASE) => 39 | val modelDef = Seq( 40 | q"val model = org.thp.scalligraph.models.Model.buildEdgeModel[$className]" 41 | ) 42 | val modelModule = tail match { 43 | case ModuleDef(moduleMods, moduleName, moduleTemplate) :: Nil => 44 | val parents = tq"org.thp.scalligraph.models.HasModel" :: moduleTemplate.parents.filterNot { 45 | case Select(_, TypeName("AnyRef")) => true 46 | case _ => false 47 | } 48 | ModuleDef( 49 | moduleMods, 50 | moduleName, 51 | Template( 52 | parents = parents, 53 | self = moduleTemplate.self, 54 | body = moduleTemplate.body ++ modelDef 55 | ) 56 | ) 57 | case Nil => 58 | val moduleName = className.toTermName 59 | q"object $moduleName extends org.thp.scalligraph.models.HasModel { ..$modelDef }" 60 | } 61 | 62 | Block(modelClass :: modelModule :: Nil, Literal(Constant(()))) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/macro/IndexMacro.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.`macro` 2 | 3 | import org.thp.scalligraph.models.DefineIndex 4 | 5 | import scala.reflect.macros.blackbox 6 | 7 | trait IndexMacro { 8 | val c: blackbox.Context 9 | 10 | import c.universe._ 11 | 12 | def getIndexes[E: WeakTypeTag]: Tree = { 13 | val eType = weakTypeOf[E] 14 | val indexes = eType.typeSymbol.annotations.collect { 15 | case annotation if annotation.tree.tpe <:< typeOf[DefineIndex] => 16 | val args = annotation.tree.children.tail 17 | val indexType = args.head 18 | val fields = args.tail 19 | q"$indexType -> $fields" 20 | } 21 | q"Seq(..$indexes)" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/macro/MacroError.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.`macro` 2 | 3 | case class MacroError(message: String, cause: Option[Throwable] = None) extends Exception(message, cause.orNull) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/macro/MacroLogger.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.`macro` 2 | 3 | import java.io.{PrintWriter, StringWriter} 4 | 5 | import scala.annotation.StaticAnnotation 6 | import scala.reflect.macros.blackbox 7 | 8 | object LogLevel { 9 | type Value = Int 10 | val trace = 5 11 | val debug = 4 12 | val info = 3 13 | val warn = 2 14 | val error = 1 15 | } 16 | 17 | class TraceLogLevel extends StaticAnnotation 18 | 19 | class DebugLogLevel extends StaticAnnotation 20 | 21 | class InfoLogLevel extends StaticAnnotation 22 | 23 | class WarnLogLevel extends StaticAnnotation 24 | 25 | class ErrorLogLevel extends StaticAnnotation 26 | 27 | class LogGeneratedCode extends StaticAnnotation 28 | 29 | trait MacroLogger { 30 | val c: blackbox.Context 31 | 32 | import c.universe._ 33 | 34 | var level: LogLevel.Value = LogLevel.error 35 | var logGeneratedCode: Boolean = false 36 | 37 | def initLogger(s: Symbol): Unit = { 38 | level = s 39 | .annotations 40 | .map(_.tree) 41 | .collect { 42 | case a if a.tpe <:< typeOf[TraceLogLevel] => LogLevel.trace 43 | case a if a.tpe <:< typeOf[DebugLogLevel] => LogLevel.debug 44 | case a if a.tpe <:< typeOf[InfoLogLevel] => LogLevel.info 45 | case a if a.tpe <:< typeOf[WarnLogLevel] => LogLevel.warn 46 | case a if a.tpe <:< typeOf[ErrorLogLevel] => LogLevel.error 47 | } 48 | .reduceOption(Math.max) 49 | .getOrElse(LogLevel.error) 50 | logGeneratedCode = s.annotations.exists(_.tree.tpe <:< typeOf[LogGeneratedCode]) 51 | } 52 | 53 | private def getCauseMessages(throwable: Throwable, causeMessages: Seq[String] = Nil): String = 54 | Option(throwable).fold(causeMessages.mkString("\n")) { e => 55 | getCauseMessages(e.getCause, causeMessages :+ s"${e.getClass}: ${e.getMessage}") 56 | } 57 | 58 | private def printStackTrace(throwable: Throwable): String = { 59 | val writer = new StringWriter 60 | throwable.printStackTrace(new PrintWriter(writer)) 61 | writer.toString 62 | } 63 | 64 | def isTraceEnabled: Boolean = level >= LogLevel.trace 65 | def trace(msg: => String): Unit = if (isTraceEnabled) println(s"[TRACE] $msg") 66 | 67 | def isDebugEnabled: Boolean = level >= LogLevel.debug 68 | def debug(msg: => String): Unit = if (isDebugEnabled) println(s"[DEBUG] $msg") 69 | 70 | def isInfoEnabled: Boolean = level >= LogLevel.info 71 | def info(msg: => String): Unit = if (isInfoEnabled) println(s"[INFO] $msg") 72 | 73 | def isWarnEnabled: Boolean = level >= LogLevel.warn 74 | def warn(msg: => String): Unit = if (isWarnEnabled) println(s"[WARN] $msg") 75 | def warn(msg: => String, throwable: Throwable): Unit = if (isWarnEnabled) println(s"[WARN] $msg\n${printStackTrace(throwable)}") 76 | 77 | def isErrorEnabled: Boolean = level >= LogLevel.error 78 | def error(msg: => String): Unit = if (isErrorEnabled) println(s"[ERROR] $msg") 79 | 80 | def error(msg: => String, throwable: Throwable): Unit = 81 | if (isErrorEnabled) c.error(c.enclosingPosition, s"[ERROR] $msg\n${getCauseMessages(throwable)}") 82 | 83 | def fatal(msg: => String): Nothing = c.abort(c.enclosingPosition, s"[ERROR] $msg") 84 | def fatal(msg: => String, throwable: Throwable): Nothing = c.abort(c.enclosingPosition, s"[ERROR] $msg\n${printStackTrace(throwable)}") 85 | 86 | def cleanupCode(code: String): String = 87 | code 88 | .replaceAllLiterally("/" + "*{}*/", "") 89 | .replaceAll(";\n", "\n") 90 | 91 | def ret(msg: => String, tree: Tree): Tree = { 92 | if (logGeneratedCode) println(s"[CODE] $msg\n${cleanupCode(showCode(tree))}") 93 | tree 94 | } 95 | 96 | def ret[T](msg: => String, expr: Expr[T]): Expr[T] = { 97 | if (logGeneratedCode) println(s"[CODE] $msg\n${cleanupCode(showCode(expr.tree, printOwners = false))}") 98 | expr 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/macro/MappingMacroHelper.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.`macro` 2 | 3 | import org.thp.scalligraph.models._ 4 | 5 | import scala.reflect.macros.blackbox 6 | 7 | trait MappingMacroHelper extends MacroUtil with MacroLogger { 8 | val c: blackbox.Context 9 | 10 | import c.universe._ 11 | 12 | case class MappingSymbol(name: String, valName: TermName, definition: Tree, tpe: Type) 13 | 14 | def getEnumMapping(eType: Type, symbol: Symbol): Option[Tree] = 15 | eType match { 16 | case EnumerationType(members @ _*) => 17 | val valueCases = members.map { 18 | case (name, value) => cq"$name => $value" 19 | } :+ 20 | cq"""other => throw org.thp.scalligraph.InternalError( 21 | "Wrong value " + other + 22 | " for numeration " + ${symbol.toString} + 23 | ". Possible values are " + ${members.map(_._1).mkString(",")})""" 24 | Some(q"""org.thp.scalligraph.models.SingleMapping[$eType, String]((_: $eType).toString, (_: String) match { case ..$valueCases })""") 25 | case _ => None 26 | } 27 | 28 | def getImplicitMapping(symbol: Symbol): Option[Tree] = { 29 | val mappingType = appliedType(typeOf[UMapping[_]].typeConstructor, symbol.typeSignature) 30 | val mapping = c.inferImplicitValue(mappingType, silent = true, withMacrosDisabled = true) 31 | if (mapping.tpe =:= NoType) None 32 | else Some(mapping) 33 | } 34 | def getEntityMappings[E: WeakTypeTag]: Seq[MappingSymbol] = { 35 | val eType = weakTypeOf[E] 36 | eType match { 37 | case CaseClassType(symbols @ _*) => 38 | symbols.map { s => 39 | val mapping = getImplicitMapping(s) 40 | .orElse(getEnumMapping(s.typeSignature, s)) 41 | .getOrElse(fatal(s"Fail to get mapping of $s (${s.typeSignature})")) 42 | MappingSymbol(s.name.decodedName.toString.trim, TermName(c.freshName(s.name + "Mapping")), mapping, s.typeSignature) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/models/Model.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.apache.tinkerpop.gremlin.structure.{Edge, Element, Vertex} 4 | import org.thp.scalligraph.`macro`.ModelMacro 5 | import org.thp.scalligraph.traversal.TraversalOps._ 6 | import org.thp.scalligraph.traversal.{Converter, Graph} 7 | import org.thp.scalligraph.{EntityId, NotFoundError} 8 | 9 | import java.util.Date 10 | import scala.annotation.StaticAnnotation 11 | import scala.collection.JavaConverters._ 12 | import scala.language.experimental.macros 13 | 14 | class Readonly extends StaticAnnotation 15 | 16 | object IndexType extends Enumeration { 17 | val basic, standard, unique, fulltext, fulltextOnly = Value 18 | } 19 | class DefineIndex(indexType: IndexType.Value, fields: String*) extends StaticAnnotation 20 | 21 | trait HasModel { 22 | val model: Model 23 | } 24 | 25 | trait Entity { _: Product => 26 | def _id: EntityId 27 | def _label: String 28 | def _createdBy: String 29 | def _updatedBy: Option[String] 30 | def _createdAt: Date 31 | def _updatedAt: Option[Date] 32 | } 33 | 34 | object Model { 35 | type Base[E0 <: Product] = Model { 36 | type E = E0 37 | } 38 | 39 | type Vertex[E0 <: Product] = VertexModel { 40 | type E = E0 41 | } 42 | 43 | type Edge[E0 <: Product] = EdgeModel { 44 | type E = E0 45 | } 46 | 47 | def buildVertexModel[E <: Product]: Model.Vertex[E] = 48 | macro ModelMacro.mkVertexModel[E] 49 | 50 | def buildEdgeModel[E <: Product]: Model.Edge[E] = 51 | macro ModelMacro.mkEdgeModel[E] 52 | 53 | def printElement(e: Element): String = 54 | e + e 55 | .properties[Any]() 56 | .asScala 57 | .map(p => s"\n - ${p.key()} = ${p.orElse("")}") 58 | .mkString 59 | 60 | implicit def vertex[E <: Product]: Vertex[E] = macro ModelMacro.getModel[E] 61 | implicit def edge[E <: Product]: Edge[E] = macro ModelMacro.getModel[E] 62 | } 63 | 64 | abstract class Model { 65 | type E <: Product 66 | type EEntity = E with Entity 67 | type ElementType <: Element 68 | 69 | val label: String 70 | 71 | val indexes: Seq[(IndexType.Value, Seq[String])] 72 | 73 | def get(id: EntityId)(implicit graph: Graph): ElementType 74 | val fields: Map[String, Mapping[_, _, _]] 75 | def addEntity(e: E, entity: Entity): EEntity 76 | val converter: Converter[EEntity, ElementType] 77 | } 78 | 79 | abstract class VertexModel extends Model { 80 | override type ElementType = Vertex 81 | 82 | val initialValues: Seq[E] = Nil 83 | 84 | def create(e: E)(implicit graph: Graph): Vertex 85 | 86 | override def get(id: EntityId)(implicit graph: Graph): Vertex = 87 | graph.V(label, id).headOption.getOrElse(throw NotFoundError(s"Vertex $id not found")) 88 | } 89 | 90 | abstract class EdgeModel extends Model { 91 | override type ElementType = Edge 92 | 93 | def create(e: E, from: Vertex, to: Vertex)(implicit graph: Graph): Edge 94 | 95 | override def get(id: EntityId)(implicit graph: Graph): Edge = 96 | graph.E(label, id).headOption.getOrElse(throw NotFoundError(s"Edge $id not found")) 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/models/NoValue.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import java.lang.{Boolean => JBoolean, Double => JDouble, Float => JFloat, Integer => JInteger, Long => JLong} 4 | import java.util.Date 5 | 6 | abstract class NoValue[A] { 7 | def apply(): A 8 | } 9 | 10 | object NoValue { 11 | def apply[A](zero: A): NoValue[A] = () => zero 12 | implicit val anyRef: NoValue[AnyRef] = NoValue[AnyRef]("") // for ID 13 | implicit val string: NoValue[String] = NoValue[String]("") 14 | implicit val long: NoValue[JLong] = NoValue[JLong](0L) 15 | implicit val int: NoValue[JInteger] = NoValue[JInteger](0) 16 | implicit val date: NoValue[Date] = NoValue[Date](new Date(0)) 17 | implicit val boolean: NoValue[JBoolean] = NoValue[JBoolean](false) 18 | implicit val double: NoValue[JDouble] = NoValue[JDouble](0.0) 19 | implicit val float: NoValue[JFloat] = NoValue[JFloat](0f) 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/models/Schema.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import org.thp.scalligraph.auth.AuthContext 4 | import play.api.Logger 5 | 6 | import javax.inject.{Inject, Provider, Singleton} 7 | import scala.collection.immutable 8 | import scala.util.Try 9 | 10 | case class SchemaStatus(name: String, currentVersion: Int, expectedVersion: Int, error: Option[Throwable]) 11 | 12 | trait UpdatableSchema extends Schema { 13 | val authContext: AuthContext 14 | val operations: Operations 15 | lazy val name: String = operations.schemaName 16 | def schemaStatus: Option[SchemaStatus] = _updateStatus 17 | private var _updateStatus: Option[SchemaStatus] = None 18 | def update(db: Database): Try[Unit] = { 19 | val result = operations.execute(db, this)(authContext) 20 | _updateStatus = Some(SchemaStatus(name, db.version(name), operations.operations.length + 1, result.fold(Some(_), _ => None))) 21 | result 22 | } 23 | } 24 | 25 | trait Schema { schema => 26 | def modelList: Seq[Model] 27 | final def getModel(label: String): Option[Model] = modelList.find(_.label == label) 28 | 29 | def +(other: Schema): Schema = 30 | new Schema { 31 | override def modelList: Seq[Model] = schema.modelList ++ other.modelList 32 | } 33 | } 34 | 35 | object Schema { 36 | 37 | def empty: Schema = 38 | new Schema { 39 | override def modelList: Seq[Model] = Nil 40 | } 41 | } 42 | 43 | @Singleton 44 | class GlobalSchema @Inject() (schemas: immutable.Set[UpdatableSchema]) extends Provider[Schema] { 45 | lazy val logger: Logger = Logger(getClass) 46 | lazy val schema: Schema = { 47 | logger.debug(s"Build global schema from ${schemas.map(_.getClass.getSimpleName).mkString("+")}") 48 | schemas.reduceOption[Schema](_ + _).getOrElse(Schema.empty) 49 | } 50 | override def get(): Schema = schema 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/models/TransactionHandler.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.models 2 | 3 | import akka.stream.SubscriptionWithCancelException.NonFailureCancellation 4 | import akka.stream._ 5 | import akka.stream.scaladsl.{Broadcast, Flow, GraphDSL, Source} 6 | import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler} 7 | 8 | object TransactionHandler { 9 | def apply[TX, E, M](newTx: () => TX, commit: TX => Unit, rollback: TX => Unit, flow: Flow[TX, E, M]): Source[E, M] = 10 | Source.fromGraph(GraphDSL.create(flow) { implicit builder => flowShape => 11 | import GraphDSL.Implicits._ 12 | val tx = Source.lazySingle(newTx) 13 | val bcast = builder.add(Broadcast[TX](2)) 14 | val txHandler = builder.add(new TransactionHandler[TX, E](commit, rollback)) 15 | 16 | tx ~> bcast 17 | bcast.out(0) ~> txHandler.in0 18 | bcast.out(1) ~> flowShape ~> txHandler.in1 19 | 20 | SourceShape(txHandler.out) 21 | }) 22 | } 23 | 24 | class TransactionHandler[TX, A](commit: TX => Unit, rollback: TX => Unit) extends GraphStage[FanInShape2[TX, A, A]] { 25 | val txIn: Inlet[TX] = Inlet[TX]("txIn") 26 | val elemIn: Inlet[A] = Inlet[A]("elemIn") 27 | val out: Outlet[A] = Outlet[A]("out") 28 | override val shape: FanInShape2[TX, A, A] = new FanInShape2(txIn, elemIn, out) 29 | 30 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) { 31 | var tx: Option[TX] = None 32 | def doCommit(): Unit = { 33 | tx.foreach(commit) 34 | tx = None 35 | } 36 | def doRollback(): Unit = { 37 | tx.foreach(rollback) 38 | tx = None 39 | } 40 | 41 | override def preStart(): Unit = pull(txIn) 42 | 43 | override def postStop(): Unit = { 44 | doCommit() 45 | super.postStop() 46 | } 47 | 48 | setHandler(txIn, new InHandler { 49 | override def onPush(): Unit = tx = Some(grab(txIn)) 50 | 51 | override def onUpstreamFinish(): Unit = () 52 | }) 53 | 54 | setHandler( 55 | elemIn, 56 | new InHandler { 57 | override def onPush(): Unit = push(out, grab(elemIn)) 58 | 59 | override def onUpstreamFinish(): Unit = { 60 | doCommit() 61 | completeStage() 62 | } 63 | 64 | override def onUpstreamFailure(ex: Throwable): Unit = { 65 | doRollback() 66 | failStage(ex) 67 | } 68 | } 69 | ) 70 | 71 | setHandler( 72 | out, 73 | new OutHandler { 74 | override def onPull(): Unit = pull(elemIn) 75 | 76 | override def onDownstreamFinish(cause: Throwable): Unit = { 77 | cause match { 78 | case _: NonFailureCancellation => doCommit() 79 | case _ => doRollback() 80 | } 81 | 82 | super.onDownstreamFinish(cause) 83 | } 84 | } 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/package.scala: -------------------------------------------------------------------------------- 1 | package org.thp 2 | 3 | import scala.concurrent.duration._ 4 | import scala.util.{Failure, Success, Try} 5 | 6 | package object scalligraph { 7 | implicit class RichOptionTry[A](o: Option[Try[A]]) { 8 | def flip: Try[Option[A]] = o.fold[Try[Option[A]]](Success(None))(_.map(Some.apply)) 9 | } 10 | implicit class RichTryOption[A](t: Try[Option[A]]) { 11 | def flip: Option[Try[A]] = t.fold(f => Some(Failure(f)), _.map(a => Success(a))) 12 | } 13 | 14 | implicit class RichOption[A](o: Option[A]) { 15 | def toTry(f: Failure[A]): Try[A] = o.fold[Try[A]](f)(Success.apply) 16 | } 17 | 18 | implicit class RichSeq[A](s: TraversableOnce[A]) { 19 | def toTry[B](f: A => Try[B]): Try[Seq[B]] = 20 | s.foldLeft[Try[Seq[B]]](Success(Nil)) { 21 | case (Success(l), a) => f(a).map(l :+ _) 22 | case (failure, _) => failure 23 | } 24 | } 25 | 26 | val timeUnitList: List[TimeUnit] = DAYS :: HOURS :: MINUTES :: SECONDS :: MILLISECONDS :: MICROSECONDS :: NANOSECONDS :: Nil 27 | implicit class RichFiniteDuration(duration: FiniteDuration) { 28 | def prettyPrint: String = 29 | timeUnitList 30 | .tails 31 | .collectFirst { 32 | case u +: r if duration >= FiniteDuration(1, u) => 33 | val l = FiniteDuration(duration.toUnit(u).toLong, u) 34 | (l.toString, duration - l, r) 35 | } 36 | .fold(duration.toString) { 37 | case (s, r, ru +: _) if r > FiniteDuration(1, ru) => s"$s ${FiniteDuration(r.toUnit(ru).toLong, ru)}" 38 | case (s, _, _) => s 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/query/InputSort.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.query 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.Order 4 | import org.scalactic.Accumulation._ 5 | import org.scalactic.{Bad, Good, One} 6 | import org.thp.scalligraph.auth.AuthContext 7 | import org.thp.scalligraph.controllers.{FPath, FSeq, FString, FieldsParser} 8 | import org.thp.scalligraph.traversal.Traversal 9 | import org.thp.scalligraph.{BadRequestError, InvalidFormatAttributeError} 10 | 11 | import scala.reflect.runtime.{universe => ru} 12 | 13 | case class InputSort(fieldOrder: (String, Order)*) extends InputQuery[Traversal.Unk, Traversal.Unk] { 14 | override def apply( 15 | publicProperties: PublicProperties, 16 | traversalType: ru.Type, 17 | traversal: Traversal.Unk, 18 | authContext: AuthContext 19 | ): Traversal.Unk = 20 | if (fieldOrder.isEmpty) traversal 21 | else 22 | fieldOrder.foldLeft(traversal.onRaw(_.order)) { 23 | case (t, (fieldName, order)) => 24 | val fieldPath = FPath(fieldName) 25 | val property = publicProperties 26 | .get[Traversal.UnkD, Traversal.UnkDU](fieldPath, traversalType) 27 | .getOrElse(throw BadRequestError(s"Property $fieldName for type $traversalType not found")) 28 | property.sort(fieldPath, t, authContext, order) 29 | } 30 | } 31 | 32 | object InputSort { 33 | implicit val fieldsParser: FieldsParser[InputSort] = FieldsParser("sort-f") { 34 | case (_, FObjOne("_fields", FSeq(f))) => 35 | f.validatedBy { 36 | case FObjOne(name, FString(order)) => 37 | try Good(new InputSort(name -> Order.valueOf(order))) 38 | catch { 39 | case _: IllegalArgumentException => 40 | Bad(One(InvalidFormatAttributeError("order", "order", Order.values().map(o => s"field: '$o'").toSet, FString(order)))) 41 | } 42 | case FString(name) if name(0) == '-' => Good(new InputSort(name -> Order.desc)) 43 | case FString(name) if name(0) == '+' => Good(new InputSort(name -> Order.asc)) 44 | case FString(name) => Good(new InputSort(name -> Order.asc)) 45 | case other => Bad(One(InvalidFormatAttributeError("order", "order", Order.values.map(o => s"field: '$o'").toSet, other))) 46 | }.map(x => new InputSort(x.flatMap(_.fieldOrder): _*)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/query/PredicateOps.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.query 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.P 4 | import org.apache.tinkerpop.gremlin.process.traversal.util.{AndP, OrP} 5 | 6 | import java.util.function.BiPredicate 7 | import java.util.stream.Collectors 8 | import java.util.{List => JList} 9 | 10 | object PredicateOps { 11 | 12 | implicit class PredicateOpsDefs[A](predicate: P[A]) { 13 | def mapValue[B](f: A => B): P[B] = 14 | predicate match { 15 | case or: OrP[_] => new OrP[B](or.getPredicates.stream().map[P[B]](p => p.asInstanceOf[P[A]].mapValue(f)).collect(Collectors.toList())) 16 | case and: AndP[_] => new AndP[B](and.getPredicates.stream().map[P[B]](p => p.asInstanceOf[P[A]].mapValue(f)).collect(Collectors.toList())) 17 | case _ => 18 | val biPredicate: BiPredicate[B, B] = predicate.getBiPredicate.asInstanceOf[BiPredicate[B, B]] 19 | predicate.getValue match { 20 | case l: JList[_] => 21 | val x: JList[B] = l.stream().map[B](v => f(v.asInstanceOf[A])).collect(Collectors.toList()) 22 | new P(biPredicate, x.asInstanceOf[B]) 23 | case v => 24 | try new P(biPredicate, f(v)) 25 | catch { case _: ClassCastException => predicate.asInstanceOf[P[B]] } 26 | } 27 | } 28 | 29 | def mapPred[B](fv: A => B, fp: P[B] => P[B]): P[B] = 30 | predicate match { 31 | case or: OrP[_] => new OrP[B](or.getPredicates.stream().map[P[B]](p => p.asInstanceOf[P[A]].mapPred(fv, fp)).collect(Collectors.toList())) 32 | case and: AndP[_] => new AndP[B](and.getPredicates.stream().map[P[B]](p => p.asInstanceOf[P[A]].mapPred(fv, fp)).collect(Collectors.toList())) 33 | case _ => 34 | val biPredicate: BiPredicate[B, B] = predicate.getBiPredicate.asInstanceOf[BiPredicate[B, B]] 35 | predicate.getValue match { 36 | case l: JList[_] => 37 | val x: JList[B] = l.stream().map[B](v => fv(v.asInstanceOf[A])).collect(Collectors.toList()) 38 | fp(new P(biPredicate, x.asInstanceOf[B])) 39 | case v => 40 | try fp(new P(biPredicate, fv(v))) 41 | catch { case _: ClassCastException => predicate.asInstanceOf[P[B]] } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/query/Utils.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.query 2 | 3 | import org.thp.scalligraph.controllers._ 4 | import org.thp.scalligraph.query.InputFilter.logger 5 | 6 | object FObjOne { 7 | 8 | def unapply(field: Field): Option[(String, Field)] = field match { 9 | case FObject(f) if f.size == 1 => f.headOption 10 | case _ => None 11 | } 12 | } 13 | 14 | object FDeprecatedObjOne { 15 | def unapply(field: Field): Option[(String, Field)] = field match { 16 | case FObject(f) if f.size == 1 => 17 | val (key, value) = f.head 18 | logger.warn(s"""Use of filter {"$key": "$value"} is deprecated. Please use {"_field": "$key", "_value": "$value"}""") 19 | f.headOption 20 | case _ => None 21 | } 22 | } 23 | 24 | object FFieldValue { 25 | 26 | def unapply(field: Field): Option[(String, Field)] = { 27 | val fieldName = field.get("_field") 28 | val fieldValue = field.get("_value") 29 | (fieldName, fieldValue) match { 30 | case (FString(name), value) if value.isDefined => Some(name -> value) 31 | case _ => None 32 | } 33 | } 34 | } 35 | 36 | object FFieldFromTo { 37 | 38 | def unapply(field: Field): Option[(String, Field, Field)] = { 39 | val fieldName = field.get("_field") 40 | val from = field.get("_from") 41 | val to = field.get("_to") 42 | (fieldName, from, to) match { 43 | case (FString(name), f, t) if from.isDefined && to.isDefined => Some((name, f, t)) 44 | case _ => None 45 | } 46 | 47 | } 48 | } 49 | 50 | object FNamedObj { 51 | 52 | def unapply(field: Field): Option[(String, FObject)] = field match { 53 | case f: FObject => 54 | f.get("_name") match { 55 | case FString(name) => Some(name -> (f - "_name")) 56 | case _ => None 57 | } 58 | case _ => None 59 | } 60 | } 61 | 62 | object FNative { 63 | 64 | def unapply(field: Field): Option[Any] = field match { 65 | case FString(s) => Some(s) 66 | case FNumber(n) => Some(n) 67 | case FBoolean(b) => Some(b) 68 | case FAny(a) => Some(a.mkString) 69 | case _ => None 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/record/Record.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.record 2 | 3 | import shapeless.{HList, Witness} 4 | 5 | import scala.language.experimental.macros 6 | 7 | trait Selector[L <: HList, K] { 8 | type Out 9 | def apply(l: L): Out 10 | } 11 | 12 | object Selector { 13 | type Aux[L <: HList, K, Out0] = Selector[L, K] { type Out = Out0 } 14 | 15 | def apply[L <: HList, K](implicit selector: Selector[L, K]): Aux[L, K, selector.Out] = selector 16 | 17 | implicit def mkSelector[L <: HList, K, O]: Aux[L, K, O] = 18 | macro RecordMacro.mkSelector[L, K] 19 | } 20 | 21 | case class Record[C <: HList](list: C) { 22 | type FSL[K] = Selector[C, K] 23 | 24 | def apply(key: Witness)(implicit selector: Selector[C, key.T]): selector.Out = 25 | selector(list) 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/record/RecordMacro.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.record 2 | 3 | import shapeless.labelled.KeyTag 4 | import shapeless.tag.Tagged 5 | import shapeless.{::, HList, HNil} 6 | 7 | import scala.annotation.tailrec 8 | import scala.reflect.macros.whitebox 9 | import scala.tools.nsc.Global 10 | import scala.{Symbol => ScalaSymbol} 11 | 12 | class UnsafeSelector[L <: HList, K, O](i: Int) extends Selector[L, K] { 13 | type Out = O 14 | 15 | def apply(l: L): O = HList.unsafeGet(l, i).asInstanceOf[O] 16 | } 17 | 18 | class RecordMacro(val c: whitebox.Context) { 19 | import c.universe._ 20 | 21 | def prefix(tpe: Type): Type = { 22 | val global = c.universe.asInstanceOf[Global] 23 | val gTpe = tpe.asInstanceOf[global.Type] 24 | gTpe.prefix.asInstanceOf[Type] 25 | } 26 | 27 | object FieldType { 28 | 29 | import internal._ 30 | 31 | val keyTagTpe: Type = typeOf[KeyTag[_, _]] 32 | val keyTagSym: Symbol = keyTagTpe.typeSymbol 33 | 34 | def apply(kTpe: Type, vTpe: Type): Type = 35 | refinedType(List(vTpe, typeRef(prefix(keyTagTpe), keyTagTpe.typeSymbol, List(kTpe, vTpe))), NoSymbol) 36 | 37 | def unapply(fTpe: Type): Option[(Type, Type)] = 38 | fTpe.dealias match { 39 | case RefinedType(l, _) => 40 | val rf = refinedType(l.init, NoSymbol) 41 | l.last match { 42 | case TypeRef(_, `keyTagSym`, List(k, v1)) if v1 =:= rf => 43 | Some(k -> rf) 44 | case _ => None 45 | } 46 | case _ => None 47 | } 48 | } 49 | 50 | private lazy val hconsTpe = typeOf[::[_, _]] 51 | private lazy val tagTpe = typeOf[Tagged[_]] 52 | private lazy val hconsPre = prefix(hconsTpe) 53 | private lazy val hnilTpe = typeOf[HNil] 54 | private lazy val symbolTpe = typeOf[ScalaSymbol] 55 | private lazy val tagPre = prefix(tagTpe) 56 | private lazy val tagSym = tagTpe.typeSymbol 57 | 58 | object Record { 59 | 60 | /** 61 | * Extract shapeless record element type 62 | * 63 | * @param l type of the shapeless record 64 | * @return a tuple with key type (singleton), value type and the rest of the record 65 | */ 66 | def unapply(l: Type): Option[(Type, Type, Type)] = 67 | if (l <:< typeOf[HNil]) None 68 | else 69 | l.baseType(hconsTpe.typeSymbol) match { 70 | case TypeRef(pre, _, List(FieldType(k, v), lTail)) if pre =:= hconsPre => 71 | Some((k, v, lTail)) 72 | case _ => None 73 | } 74 | } 75 | 76 | object KeyTag { 77 | 78 | def unapply(tTpe: Type): Option[String] = 79 | tTpe.dealias match { 80 | case RefinedType(List(`symbolTpe`, TypeRef(tPre, `tagSym`, List(ConstantType(Constant(name: String))))), _) if tPre =:= tagPre => 81 | Some(name) 82 | case _ => None 83 | } 84 | } 85 | 86 | def extractHlistTypes(hl: Type): List[(String, Type)] = { 87 | @tailrec 88 | def unfold(l: Type, acc: List[(String, Type)]): List[(String, Type)] = 89 | if (l <:< hnilTpe) acc 90 | else 91 | l.baseType(hconsTpe.typeSymbol) match { 92 | case TypeRef(pre, _, List(FieldType(KeyTag(k), v), lTail)) if pre =:= hconsPre => 93 | unfold(lTail, (k, v) :: acc) 94 | case _ => c.abort(c.enclosingPosition, s"$l is not an HList type") 95 | } 96 | 97 | unfold(hl, Nil) 98 | } 99 | 100 | def mkSelector[L <: HList, K](implicit lTag: WeakTypeTag[L], kTag: WeakTypeTag[K]): Tree = { 101 | def unfold(list: Type, key: Type): Option[(Type, Int)] = 102 | list match { 103 | case l if l <:< typeOf[HNil] => None 104 | case Record(k, v, _) if k =:= key => Some(v -> 0) 105 | case Record(_, _, tail) => 106 | unfold(tail, key).map { case (_v, i) => _v -> (i + 1) } 107 | case l => c.abort(c.enclosingPosition, s"$l is not an HList type") 108 | } 109 | 110 | val lTpe = lTag.tpe.dealias 111 | val kTpe = kTag.tpe.dealias 112 | unfold(lTpe, kTpe) match { 113 | case Some((v, i)) => 114 | q" new org.thp.scalligraph.record.UnsafeSelector[$lTpe, $kTpe, $v]($i) " 115 | case _ => 116 | c.echo(c.enclosingPosition, s"DEBUG: $lTag") 117 | c.echo(c.enclosingPosition, s"No field $kTpe in record [${extractHlistTypes(lTpe).map(_._1).mkString(",")}]") 118 | q" new org.thp.scalligraph.record.UnsafeSelector[$lTpe, $kTpe, Nothing](0) " 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/EdgeSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Edge 4 | import org.thp.scalligraph.auth.AuthContext 5 | import org.thp.scalligraph.models._ 6 | import org.thp.scalligraph.traversal.TraversalOps._ 7 | import org.thp.scalligraph.traversal.{Converter, Graph, Traversal} 8 | import org.thp.scalligraph.{EntityId, EntityIdOrName, NotFoundError} 9 | 10 | import scala.util.{Failure, Success, Try} 11 | 12 | class EdgeSrv[E <: Product, FROM <: Product, TO <: Product](implicit val model: Model.Edge[E]) extends ElementSrv[E, Edge] { 13 | override def startTraversal(implicit graph: Graph): Traversal[E with Entity, Edge, Converter[E with Entity, Edge]] = 14 | graph.E[E]()(model) 15 | 16 | // override def startTraversal(strategy: GraphStrategy)(implicit graph: Graph): Traversal.E[E] = 17 | // filterTraversal(Traversal.strategedE(strategy)) 18 | 19 | override def getByIds(ids: EntityId*)(implicit graph: Graph): Traversal.E[E] = 20 | if (ids.isEmpty) graph.empty 21 | else graph.E[E](ids: _*)(model) 22 | 23 | def getOrFail(idOrName: EntityIdOrName)(implicit graph: Graph): Try[E with Entity] = 24 | get(idOrName) 25 | .headOption 26 | .fold[Try[E with Entity]](Failure(NotFoundError(s"${model.label} $idOrName not found")))(Success.apply) 27 | 28 | def get(edge: Edge)(implicit graph: Graph): Traversal[E with Entity, Edge, Converter[E with Entity, Edge]] = 29 | graph.E[E](EntityId(edge.id()))(model) 30 | 31 | def getOrFail(edge: Edge)(implicit graph: Graph): Try[E with Entity] = 32 | get(edge) 33 | .headOption 34 | .fold[Try[E with Entity]](Failure(NotFoundError(s"${model.label} ${edge.id()} not found")))(Success.apply) 35 | 36 | def create(e: E, from: FROM with Entity, to: TO with Entity)(implicit graph: Graph, authContext: AuthContext): Try[E with Entity] = 37 | Try(graph.db.createEdge[E, FROM, TO](graph, authContext, model.asInstanceOf[Model.Edge[E]], e, from, to)) 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/ElementSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Element 4 | import org.thp.scalligraph.models.{Entity, Model} 5 | import org.thp.scalligraph.traversal.TraversalOps._ 6 | import org.thp.scalligraph.traversal.{Converter, Graph, Traversal} 7 | import org.thp.scalligraph.{EntityId, EntityIdOrName, InternalError} 8 | import play.api.Logger 9 | 10 | abstract class ElementSrv[E <: Product, G <: Element] { 11 | lazy val logger: Logger = Logger(getClass) 12 | 13 | val model: Model.Base[E] 14 | 15 | def startTraversal(implicit graph: Graph): Traversal[E with Entity, G, Converter[E with Entity, G]] 16 | 17 | def filterTraversal(traversal: Traversal[G, G, Converter.Identity[G]]): Traversal[E with Entity, G, Converter[E with Entity, G]] = 18 | traversal 19 | .graph 20 | .db 21 | .labelFilter(model.label, traversal) 22 | .setConverter[E with Entity, Converter[E with Entity, G]](model.converter.asInstanceOf[Converter[E with Entity, G]]) 23 | 24 | def get(idOrName: EntityIdOrName)(implicit graph: Graph): Traversal[E with Entity, G, Converter[E with Entity, G]] = 25 | idOrName.fold(getByIds(_), getByName) 26 | 27 | def getByIds(ids: EntityId*)(implicit graph: Graph): Traversal[E with Entity, G, Converter[E with Entity, G]] 28 | 29 | def getByName(name: String)(implicit graph: Graph): Traversal[E with Entity, G, Converter[E with Entity, G]] = 30 | throw InternalError(s"Entity ${model.label} cannot be retrieve by its name") 31 | 32 | def get(e: Entity)(implicit graph: Graph): Traversal[E with Entity, G, Converter[E with Entity, G]] = getByIds(e._id) 33 | 34 | def count(implicit graph: Graph): Long = startTraversal.getCount 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/EventSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services 2 | 3 | import akka.actor.{ActorRef, ActorSystem} 4 | import akka.cluster.pubsub.DistributedPubSub 5 | import akka.cluster.pubsub.DistributedPubSubMediator.{Publish, Subscribe, Unsubscribe} 6 | import akka.pattern.{ask => akkaAsk} 7 | import akka.util.Timeout 8 | import javax.inject.{Inject, Singleton} 9 | import play.api.Logger 10 | 11 | import scala.concurrent.Future 12 | 13 | @Singleton 14 | class EventSrv @Inject() (system: ActorSystem) { 15 | lazy val logger: Logger = Logger(getClass) 16 | private val mediator = DistributedPubSub(system).mediator 17 | 18 | def publish(topicName: String)(message: Any): Unit = { 19 | logger.debug(s"publish $topicName $message") 20 | mediator ! Publish(topicName, message) 21 | } 22 | 23 | def publishAsk(topicName: String)(message: Any)(implicit timeout: Timeout): Future[Any] = { 24 | logger.debug(s"publish $topicName $message") 25 | mediator ? Publish(topicName, message) 26 | } 27 | 28 | def subscribe(topicName: String, actor: ActorRef): Unit = mediator ! Subscribe(topicName, actor) 29 | 30 | def unsubscribe(topicName: String, actor: ActorRef): Unit = mediator ! Unsubscribe(topicName, actor) 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/ModelSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services 2 | 3 | import javax.inject.{Inject, Provider, Singleton} 4 | import org.thp.scalligraph.models.Model 5 | import play.api.Logger 6 | 7 | import scala.collection.immutable 8 | 9 | @Singleton 10 | class ModelSrv @Inject() (modelsProvider: Provider[immutable.Set[Model]]) { 11 | private[ModelSrv] lazy val logger: Logger = Logger(getClass) 12 | 13 | lazy val models: Set[Model] = modelsProvider.get 14 | private[ModelSrv] lazy val modelMap = models.map(m => m.label -> m).toMap 15 | def apply(modelName: String): Option[Model] = modelMap.get(modelName) 16 | val list: Set[Model] = models 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/VertexSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Vertex 4 | import org.thp.scalligraph.auth.AuthContext 5 | import org.thp.scalligraph.models._ 6 | import org.thp.scalligraph.query.PropertyUpdater 7 | import org.thp.scalligraph.traversal.TraversalOps._ 8 | import org.thp.scalligraph.traversal.{Converter, Graph, IdentityConverter, Traversal} 9 | import org.thp.scalligraph.{EntityId, EntityIdOrName, NotFoundError, RichOptionTry, RichTryOption} 10 | import play.api.libs.json.JsObject 11 | 12 | import java.util.Date 13 | import scala.collection.Iterator 14 | import scala.util.{Failure, Success, Try} 15 | 16 | abstract class VertexSrv[V <: Product](implicit val model: Model.Vertex[V]) extends ElementSrv[V, Vertex] { 17 | 18 | override def startTraversal(implicit graph: Graph): Traversal[V with Entity, Vertex, Converter[V with Entity, Vertex]] = 19 | graph.V[V]()(model) 20 | 21 | def pagedTraversal[R](db: Database, pageSize: Int, filter: Traversal.V[V] => Traversal.V[V] = identity)( 22 | process: Traversal.V[V] => Option[Try[R]] 23 | ): Iterator[Try[R]] = 24 | pagedTraversalIds(db, pageSize, filter) { ids => 25 | db.tryTransaction { implicit graph => 26 | process(getByIds(ids: _*)).flip 27 | }.flip 28 | } 29 | def pagedTraversalIds[R](db: Database, pageSize: Int, filter: Traversal.V[V] => Traversal.V[V] = identity)( 30 | process: Seq[EntityId] => Option[R] 31 | ): Iterator[R] = 32 | db.pagedTraversalIds[R]( 33 | pageSize, 34 | filter 35 | .compose[Traversal.Identity[Vertex]]( 36 | db.labelFilter(model.label, _).setConverter[V with Entity, Converter[V with Entity, Vertex]](model.converter) 37 | ) 38 | .andThen(_.unsetConverter) 39 | )(process) 40 | 41 | // override def startTraversal(strategy: GraphStrategy)(implicit graph: Graph): Traversal.V[V] = 42 | // filterTraversal(Traversal.strategedV(strategy)) 43 | 44 | override def getByIds(ids: EntityId*)(implicit graph: Graph): Traversal.V[V] = 45 | if (ids.isEmpty) graph.empty 46 | else graph.V[V](ids: _*)(model) 47 | 48 | def get(vertex: Vertex)(implicit graph: Graph): Traversal.V[V] = 49 | graph.V[V](EntityId(vertex.id()))(model) 50 | 51 | def getOrFail(idOrName: EntityIdOrName)(implicit graph: Graph): Try[V with Entity] = 52 | get(idOrName) 53 | .headOption 54 | .fold[Try[V with Entity]](Failure(NotFoundError(s"${model.label} $idOrName not found")))(Success.apply) 55 | 56 | def getOrFail(vertex: Vertex)(implicit graph: Graph): Try[V with Entity] = 57 | get(vertex) 58 | .headOption 59 | .fold[Try[V with Entity]](Failure(NotFoundError(s"${model.label} ${vertex.id()} not found")))(Success.apply) 60 | 61 | def createEntity(e: V)(implicit graph: Graph, authContext: AuthContext): Try[V with Entity] = 62 | Success(graph.db.createVertex[V](graph, authContext, model, e)) 63 | 64 | def exists(e: V)(implicit graph: Graph): Boolean = false 65 | 66 | def update( 67 | traversalSelect: Traversal[V with Entity, Vertex, Converter[V with Entity, Vertex]] => Traversal[ 68 | V with Entity, 69 | Vertex, 70 | Converter[V with Entity, Vertex] 71 | ], 72 | propertyUpdaters: Seq[PropertyUpdater] 73 | )(implicit graph: Graph, authContext: AuthContext): Try[(Traversal.V[V], JsObject)] = 74 | update(traversalSelect(startTraversal), propertyUpdaters) 75 | 76 | def update(traversal: Traversal.V[V], propertyUpdaters: Seq[PropertyUpdater])(implicit 77 | graph: Graph, 78 | authContext: AuthContext 79 | ): Try[(Traversal[V with Entity, Vertex, Converter[V with Entity, Vertex]], JsObject)] = { 80 | val myClone = traversal.clone() 81 | traversal.debug("update") 82 | traversal 83 | .setConverter[Vertex, IdentityConverter[Vertex]](Converter.identity) 84 | .headOption 85 | .fold[Try[(Traversal.V[V], JsObject)]](Failure(NotFoundError(s"${model.label} not found"))) { vertex => 86 | logger.trace(s"Update ${vertex.id()} by ${authContext.userId}") 87 | propertyUpdaters 88 | .toTry(u => u(vertex, graph, authContext)) 89 | .map { o => 90 | graph.db.updatedAtMapping.setProperty(vertex, "_updatedAt", Some(new Date)) 91 | graph.db.updatedByMapping.setProperty(vertex, "_updatedBy", Some(authContext.userId)) 92 | myClone -> o.reduceOption(_ ++ _).getOrElse(JsObject.empty) 93 | } 94 | } 95 | } 96 | 97 | def delete(e: V with Entity)(implicit graph: Graph, authContext: AuthContext): Try[Unit] = 98 | Try(get(e).remove()) 99 | } 100 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/config/ConfigActor.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services.config 2 | 3 | import akka.actor.{Actor, ActorRef} 4 | import javax.inject.Inject 5 | import org.thp.scalligraph.services.EventSrv 6 | 7 | class ConfigActor @Inject() (eventSrv: EventSrv) extends Actor { 8 | 9 | override def preStart(): Unit = { 10 | eventSrv.subscribe(ConfigTopic.topicName, self) 11 | super.preStart() 12 | } 13 | 14 | override def receive: Receive = receive(Nil) 15 | 16 | def receive(clients: List[(String, ActorRef)]): Receive = { 17 | case WaitNotification(path) => context.become(receive((path -> sender()) :: clients)) 18 | case msg @ Invalidate(path) => 19 | val (clientsToBeNotified, otherClients) = clients.partition(_._1 == path) 20 | clientsToBeNotified.foreach(_._2 ! msg) 21 | context.become(receive(otherClients)) 22 | } 23 | } 24 | 25 | object ConfigTopic { 26 | val topicName: String = "config" 27 | } 28 | sealed trait ConfigMessage 29 | case class WaitNotification(path: String) extends ConfigMessage 30 | case class Invalidate(path: String) extends ConfigMessage 31 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/config/ConfigItem.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services.config 2 | 3 | import akka.actor.ActorRef 4 | import akka.pattern.ask 5 | import org.thp.scalligraph.BadConfigurationError 6 | import org.thp.scalligraph.auth.AuthContext 7 | import org.thp.scalligraph.models.Database 8 | import org.thp.scalligraph.services.EventSrv 9 | import play.api.Logger 10 | import play.api.libs.json.{Format, JsObject, JsValue, Json} 11 | 12 | import scala.compat.java8.OptionConverters._ 13 | import scala.concurrent.ExecutionContext 14 | import scala.concurrent.duration.DurationInt 15 | import scala.util.{Failure, Success, Try} 16 | 17 | trait ConfigItem[B, F] { 18 | val path: String 19 | val description: String 20 | val defaultValue: B 21 | val jsonFormat: Format[B] 22 | def get: F 23 | def set(v: B)(implicit authContext: AuthContext): Try[Unit] 24 | def validation(v: B): Try[B] 25 | def getDefaultValueJson: JsValue = jsonFormat.writes(defaultValue) 26 | def getJson: JsValue 27 | 28 | def setJson(v: JsValue)(implicit authContext: AuthContext): Try[Unit] = 29 | jsonFormat 30 | .reads(v) 31 | .map(b => set(b)) 32 | .fold( 33 | error => { 34 | val message = JsObject(error.map { 35 | case (path, es) => path.toString -> Json.toJson(es.flatMap(_.messages)) 36 | }) 37 | Failure(BadConfigurationError(message.toString)) 38 | }, 39 | identity 40 | ) 41 | def onUpdate(f: (B, B) => Unit): Unit 42 | } 43 | 44 | class ConfigItemImpl[B, F]( 45 | val path: String, 46 | val description: String, 47 | val defaultValue: B, 48 | val jsonFormat: Format[B], 49 | val validationFunction: B => Try[B], 50 | mapFunction: B => F, 51 | db: Database, 52 | eventSrv: EventSrv, 53 | configActor: ActorRef, 54 | implicit val ec: ExecutionContext 55 | ) extends ConfigItem[B, F] { 56 | lazy val logger: Logger = Logger(getClass) 57 | private var fValue: F = _ 58 | private var bValue: B = _ 59 | @volatile private var flag = false 60 | private var updateCallbacks: List[(B, B) => Unit] = Nil 61 | 62 | invalidateCache(Success(())) 63 | 64 | private def invalidateCache(msg: Try[Any]): Unit = { 65 | msg.foreach { 66 | case Invalidate(_) => 67 | val oldValue = bValue 68 | bValue = getValue.getOrElse(defaultValue) 69 | fValue = mapFunction(bValue) 70 | updateCallbacks.foreach(_.apply(oldValue, bValue)) 71 | case _ => 72 | } 73 | configActor 74 | .ask(WaitNotification(path))(1.hour) 75 | .onComplete { msg => 76 | logger.debug(s"Receive message from config actor: $msg") 77 | invalidateCache(msg) 78 | } 79 | } 80 | 81 | protected def getValue: Option[B] = 82 | db.roTransaction { implicit graph => 83 | graph 84 | .variables 85 | .get[String](s"config.$path") 86 | }.asScala 87 | .flatMap(s => Try(Json.parse(s)).toOption) 88 | .flatMap(jsonFormat.reads(_).asOpt) 89 | 90 | private def retrieveValues(): Unit = 91 | synchronized { 92 | if (!flag) { 93 | bValue = getValue.getOrElse(defaultValue) 94 | fValue = mapFunction(bValue) 95 | flag = true 96 | } 97 | } 98 | 99 | override def get: F = { 100 | if (!flag) retrieveValues() 101 | fValue 102 | } 103 | 104 | override def getJson: JsValue = { 105 | if (!flag) retrieveValues() 106 | jsonFormat.writes(bValue) 107 | } 108 | 109 | override def set(v: B)(implicit authContext: AuthContext): Try[Unit] = 110 | validation(v).flatMap { value => 111 | val valueJson = jsonFormat.writes(value) 112 | db.tryTransaction { implicit graph => 113 | Try( 114 | graph 115 | .variables 116 | .set(s"config.$path", valueJson.toString) 117 | ) 118 | }.map(_ => eventSrv.publish(ConfigTopic.topicName)(Invalidate(path))) 119 | } 120 | 121 | override def validation(v: B): Try[B] = validationFunction(v) 122 | 123 | override def onUpdate(f: (B, B) => Unit): Unit = 124 | synchronized { 125 | updateCallbacks = f :: updateCallbacks 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/config/ConfigSerializer.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services.config 2 | 3 | import java.io.NotSerializableException 4 | 5 | import akka.serialization.Serializer 6 | 7 | class ConfigSerializer extends Serializer { 8 | override def identifier: Int = 226591534 9 | 10 | override def includeManifest: Boolean = false 11 | 12 | /** 13 | * Serializes the given object into an Array of Byte 14 | */ 15 | def toBinary(o: AnyRef): Array[Byte] = 16 | o match { 17 | case WaitNotification(path) => s"W$path".getBytes 18 | case Invalidate(path) => s"I$path".getBytes 19 | case _ => Array.empty[Byte] // Not serializable 20 | } 21 | 22 | /** 23 | * Produces an object from an array of bytes, with an optional type-hint; 24 | * the class should be loaded using ActorSystem.dynamicAccess. 25 | */ 26 | @throws(classOf[NotSerializableException]) 27 | def fromBinary(bytes: Array[Byte], manifest: Option[Class[_]]): AnyRef = { 28 | val s = new String(bytes) 29 | s(0) match { 30 | case 'W' => WaitNotification(s.tail) 31 | case 'I' => Invalidate(s.tail) 32 | case _ => throw new NotSerializableException 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/services/config/ContextConfigItem.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.services.config 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import org.thp.scalligraph.BadConfigurationError 5 | import org.thp.scalligraph.auth.AuthContext 6 | import org.thp.scalligraph.models.Database 7 | import org.thp.scalligraph.services.EventSrv 8 | import play.api.libs.json.{Format, JsObject, JsValue, Json} 9 | 10 | import scala.compat.java8.OptionConverters._ 11 | import scala.concurrent.ExecutionContext 12 | import scala.util.{Failure, Try} 13 | 14 | trait ConfigContext[C] { 15 | def defaultPath(path: String): String 16 | def getValue(context: C, path: String): Option[JsValue] 17 | def setValue(context: C, path: String, value: JsValue): Try[String] 18 | } 19 | 20 | @Singleton 21 | class GlobalConfigContext @Inject() (db: Database) extends ConfigContext[Unit] { 22 | override def defaultPath(path: String): String = path 23 | 24 | override def getValue(context: Unit, path: String): Option[JsValue] = 25 | db.roTransaction { implicit graph => 26 | graph 27 | .variables 28 | .get[String](s"config.$path") 29 | }.asScala 30 | .map(Json.parse) 31 | 32 | override def setValue(context: Unit, path: String, value: JsValue): Try[String] = 33 | db.tryTransaction { implicit graph => 34 | Try( 35 | graph 36 | .variables 37 | .set(s"config.$path", value.toString) 38 | ) 39 | }.map(_ => path) 40 | } 41 | 42 | trait ContextConfigItem[T, C] { 43 | val context: ConfigContext[C] 44 | val path: String 45 | val description: String 46 | val defaultValue: T 47 | val jsonFormat: Format[T] 48 | def get(context: C): T 49 | def set(context: C, v: T)(implicit authContext: AuthContext): Try[Unit] 50 | def validation(v: T): Try[T] 51 | def getDefaultValueJson: JsValue = jsonFormat.writes(defaultValue) 52 | def getJson(context: C): JsValue = jsonFormat.writes(get(context)) 53 | 54 | def setJson(context: C, v: JsValue)(implicit authContext: AuthContext): Try[Unit] = 55 | jsonFormat 56 | .reads(v) 57 | .map(set(context, _)) 58 | .fold( 59 | error => { 60 | val message = JsObject(error.map { 61 | case (path, es) => path.toString -> Json.toJson(es.flatMap(_.messages)) 62 | }) 63 | Failure(BadConfigurationError(message.toString)) 64 | }, 65 | identity 66 | ) 67 | } 68 | 69 | class ContextConfigItemImpl[T, C]( 70 | val context: ConfigContext[C], 71 | val path: String, 72 | val description: String, 73 | val defaultValue: T, 74 | val jsonFormat: Format[T], 75 | val validationFunction: T => Try[T], 76 | eventSrv: EventSrv, 77 | implicit val ec: ExecutionContext 78 | ) extends ContextConfigItem[T, C] { 79 | 80 | private var value: T = _ 81 | @volatile private var flag = false 82 | 83 | override def get(ctx: C): T = { 84 | if (!flag) 85 | synchronized { 86 | if (!flag) 87 | value = context 88 | .getValue(ctx, path) 89 | .flatMap(s => jsonFormat.reads(s).asOpt) 90 | .getOrElse(defaultValue) 91 | flag = true 92 | } 93 | value 94 | } 95 | 96 | override def set(ctx: C, v: T)(implicit authContext: AuthContext): Try[Unit] = 97 | validation(v).flatMap { value => 98 | val valueJson = jsonFormat.writes(value) 99 | context 100 | .setValue(ctx, path, valueJson) 101 | .map(p => eventSrv.publish(ConfigTopic.topicName)(Invalidate(p))) 102 | } 103 | override def validation(v: T): Try[T] = validationFunction(v) 104 | } 105 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/BranchSelector.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 4 | import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent.Pick 5 | 6 | class BranchSelector[D, G, C <: Converter[D, G], GG](traversal: Traversal[D, G, C]) { 7 | def on[S](f: Traversal[D, G, C] => Traversal[_, S, _]) = new BranchSelectorOn[D, G, C, S, GG](traversal, f, Nil) 8 | } 9 | 10 | class BranchSelectorOn[D, G, C <: Converter[D, G], S, GG]( 11 | traversal: Traversal[D, G, C], 12 | on: Traversal[D, G, C] => Traversal[_, S, _], 13 | options: Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[_, GG, _])] 14 | ) { 15 | def option(o: S, f: Traversal[D, G, C] => Traversal[_, GG, _]) = 16 | new BranchSelectorOn[D, G, C, S, GG]( 17 | traversal, 18 | on, 19 | options.asInstanceOf[Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[_, GG, _])]] :+ (Right(o) -> f) 20 | ) 21 | 22 | def any(f: Traversal[D, G, C] => Traversal[_, GG, _]) = 23 | new BranchSelectorOn[D, G, C, S, GG]( 24 | traversal, 25 | on, 26 | options.asInstanceOf[Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[_, GG, _])]] :+ (Left(Pick.any) -> f) 27 | ) 28 | 29 | def none(f: Traversal[D, G, C] => Traversal[_, GG, _]) = 30 | new BranchSelectorOn[D, G, C, S, GG]( 31 | traversal, 32 | on, 33 | options.asInstanceOf[Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[_, GG, _])]] :+ (Left(Pick.none) -> f) 34 | ) 35 | 36 | private[traversal] def build: Traversal[GG, GG, IdentityConverter[GG]] = 37 | traversal.onRawMap[GG, GG, IdentityConverter[GG]](gt => 38 | options 39 | .foldLeft(gt.choose(on(traversal.start).raw)) { 40 | case (acc, (Left(pick), t)) => acc.option(pick, t(traversal.start).raw) 41 | case (acc, (Right(value), t)) => acc.option(value, t(traversal.start).raw) 42 | } 43 | .asInstanceOf[GraphTraversal[_, GG]] 44 | )(Converter.identity[GG]) 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/IteratorOutput.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import org.thp.scalligraph.controllers.{Output, Renderer} 4 | import org.thp.scalligraph.traversal.TraversalOps._ 5 | import play.api.libs.json.{JsArray, JsValue} 6 | 7 | class IteratorOutput(val iterator: Iterator[JsValue], val totalSize: Option[() => Long]) extends Output[JsValue] { 8 | override def toValue: JsValue = toJson 9 | override def toJson: JsValue = JsArray(iterator.toSeq) 10 | } 11 | 12 | object IteratorOutput { 13 | def apply[V](traversal: Traversal[V, _, _], totalSize: Option[() => Long] = None)(implicit renderer: Renderer[V]) = 14 | new IteratorOutput(traversal.cast[V, Any].toIterator.map(renderer.toJson), totalSize) 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/MatchElement.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.{__, GraphTraversal} 4 | 5 | object MatchElementBuilder { 6 | def as[FD, FG, FC <: Converter[FD, FG]](fromLabel: StepLabel[FD, FG, FC]): MatchElementAs[FD, FG, FC] = 7 | new MatchElementAs[FD, FG, FC](fromLabel) 8 | def dedup /*[FD, FG, FC <: Converter[FD, FG]]*/ (labels: StepLabel[_, _, _]* /*[FD, FG, FC]*/ ): MatchElementDedup = 9 | new MatchElementDedup(labels.map(_.name)) 10 | } 11 | 12 | class MatchElementAs[FD, FG, FC <: Converter[FD, FG]](fromLabel: StepLabel[FD, FG, FC]) { 13 | def apply[TD, TG, TC <: Converter[TD, TG]](f: Traversal[FD, FG, FC] => Traversal[TD, TG, TC]) = 14 | new MatchElementTraversal[FD, FG, FC, TD, TG, TC](fromLabel, f) 15 | } 16 | 17 | class MatchElementTraversal[FD, FG, FC <: Converter[FD, FG], TD, TG, TC <: Converter[TD, TG]]( 18 | fromLabel: StepLabel[FD, FG, FC], 19 | f: Traversal[FD, FG, FC] => Traversal[TD, TG, TC] 20 | ) { 21 | def as(toLabel: StepLabel[TD, TG, TC]) = new MatchElementTraversalAs[FD, FG, FC, TD, TG, TC](fromLabel, f, toLabel) 22 | } 23 | 24 | trait MatchElement { 25 | private[traversal] def traversal: GraphTraversal[_, _] 26 | } 27 | 28 | class MatchElementTraversalAs[FD, FG, FC <: Converter[FD, FG], TD, TG, TC <: Converter[TD, TG]]( 29 | val fromLabel: StepLabel[FD, FG, FC], 30 | val f: Traversal[FD, FG, FC] => Traversal[TD, TG, TC], 31 | val toLabel: StepLabel[TD, TG, TC] 32 | ) extends MatchElement { 33 | 34 | import TraversalOps._ 35 | 36 | private[traversal] def traversal: GraphTraversal[_, _] = f(fromLabel.converter.startTraversal.as(fromLabel)).as(toLabel).raw 37 | } 38 | 39 | class MatchElementDedup(labels: Seq[String]) extends MatchElement { 40 | override private[traversal] def traversal: GraphTraversal[_, _] = __.start().dedup(labels: _*) 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/ProjectionBuilder.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import java.util.{UUID, Map => JMap} 4 | 5 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal 6 | import org.apache.tinkerpop.gremlin.structure.Element 7 | import org.thp.scalligraph.`macro`.TraversalMacro 8 | import org.thp.scalligraph.models.{Entity, Mapping} 9 | import shapeless.ops.tuple.Prepend 10 | import shapeless.syntax.std.tuple._ 11 | 12 | import scala.language.experimental.macros 13 | 14 | class ProjectionBuilder[E <: Product, D, G, C <: Converter[D, G]]( 15 | traversal: Traversal[D, G, C], 16 | labels: Seq[String], 17 | addBy: GraphTraversal[_, JMap[String, Any]] => GraphTraversal[_, JMap[String, Any]], 18 | buildResult: JMap[String, Any] => E 19 | ) { 20 | // def apply[U, TR <: Product](by: By[U])(implicit prepend: Prepend.Aux[E, Tuple1[U], TR]): ProjectionBuilder[TR, T] = { 21 | // val label = UUID.randomUUID().toString 22 | // new ProjectionBuilder[TR, T](traversal, labels :+ label, addBy.andThen(by.apply), map => buildResult(map) :+ map.get(label).asInstanceOf[U]) 23 | // } 24 | 25 | def by[TR <: Product](implicit prepend: Prepend.Aux[E, Tuple1[D], TR]): ProjectionBuilder[TR, D, G, C] = { 26 | val label = UUID.randomUUID().toString 27 | new ProjectionBuilder[TR, D, G, C]( 28 | traversal, 29 | labels :+ label, 30 | addBy.andThen(_.by), 31 | map => buildResult(map) :+ traversal.converter(map.get(label).asInstanceOf[G]) 32 | ) 33 | } 34 | 35 | // def by[U, TR <: Product](key: Key[U])(implicit prepend: Prepend.Aux[E, Tuple1[U], TR]): ProjectionBuilder[TR, D, G] = { 36 | // val label = UUID.randomUUID().toString 37 | // new ProjectionBuilder[TR, D, G]( 38 | // traversal, 39 | // labels :+ label, 40 | // addBy.andThen(_.by(key.name)), 41 | // map => buildResult(map) :+ map.get(label).asInstanceOf[U] 42 | // ) 43 | // } 44 | def byValue[DD, DU, TR <: Product]( 45 | selector: D => DD 46 | )(implicit 47 | mapping: Mapping[DD, DU, _], 48 | ev1: D <:< Product with Entity, 49 | ev2: G <:< Element, 50 | prepend: Prepend.Aux[E, Tuple1[DU], TR] 51 | ): ProjectionBuilder[TR, D, G, C] = macro TraversalMacro.projectionBuilderByValue[DD, DU, TR] 52 | 53 | def _byValue[DD, GG, TR <: Product](name: String, converter: Converter[DD, GG])(implicit 54 | prepend: Prepend.Aux[E, Tuple1[DD], TR] 55 | ): ProjectionBuilder[TR, D, G, C] = { 56 | val label = UUID.randomUUID().toString 57 | new ProjectionBuilder[TR, D, G, C]( 58 | traversal, 59 | labels :+ label, 60 | addBy.andThen(_.by(name)), 61 | map => buildResult(map) :+ converter(map.get(label).asInstanceOf[GG]) 62 | ) 63 | } 64 | 65 | def by[DD, GG, TR <: Product]( 66 | f: Traversal[D, G, C] => Traversal[DD, GG, _] 67 | )(implicit prepend: Prepend.Aux[E, Tuple1[DD], TR]): ProjectionBuilder[TR, D, G, C] = { 68 | val label = UUID.randomUUID().toString 69 | val p = f(traversal.start).asInstanceOf[Traversal[DD, GG, Converter[DD, GG]]] 70 | new ProjectionBuilder[TR, D, G, C]( 71 | traversal, 72 | labels :+ label, 73 | addBy.andThen(_.by(p.raw)), 74 | map => buildResult(map) :+ p.converter(map.get(label).asInstanceOf[GG]) 75 | ) 76 | } 77 | 78 | private[traversal] def traversal(g: GraphTraversal[_, _]): GraphTraversal[_, JMap[String, Any]] = addBy(g.project(labels.head, labels.tail: _*)) 79 | private[traversal] def converter: Converter[E, JMap[String, Any]] = buildResult(_) 80 | } 81 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/StepLabel.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import java.util.{UUID, List => JList} 4 | 5 | import org.apache.tinkerpop.gremlin.structure.{Edge, Vertex} 6 | import org.thp.scalligraph.InternalError 7 | import org.thp.scalligraph.models.{Entity, Model} 8 | 9 | class StepLabel[D, G, C <: Converter[D, G]](private var _converter: Option[C]) { 10 | def this() = this(None) 11 | def this(converter: C) = this(Some(converter)) 12 | val name: String = UUID.randomUUID().toString 13 | 14 | def converter: C = _converter.getOrElse(throw InternalError(s"StepLabel $name is use before set")) 15 | def setConverter(conv: C): Unit = _converter = Some(conv) 16 | } 17 | 18 | object StepLabel { 19 | def apply[D, G, C <: Converter[D, G]]: StepLabel[D, G, C] = new StepLabel[D, G, C] 20 | def v[V <: Product](implicit model: Model.Vertex[V]): StepLabel[V with Entity, Vertex, Converter[V with Entity, Vertex]] = 21 | new StepLabel[V with Entity, Vertex, Converter[V with Entity, Vertex]](model.converter) 22 | def vs[V <: Product]( 23 | implicit model: Model.Vertex[V] 24 | ): StepLabel[Seq[V with Entity], JList[Vertex], Converter.CList[V with Entity, Vertex, Converter[V with Entity, Vertex]]] = 25 | new StepLabel[Seq[V with Entity], JList[Vertex], Converter.CList[V with Entity, Vertex, Converter[V with Entity, Vertex]]]( 26 | Converter.clist[V with Entity, Vertex, Converter[V with Entity, Vertex]](model.converter) 27 | ) 28 | def e[E <: Product](implicit model: Model.Edge[E]): StepLabel[E with Entity, Edge, Converter[E with Entity, Edge]] = 29 | new StepLabel[E with Entity, Edge, Converter[E with Entity, Edge]](model.converter) 30 | def identity[E]: StepLabel[E, E, Converter.Identity[E]] = new StepLabel[E, E, IdentityConverter[E]](Converter.identity[E]) 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/Traversal.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.Traverser 4 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.{__, DefaultGraphTraversal, GraphTraversal} 5 | import org.apache.tinkerpop.gremlin.structure.{Edge, Vertex} 6 | import org.thp.scalligraph.models.Entity 7 | 8 | import scala.language.existentials 9 | 10 | object Traversal { 11 | type Identity[T] = Traversal[T, T, Converter.Identity[T]] 12 | type V[T] = Traversal[T with Entity, Vertex, Converter[T with Entity, Vertex]] 13 | type E[T] = Traversal[T with Entity, Edge, Converter[T with Entity, Edge]] 14 | type Unk = Traversal[UnkD, UnkG, Converter[UnkD, UnkG]] 15 | type UnkD = Any 16 | type UnkDU = Any 17 | type UnkG = Any 18 | 19 | type Some = Traversal[D, G, C] forSome { type D; type G; type C <: Converter[D, G] } 20 | type SomeDomain[D] = Traversal[D, G, C] forSome { type G; type C <: Converter[D, G] } 21 | type Domain[D] = Traversal[D, UnkG, Converter[D, UnkG]] 22 | } 23 | 24 | class Traversal[+D, G, +C <: Converter[D, G]](val graph: Graph, val raw: GraphTraversal[_, G], val converter: C) { 25 | def onRaw(f: GraphTraversal[_, G] => GraphTraversal[_, G]): Traversal[D, G, C] = 26 | new Traversal[D, G, C](graph, f(raw), converter) 27 | def onRawMap[DD, GG, CC <: Converter[DD, GG]](f: GraphTraversal[_, G] => GraphTraversal[_, GG])(conv: CC): Traversal[DD, GG, CC] = 28 | new Traversal[DD, GG, CC](graph, f(raw), conv) 29 | def domainMap[DD](f: D => DD): Traversal[DD, G, Converter[DD, G]] = 30 | new Traversal[DD, G, Converter[DD, G]](graph, raw, g => converter.andThen(f).apply(g)) 31 | def graphMap[DD, GG, CC <: Converter[DD, GG]](d: G => GG, conv: CC): Traversal[DD, GG, CC] = 32 | new Traversal[DD, GG, CC](graph, raw.map[GG]((t: Traverser[G]) => d(t.get)), conv) 33 | def setConverter[DD, CC <: Converter[DD, G]](conv: CC): Traversal[DD, G, CC] = new Traversal[DD, G, CC](graph, raw, conv) 34 | def unsetConverter = new Traversal[G, G, IdentityConverter[G]](graph, raw, Converter.identity) 35 | def start = new Traversal[D, G, C](graph, __.start[G](), converter) 36 | def mapAsNumber( 37 | f: Traversal[Number, Number, IdentityConverter[Number]] => Traversal[Number, Number, IdentityConverter[Number]] 38 | ): Traversal[D, G, C] = 39 | f(this.asInstanceOf[Traversal[Number, Number, IdentityConverter[Number]]]).asInstanceOf[Traversal[D, G, C]] 40 | def mapAsComparable(f: Traversal[Comparable[_], Comparable[G], _] => Traversal[Comparable[_], Comparable[G], _]): Traversal[D, G, C] = 41 | f(this.asInstanceOf[Traversal[Comparable[_], Comparable[G], _]]).asInstanceOf[Traversal[D, G, C]] 42 | override def clone(): Traversal[D, G, C] = 43 | raw match { 44 | case dgt: DefaultGraphTraversal[_, G] => new Traversal[D, G, C](graph, dgt.clone, converter) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/traversal/ValueSelector.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.traversal 2 | 3 | import java.util.{Map => JMap} 4 | 5 | import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.{__, GraphTraversal} 6 | import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent.Pick 7 | 8 | class ValueSelector[D, G, C <: Converter[D, G]](traversal: Traversal[D, G, C]) { 9 | def on[S](f: Traversal[D, G, C] => Traversal[_, S, _]) = new ValueSelectorOn[D, G, C, S, Nothing](traversal, f, Nil) 10 | } 11 | 12 | class ValueSelectorOn[D, G, C <: Converter[D, G], S, DD]( 13 | traversal: Traversal[D, G, C], 14 | on: Traversal[D, G, C] => Traversal[_, S, _], 15 | options: Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[DD, _, _])] 16 | ) { 17 | private lazy val optionsTraversals: Seq[(Either[Pick, S], GraphTraversal[_, JMap[String, Any]], Converter[DD, Any])] = 18 | options 19 | .zipWithIndex 20 | .map { 21 | case ((p, t), i) => 22 | val nt = t(traversal.start).asInstanceOf[Traversal[DD, Any, Converter[DD, Any]]] 23 | ( 24 | p, 25 | __.start().project("chooseIndex", "chooseValue").by(__.start().constant(i)).by(nt.raw).asInstanceOf[GraphTraversal[_, JMap[String, Any]]], 26 | nt.converter 27 | ) 28 | } 29 | 30 | def option[OD >: DD, GG, CC <: Converter[OD, GG]](o: S, f: Traversal[D, G, C] => Traversal[OD, GG, CC]) = 31 | new ValueSelectorOn[D, G, C, S, OD]( 32 | traversal, 33 | on, 34 | options.asInstanceOf[Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[OD, _, _])]] :+ (Right(o) -> f) 35 | ) 36 | 37 | def any[OD >: DD, GG, CC <: Converter[OD, GG]](f: Traversal[D, G, C] => Traversal[OD, GG, CC]) = 38 | new ValueSelectorOn[D, G, C, S, OD]( 39 | traversal, 40 | on, 41 | options.asInstanceOf[Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[OD, _, _])]] :+ (Left(Pick.any) -> f) 42 | ) 43 | 44 | def none[OD >: DD, GG, CC <: Converter[OD, GG]](f: Traversal[D, G, C] => Traversal[OD, GG, CC]) = 45 | new ValueSelectorOn[D, G, C, S, OD]( 46 | traversal, 47 | on, 48 | options.asInstanceOf[Seq[(Either[Pick, S], Traversal[D, G, C] => Traversal[OD, _, _])]] :+ (Left(Pick.none) -> f) 49 | ) 50 | 51 | private[traversal] def build: Traversal[DD, JMap[String, Any], Converter[DD, JMap[String, Any]]] = 52 | traversal.onRawMap[DD, JMap[String, Any], Converter[DD, JMap[String, Any]]](gt => 53 | optionsTraversals 54 | .foldLeft(gt.choose(on(traversal.start).raw)) { 55 | case (acc, (Left(pick), t, _)) => acc.option(pick, t) 56 | case (acc, (Right(value), t, _)) => acc.option(value, t) 57 | } 58 | .asInstanceOf[GraphTraversal[_, JMap[String, Any]]] 59 | ) { (jmap: JMap[String, Any]) => 60 | val index = jmap.get("chooseIndex").asInstanceOf[Int] 61 | val converter = optionsTraversals(index)._3 62 | converter(jmap.get("chooseValue")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/utils/FunctionalCondition.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.utils 2 | 3 | object FunctionalCondition { 4 | implicit class When[A](a: A) { 5 | def whenValue(cond: A => Boolean)(f: A => A): A = if (cond(a)) f(a) else a 6 | def when(cond: Boolean)(f: A => A): A = if (cond) f(a) else a 7 | def merge[B](opt: Option[B])(f: (A, B) => A): A = opt.fold(a)(f(a, _)) 8 | } 9 | 10 | implicit class When2[A, B](ab: (A, B)) { 11 | def when(cond: Boolean)(fa: A => A, fb: B => B): (A, B) = if (cond) (fa(ab._1), fb(ab._2)) else ab 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/utils/Hash.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.utils 2 | 3 | import java.io.InputStream 4 | import java.nio.charset.Charset 5 | import java.nio.file.{Files, Path, Paths} 6 | import java.security.MessageDigest 7 | 8 | import akka.stream.Materializer 9 | import akka.stream.scaladsl.{FileIO, Source} 10 | import akka.util.ByteString 11 | import play.api.libs.json.{Format, JsString, Reads, Writes} 12 | 13 | import scala.concurrent.duration.DurationInt 14 | import scala.concurrent.{Await, ExecutionContext, Future} 15 | 16 | case class Hasher(algorithms: String*) { 17 | 18 | val bufferSize = 4096 19 | 20 | def fromPath(path: Path): Seq[Hash] = { 21 | val is = Files.newInputStream(path) 22 | try fromInputStream(is) 23 | finally is.close() 24 | } 25 | 26 | def fromInputStream(is: InputStream): Seq[Hash] = { 27 | val mds = algorithms.map(algo => MessageDigest.getInstance(algo)) 28 | def readNextBuffer: Array[Byte] = { 29 | val buffer = Array.ofDim[Byte](bufferSize) 30 | val len = is.read(buffer) 31 | if (len == bufferSize) buffer else buffer.take(len) 32 | } 33 | 34 | Iterator 35 | .continually(readNextBuffer) 36 | .takeWhile(_.nonEmpty) 37 | .foreach(buffer => mds.foreach(md => md.update(buffer))) 38 | mds.map(md => Hash(md.digest())) 39 | } 40 | 41 | def fromString(data: String): Seq[Hash] = fromBinary(data.getBytes(Charset.forName("UTF8"))) 42 | 43 | def fromBinary(data: Array[Byte]): Seq[Hash] = { 44 | val mds = algorithms.map(algo => MessageDigest.getInstance(algo)) 45 | mds.map(md => Hash(md.digest(data))) 46 | } 47 | 48 | def fromBinary(data: Source[ByteString, _])(implicit mat: Materializer): Seq[Hash] = { 49 | val mds = algorithms.map(algo => MessageDigest.getInstance(algo)) 50 | Await.ready(data.runForeach(bs => mds.foreach(_.update(bs.toByteBuffer))), 5.minutes) 51 | mds.map(md => Hash(md.digest())) 52 | } 53 | } 54 | 55 | class MultiHash(algorithms: String)(implicit mat: Materializer, ec: ExecutionContext) { 56 | private val md = MessageDigest.getInstance(algorithms) 57 | 58 | def addValue(value: String): Unit = { 59 | md.update(0.asInstanceOf[Byte]) 60 | md.update(value.getBytes) 61 | } 62 | def addFile(filename: String): Future[Unit] = addFile(Paths.get(filename)) 63 | 64 | def addFile(file: Path): Future[Unit] = { 65 | md.update(0.asInstanceOf[Byte]) 66 | FileIO 67 | .fromPath(file) 68 | .runForeach(bs => md.update(bs.toByteBuffer)) 69 | .map(_ => ()) 70 | } 71 | 72 | def addSource(source: Source[ByteString, _]): Future[Unit] = 73 | source 74 | .runForeach(bs => md.update(bs.toByteBuffer)) 75 | .map(_ => ()) 76 | def digest: Hash = Hash(md.digest()) 77 | } 78 | 79 | case class Hash(data: Array[Byte]) { 80 | override def toString: String = data.map(b => f"$b%02x").mkString 81 | 82 | override def equals(obj: scala.Any): Boolean = 83 | obj match { 84 | case Hash(d) => d.sameElements(data) 85 | case _ => false 86 | } 87 | } 88 | 89 | object Hash { 90 | 91 | def apply(s: String): Hash = 92 | Hash { 93 | s.grouped(2) 94 | .map { cc => 95 | (Character.digit(cc(0), 16) << 4 | Character.digit(cc(1), 16)).toByte 96 | } 97 | .toArray 98 | } 99 | 100 | val hashReads: Reads[Hash] = Reads(json => json.validate[String].map(h => Hash(h))) 101 | val hashWrites: Writes[Hash] = Writes[Hash](h => JsString(h.toString())) 102 | implicit val hashFormat: Format[Hash] = Format(hashReads, hashWrites) 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/utils/Instance.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.utils 2 | 3 | import java.rmi.dgc.VMID 4 | import java.util.concurrent.atomic.AtomicInteger 5 | 6 | import play.api.mvc.RequestHeader 7 | 8 | object Instance { 9 | val id: String = (new VMID).toString 10 | val counter = new AtomicInteger(0) 11 | def getRequestId(request: RequestHeader) = s"$id:${request.id}" 12 | def getInternalId = s"$id::${counter.incrementAndGet}" 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/utils/ProcessStats.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.utils 2 | 3 | import scala.collection.concurrent.TrieMap 4 | import scala.concurrent.{blocking, ExecutionContext, Future} 5 | import scala.concurrent.duration.FiniteDuration 6 | import scala.util.Try 7 | 8 | class ProcessStats() { 9 | private class AVG(var count: Long = 0, var sum: Long = 0) { 10 | def +=(value: Long): Unit = { 11 | count += 1 12 | sum += value 13 | } 14 | def ++=(avg: AVG): Unit = { 15 | count += avg.count 16 | sum += avg.sum 17 | } 18 | def reset(): Unit = { 19 | count = 0 20 | sum = 0 21 | } 22 | def isEmpty: Boolean = count == 0L 23 | override def toString: String = if (isEmpty) "0" else (sum / count).toString 24 | } 25 | 26 | private class StatEntry( 27 | var total: Long = -1, 28 | var nSuccess: Int = 0, 29 | var nFailure: Int = 0, 30 | global: AVG = new AVG, 31 | current: AVG = new AVG 32 | ) { 33 | def update(isSuccess: Boolean, time: Long): Unit = { 34 | if (isSuccess) nSuccess += 1 35 | else nFailure += 1 36 | current += time 37 | } 38 | 39 | def failure(): Unit = nFailure += 1 40 | 41 | def flush(): Unit = { 42 | global ++= current 43 | current.reset() 44 | } 45 | 46 | def isEmpty: Boolean = nSuccess == 0 && nFailure == 0 47 | 48 | def currentStats: String = { 49 | val totalTxt = if (total < 0) "" else s"/$total" 50 | val avg = if (current.isEmpty) "" else s"(${current}ms)" 51 | s"${nSuccess + nFailure}$totalTxt$avg" 52 | } 53 | 54 | def setTotal(v: Long): Unit = total = v 55 | 56 | override def toString: String = { 57 | val totalTxt = if (total < 0) s"/${nSuccess + nFailure}" else s"/$total" 58 | val avg = if (global.isEmpty) "" else s" avg:${global}ms" 59 | val failureTxt = if (nFailure > 0) s"$nFailure failures" else "" 60 | s"$nSuccess$totalTxt$failureTxt$avg" 61 | } 62 | } 63 | 64 | private val stats: TrieMap[String, StatEntry] = TrieMap.empty 65 | private var stage: Option[String] = None 66 | 67 | def `try`[A](name: String)(body: => Try[A]): Try[A] = { 68 | val start = System.currentTimeMillis() 69 | val ret = body 70 | val time = System.currentTimeMillis() - start 71 | stats.getOrElseUpdate(name, new StatEntry).update(ret.isSuccess, time) 72 | ret 73 | } 74 | 75 | def apply[A](name: String)(body: => A): A = { 76 | val start = System.currentTimeMillis() 77 | try { 78 | val ret = body 79 | val time = System.currentTimeMillis() - start 80 | stats.getOrElseUpdate(name, new StatEntry).update(isSuccess = true, time) 81 | ret 82 | } catch { 83 | case error: Throwable => 84 | val time = System.currentTimeMillis() - start 85 | stats.getOrElseUpdate(name, new StatEntry).update(isSuccess = false, time) 86 | throw error 87 | } 88 | } 89 | 90 | def failure(name: String): Unit = stats.getOrElseUpdate(name, new StatEntry).failure() 91 | 92 | def flush(): Unit = stats.foreach(_._2.flush()) 93 | 94 | def setStage(s: String): Unit = stage = Some(s) 95 | def unsetStage(): Unit = stage = None 96 | 97 | def showStats(): String = 98 | stats 99 | .collect { 100 | case (name, entry) if !entry.isEmpty => s"$name:${entry.currentStats}" 101 | } 102 | .mkString(stage.fold("")(s => s"[$s] "), " ", "") 103 | 104 | def showStats[A](interval: FiniteDuration, printMessage: String => Unit)(body: => A)(implicit ec: ExecutionContext): A = { 105 | var stop = false 106 | Future { 107 | blocking { 108 | while (!stop) { 109 | Thread.sleep(interval.toMillis) 110 | printMessage(showStats()) 111 | flush() 112 | } 113 | } 114 | } 115 | try body 116 | finally stop = true 117 | } 118 | override def toString: String = 119 | stats 120 | .map { 121 | case (name, entry) => s"$name: $entry" 122 | } 123 | .toSeq 124 | .sorted 125 | .mkString(stage.fold("")(s => s"Stage: $s\n"), "\n", "") 126 | 127 | def setTotal(name: String, count: Long): Unit = 128 | stats.getOrElseUpdate(name, new StatEntry).setTotal(count) 129 | } 130 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/utils/RichType.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.utils 2 | 3 | import scala.reflect.runtime.universe._ 4 | 5 | object RichType { 6 | 7 | def getTypeArgs(t: Type, fromType: Type): List[Type] = 8 | t.baseType(fromType.typeSymbol).typeArgs 9 | } 10 | 11 | object CaseClassType { 12 | 13 | def unapplySeq(tpe: Type): Option[Seq[Symbol]] = 14 | unapplySeq(tpe.typeSymbol) 15 | 16 | def unapplySeq(s: Symbol): Option[Seq[Symbol]] = 17 | if (s.isClass) { 18 | val c = s.asClass 19 | if (c.isCaseClass) 20 | Some(c.primaryConstructor.typeSignature.paramLists.head) 21 | else None 22 | } else None 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/scala/org/thp/scalligraph/utils/UnthreadedExecutionContext.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.utils 2 | 3 | import scala.concurrent.ExecutionContext 4 | 5 | object UnthreadedExecutionContext extends ExecutionContext { 6 | override def execute(runnable: Runnable): Unit = runnable.run() 7 | override def reportFailure(t: Throwable): Unit = 8 | throw new IllegalStateException("exception in sameThreadExecutionContext", t) 9 | } 10 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/java/org/thp/scalligraph/janus/strategies/ElementValueComparatorAcceptNull.java: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus.strategies; 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Element; 4 | import org.apache.tinkerpop.gremlin.structure.Property; 5 | 6 | import java.io.Serializable; 7 | import java.util.Comparator; 8 | 9 | public final class ElementValueComparatorAcceptNull implements Comparator, Serializable { 10 | 11 | private final String propertyKey; 12 | private final Comparator valueComparator; 13 | 14 | public ElementValueComparatorAcceptNull(final String propertyKey, final Comparator valueComparator) { 15 | this.propertyKey = propertyKey; 16 | this.valueComparator = valueComparator; 17 | } 18 | 19 | public String getPropertyKey() { 20 | return this.propertyKey; 21 | } 22 | 23 | public Comparator getValueComparator() { 24 | return this.valueComparator; 25 | } 26 | 27 | @Override 28 | public int compare(final Element elementA, final Element elementB) { 29 | // return this.valueComparator.compare(elementA.value(this.propertyKey), elementB.value(this.propertyKey)); 30 | Property propA = elementA.property(this.propertyKey); 31 | Property propB = elementB.property(this.propertyKey); 32 | if (propA.isPresent() && propB.isPresent()) return this.valueComparator.compare(propA.value(), propB.value()); 33 | else if (propA.isPresent()) return -1; 34 | else if (propB.isPresent()) return 1; 35 | else return 0; 36 | 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return this.valueComparator.toString() + "AcceptNull(" + this.propertyKey + ')'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/java/org/thp/scalligraph/janus/strategies/IndexOptimizerStrategy.java: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus.strategies; 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.Step; 4 | import org.apache.tinkerpop.gremlin.process.traversal.Traversal; 5 | import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; 6 | import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; 7 | import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NotStep; 8 | import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep; 9 | import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; 10 | import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep; 11 | import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy; 12 | import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.FilterRankingStrategy; 13 | import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.InlineFilterStrategy; 14 | 15 | import java.util.Arrays; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Set; 19 | 20 | /** 21 | * {@code IndexOptimizerStrategy} reorders HasSteps, FilterSteps and OrderSteps that follow a GraphStep 22 | * HasSteps and OrderSteps are put just after GraphStep in order to use graph index 23 | */ 24 | public final class IndexOptimizerStrategy extends AbstractTraversalStrategy implements TraversalStrategy.OptimizationStrategy { 25 | 26 | private static final IndexOptimizerStrategy INSTANCE = new IndexOptimizerStrategy(); 27 | private static final Set> PRIORS = new HashSet<>(Arrays.asList(InlineFilterStrategy.class, FilterRankingStrategy.class)); 28 | 29 | private IndexOptimizerStrategy() { 30 | } 31 | 32 | @Override 33 | public void apply(final Traversal.Admin traversal) { 34 | if (traversal.getStartStep() instanceof GraphStep) { 35 | apply(traversal, traversal.getSteps(), 1); 36 | } 37 | } 38 | 39 | private int apply(final Traversal.Admin traversal, List steps, int index) { 40 | if (index < steps.size()) { 41 | Step step = steps.get(index); 42 | if (step instanceof HasStep || step instanceof OrderGlobalStep) 43 | return apply(traversal, steps, index + 1); 44 | else if (step instanceof TraversalFilterStep || step instanceof NotStep) { 45 | traversal.removeStep(index); 46 | int newPosition = apply(traversal, traversal.getSteps(), index); 47 | traversal.addStep(newPosition, step); 48 | return newPosition; 49 | } else 50 | return index; 51 | } else return index; 52 | } 53 | 54 | @Override 55 | public Set> applyPrior() { 56 | return PRIORS; 57 | } 58 | 59 | public static IndexOptimizerStrategy instance() { 60 | return INSTANCE; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/java/org/thp/scalligraph/janus/strategies/LimitedIterator.java: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus.strategies; 2 | 3 | import org.apache.tinkerpop.gremlin.structure.Element; 4 | 5 | import java.util.Iterator; 6 | 7 | public class LimitedIterator implements Iterator { 8 | 9 | final Iterator iterator; 10 | int count = 0; 11 | final int highLimit; 12 | 13 | public LimitedIterator(final Integer lowLimit, final Integer highLimit, final Iterator iterator) { 14 | this.iterator = iterator; 15 | this.highLimit = highLimit; 16 | while (iterator.hasNext() && count < lowLimit) { 17 | iterator.next(); 18 | count++; 19 | } 20 | } 21 | 22 | @Override 23 | public boolean hasNext() { 24 | return count < this.highLimit && this.iterator.hasNext(); 25 | } 26 | 27 | @Override 28 | public E next() { 29 | return this.iterator.next(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/java/org/thp/scalligraph/janus/strategies/MultiComparatorAcceptNull.java: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus.strategies; 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.Order; 4 | import org.apache.tinkerpop.gremlin.process.traversal.traverser.ProjectedTraverser; 5 | 6 | import java.io.Serializable; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | 10 | /** 11 | * @author Marko A. Rodriguez (http://markorodriguez.com) 12 | */ 13 | public final class MultiComparatorAcceptNull implements Comparator, Serializable { 14 | 15 | private List comparators; 16 | private boolean isShuffle; 17 | int startIndex = 0; 18 | 19 | private MultiComparatorAcceptNull() { 20 | // for serialization purposes 21 | } 22 | 23 | public MultiComparatorAcceptNull(final List> comparators) { 24 | this.comparators = (List) comparators; 25 | this.isShuffle = !this.comparators.isEmpty() && Order.shuffle == this.comparators.get(this.comparators.size() - 1); 26 | for (int i = 0; i < this.comparators.size(); i++) { 27 | if (this.comparators.get(i) == Order.shuffle) 28 | this.startIndex = i + 1; 29 | } 30 | } 31 | 32 | @Override 33 | public int compare(final C objectA, final C objectB) { 34 | if (this.comparators.isEmpty()) { 35 | return Order.asc.compare(objectA, objectB); 36 | } else { 37 | for (int i = this.startIndex; i < this.comparators.size(); i++) { 38 | Object a = this.getObject(objectA, i); 39 | Object b = this.getObject(objectB, i); 40 | if (a != null && b != null) { 41 | final int comparison = this.comparators.get(i).compare(a, b); 42 | if (comparison != 0) 43 | return comparison; 44 | } else if (a != null) 45 | return -1; 46 | else if (b != null) 47 | return 1; 48 | } 49 | return 0; 50 | } 51 | } 52 | 53 | public boolean isShuffle() { 54 | return this.isShuffle; 55 | } 56 | 57 | private final Object getObject(final C object, final int index) { 58 | if (object instanceof ProjectedTraverser) 59 | return ((ProjectedTraverser) object).getProjections().get(index); 60 | else 61 | return object; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/java/org/thp/scalligraph/janus/strategies/MultiDistinctOrderedIteratorAcceptNull.java: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus.strategies; 2 | 3 | import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; 4 | import org.apache.tinkerpop.gremlin.util.function.MultiComparator; 5 | import org.janusgraph.graphdb.tinkerpop.optimize.HasStepFolder.OrderEntry; 6 | 7 | import java.util.*; 8 | 9 | // from: https://raw.githubusercontent.com/JanusGraph/janusgraph/v0.5.3/janusgraph-core/src/main/java/org/janusgraph/graphdb/util/MultiDistinctOrderedIterator.java 10 | public class MultiDistinctOrderedIteratorAcceptNull implements CloseableIterator { 11 | 12 | private final Map> iterators = new LinkedHashMap<>(); 13 | private final Map values = new LinkedHashMap<>(); 14 | private final TreeMap currentElements; 15 | private final Set allElements = new HashSet<>(); 16 | private final Integer limit; 17 | private long count = 0; 18 | 19 | public MultiDistinctOrderedIteratorAcceptNull(final Integer lowLimit, final Integer highLimit, final List> iterators, final List orders) { 20 | this.limit = highLimit; 21 | final List> comp = new ArrayList<>(); 22 | orders.forEach(o -> comp.add(new ElementValueComparatorAcceptNull(o.key, o.order))); 23 | Comparator comparator = new MultiComparator<>(comp); 24 | for (int i = 0; i < iterators.size(); i++) { 25 | this.iterators.put(i, iterators.get(i)); 26 | } 27 | currentElements = new TreeMap<>(comparator); 28 | long i = 0; 29 | while (i < lowLimit && this.hasNext()) { 30 | this.next(); 31 | i++; 32 | } 33 | } 34 | 35 | @Override 36 | public boolean hasNext() { 37 | if (limit != null && count >= limit) { 38 | return false; 39 | } 40 | for (int i = 0; i < iterators.size(); i++) { 41 | if (!values.containsKey(i) && iterators.get(i).hasNext()){ 42 | E element = null; 43 | do { 44 | element = iterators.get(i).next(); 45 | if (allElements.contains(element)) { 46 | element = null; 47 | } 48 | } while (element == null && iterators.get(i).hasNext()); 49 | if (element != null) { 50 | values.put(i, element); 51 | currentElements.put(element, i); 52 | allElements.add(element); 53 | } 54 | } 55 | } 56 | return !values.isEmpty(); 57 | } 58 | 59 | @Override 60 | public E next() { 61 | count++; 62 | return values.remove(currentElements.remove(currentElements.firstKey())); 63 | } 64 | 65 | @Override 66 | public void close() { 67 | iterators.values().forEach(CloseableIterator::closeIterator); 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/java/org/thp/scalligraph/janus/strategies/OrderAcceptNullStrategy.java: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus.strategies; 2 | 3 | import org.apache.tinkerpop.gremlin.process.traversal.Traversal; 4 | import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; 5 | import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep; 6 | import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy; 7 | import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper; 8 | import org.javatuples.Pair; 9 | 10 | import java.util.Comparator; 11 | 12 | public final class OrderAcceptNullStrategy extends AbstractTraversalStrategy implements TraversalStrategy.OptimizationStrategy { 13 | 14 | private static final OrderAcceptNullStrategy INSTANCE = new OrderAcceptNullStrategy(); 15 | 16 | private OrderAcceptNullStrategy() { 17 | } 18 | 19 | @Override 20 | public void apply(final Traversal.Admin traversal) { 21 | TraversalHelper.getStepsOfClass(OrderGlobalStep.class, traversal).forEach((originalStep) -> { 22 | OrderGlobalStepAcceptNull step = new OrderGlobalStepAcceptNull(originalStep.getTraversal(), originalStep.getLimit()); 23 | originalStep.getComparators().forEach((pairObj) -> { 24 | Pair comparatorPair = (Pair)pairObj; 25 | step.addComparator(comparatorPair.getValue0(), comparatorPair.getValue1()); 26 | }); 27 | TraversalHelper.replaceStep(originalStep, step, traversal); 28 | }); 29 | } 30 | 31 | public static OrderAcceptNullStrategy instance() { 32 | return INSTANCE; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/resources/play/reference-overrides.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | serializers { 4 | janus-cluster = "org.thp.scalligraph.janus.JanusClusterSerializer" 5 | } 6 | 7 | serialization-bindings { 8 | "org.thp.scalligraph.janus.JanusClusterManagerActor$Message" = janus-cluster 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | db { 2 | janusgraph { 3 | storage.backend = inmemory 4 | storage.directory = target/janusgraph-test-database.db 5 | index.search { 6 | backend = lucene 7 | directory = target/janusgraph-test-database.idx 8 | index-name = "scalligraph" 9 | } 10 | cache.db-cache = true 11 | graph.replace-instance-if-exists = true 12 | connect { 13 | maxAttempts = 10 14 | minBackoff = 1 second 15 | maxBackoff = 5 seconds 16 | randomFactor = 0.2 17 | } 18 | dropAndRebuildIndexOnFailure = false 19 | forceDropAndRebuildIndex = false 20 | immenseTermProcessing = {} 21 | } 22 | onConflict { 23 | maxAttempts = 6 24 | minBackoff = 100 milliseconds 25 | maxBackoff = 1 seconds 26 | randomFactor = 0.2 27 | } 28 | chunkSize = 32k 29 | } 30 | -------------------------------------------------------------------------------- /database/janusgraph/src/main/scala/org/thp/scalligraph/janus/JanusClusterSerializer.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.janus 2 | 3 | import akka.actor.ExtendedActorSystem 4 | import akka.actor.typed.{ActorRef, ActorRefResolver} 5 | import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps 6 | import akka.serialization.Serializer 7 | import play.api.libs.json.{Json, OFormat, Reads, Writes} 8 | 9 | import java.io.NotSerializableException 10 | 11 | class JanusClusterSerializer(system: ExtendedActorSystem) extends Serializer { 12 | import JanusClusterManagerActor._ 13 | 14 | private val actorRefResolver = ActorRefResolver(system.toTyped) 15 | 16 | implicit def actorRefReads[T]: Reads[ActorRef[T]] = Reads.StringReads.map(actorRefResolver.resolveActorRef) 17 | implicit def actorRefWrites[T]: Writes[ActorRef[T]] = Writes.StringWrites.contramap[ActorRef[T]](actorRefResolver.toSerializationFormat) 18 | implicit val joinClusterFormat: OFormat[JoinCluster] = Json.format[JoinCluster] 19 | 20 | override def identifier: Int = 775347820 21 | 22 | override def toBinary(o: AnyRef): Array[Byte] = 23 | o match { 24 | case joinCluster: JoinCluster => 0.toByte +: Json.toJson(joinCluster).toString.getBytes 25 | case ClusterRequestInit => Array(1) 26 | case ClusterInitSuccess => Array(2) 27 | case ClusterInitFailure => Array(3) 28 | case ClusterSuccessConfigurationIgnored(indexBackend) => 4.toByte +: indexBackend.getBytes 29 | case ClusterSuccess => Array(5) 30 | case ClusterFailure => Array(6) 31 | case GetStatus(replyTo) => 7.toByte +: actorRefResolver.toSerializationFormat(replyTo).getBytes 32 | case StatusInit => Array(8) 33 | case StatusSuccess => Array(9) 34 | case StatusFailure => Array(10) 35 | case _ => throw new NotSerializableException 36 | } 37 | 38 | override def includeManifest: Boolean = false 39 | 40 | override def fromBinary(bytes: Array[Byte], manifest: Option[Class[_]]): AnyRef = 41 | bytes(0) match { 42 | case 0 => Json.parse(bytes.tail).as[JoinCluster] 43 | case 1 => ClusterRequestInit 44 | case 2 => ClusterInitSuccess 45 | case 3 => ClusterInitFailure 46 | case 4 => ClusterSuccessConfigurationIgnored(new String(bytes.tail)) 47 | case 5 => ClusterSuccess 48 | case 6 => ClusterFailure 49 | case 7 => GetStatus(actorRefResolver.resolveActorRef(new String(bytes.tail))) 50 | case 8 => StatusInit 51 | case 9 => StatusSuccess 52 | case 10 => StatusFailure 53 | case _ => throw new NotSerializableException 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /database/neo4j/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | database.maxRetryOnConflict = 5 -------------------------------------------------------------------------------- /database/orientdb/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | db { 2 | orientdb { 3 | url = "memory:orientdb-test-database.db" 4 | user = admin 5 | password = admin 6 | } 7 | maxRetryOnConflict = 5 8 | chunkSize = 32k 9 | } -------------------------------------------------------------------------------- /database/orientdb/src/main/scala/org/thp/scalligraph/orientdb/OrientDatabaseStorageSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.orientdb 2 | import java.io.InputStream 3 | import java.util.{Base64, List => JList} 4 | 5 | import javax.inject.{Inject, Singleton} 6 | import org.thp.scalligraph.services.StorageSrv 7 | import play.api.Configuration 8 | 9 | import scala.collection.JavaConverters._ 10 | import scala.util.{Success, Try} 11 | 12 | @Singleton 13 | class OrientDatabaseStorageSrv(db: OrientDatabase, chunkSize: Int) extends StorageSrv { 14 | 15 | case class State(recordIds: List[OIdentifiable], buffer: Array[Byte]) { 16 | 17 | def next: Option[State] = recordIds match { 18 | case head :: tail => 19 | val buffer = head.getRecord[ORecordBytes].toStream 20 | Some(State(tail, buffer)) 21 | case _ => None 22 | } 23 | } 24 | 25 | object State { 26 | val b64decoder: Base64.Decoder = Base64.getDecoder 27 | 28 | def apply(id: String): Option[State] = db.roTransaction { implicit graph => 29 | graph 30 | .V() 31 | .hasId(id) 32 | .value[JList[OIdentifiable]]("binary") 33 | .headOption 34 | .fold(List.empty[OIdentifiable])(_.asScala.toList) match { 35 | case head :: tail => 36 | val buffer = head.getRecord[ORecordBytes].toStream 37 | Some(State(tail, buffer)) 38 | case _ => None 39 | } 40 | } 41 | } 42 | 43 | @Inject 44 | def this(db: OrientDatabase, configuration: Configuration) = this(db, configuration.underlying.getBytes("storage.database.chunkSize").toInt) 45 | 46 | override def loadBinary(folder: String, id: String): InputStream = 47 | new InputStream { 48 | private var state = State(id) 49 | private var index = 0 50 | 51 | @scala.annotation.tailrec 52 | override def read(): Int = 53 | state match { 54 | case Some(State(_, b)) if b.length > index => 55 | val d = b(index) 56 | index += 1 57 | d.toInt & 0xff 58 | case None => -1 59 | case Some(s) => 60 | state = s.next 61 | index = 0 62 | read() 63 | } 64 | } 65 | 66 | override def exists(folder: String, id: String): Boolean = db.roTransaction { implicit graph => 67 | graph.V(id).exists 68 | } 69 | 70 | override def saveBinary(folder: String, id: String, is: InputStream)(implicit graph: Graph): Try[Unit] = { 71 | val odb = graph.asInstanceOf[OrientGraph].database() 72 | 73 | odb.declareIntent(new OIntentMassiveInsert) 74 | val chunkIds = Iterator 75 | .continually { 76 | val chunk = new ORecordBytes 77 | val len = chunk.fromInputStream(is, chunkSize) 78 | odb.save[ORecordBytes](chunk) 79 | len -> chunk.getIdentity.asInstanceOf[OIdentifiable] 80 | } 81 | .takeWhile(_._1 > 0) 82 | .map(_._2) 83 | .toSeq 84 | odb.declareIntent(null) 85 | val v = graph.addVertex(db.attachmentVertexLabel) 86 | v.property("_id", id) // FIXME:ID 87 | v.property(db.attachmentPropertyName, chunkIds.asJava) 88 | Success(()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /graphql/src/main/scala/org/thp/scalligraph/graphql/Order.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.graphql 2 | 3 | 4 | import org.thp.scalligraph.auth.AuthContext 5 | import org.thp.scalligraph.query.PublicProperty 6 | 7 | import scala.reflect.{ClassTag, classTag} 8 | import scala.util.Try 9 | 10 | object Order { 11 | 12 | lazy val orderEnumeration = EnumType( 13 | "Order", 14 | values = List( 15 | EnumValue("decr", value = org.apache.tinkerpop.gremlin.process.traversal.Order.desc), 16 | EnumValue("incr", value = org.apache.tinkerpop.gremlin.process.traversal.Order.asc), 17 | EnumValue("shuffle", value = org.apache.tinkerpop.gremlin.process.traversal.Order.shuffle) 18 | ) 19 | ) 20 | 21 | def getField[S <: Traversal.V[Scalli][_, E, S]: ClassTag, E <: Element]( 22 | properties: List[PublicProperty[_ <: Element, _, _]], 23 | traversalType: OutputType[S] 24 | ): Option[Field[AuthGraph, S]] = { 25 | 26 | case class FieldOrder[A <: Element](property: PublicProperty[A, _, _], order: org.apache.tinkerpop.gremlin.process.traversal.Order) { 27 | def orderBy(authContext: AuthContext): OrderBy[_] = By(property.select(__[A], authContext), order) 28 | } 29 | 30 | val fields = properties.map(p => InputField(p.propertyName, OptionInputType(orderEnumeration))) 31 | val inputType: InputObjectType[Seq[FieldOrder[_]]] = 32 | InputObjectType[Seq[FieldOrder[_]]](classTag[S].runtimeClass.getSimpleName + "Order", fields) 33 | 34 | val fromInput: FromInput[Seq[FieldOrder[_]]] = new FromInput[Seq[FieldOrder[_]]] { 35 | override val marshaller: ResultMarshaller = CoercedScalaResultMarshaller.default 36 | 37 | override def fromResult(node: marshaller.Node): Seq[FieldOrder[_]] = { 38 | val input = node.asInstanceOf[Map[String, Option[Any]]] 39 | for { 40 | (key, valueMaybe) <- input.toSeq 41 | value <- valueMaybe 42 | order <- Try(org.apache.tinkerpop.gremlin.process.traversal.Order.valueOf(value.toString)).toOption 43 | property <- properties.find(_.propertyName == key) 44 | } yield FieldOrder(property, order) 45 | } 46 | } 47 | val arg = Argument("order", inputType)( 48 | fromInput.asInstanceOf[FromInput[Seq[FieldOrder[_]] @@ FromInput.InputObjectResult]], 49 | WithoutInputTypeTags.ioArgTpe[Seq[FieldOrder[_]]] 50 | ) 51 | Some( 52 | Field[AuthGraph, S, S, S]( 53 | "order", 54 | traversalType, 55 | arguments = List(arg), 56 | resolve = ctx => ctx.value.sort(ctx.arg(arg).map(_.orderBy(ctx.ctx.auth)): _*) 57 | ) 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /graphql/src/main/scala/org/thp/scalligraph/graphql/package.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph 2 | 3 | import java.util.Date 4 | 5 | package object graphql { 6 | val DateType: ScalarAlias[Date, Long] = ScalarAlias[Date, Long](LongType, _.getTime, ts => Right(new Date(ts))) 7 | } 8 | -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/complexQuery.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "allPeople": { 4 | "created": { 5 | "sort": { 6 | "name": { 7 | "toList": ["lop", "lop", "lop", "ripple"] 8 | } 9 | } 10 | }, 11 | "sort": { 12 | "toList": [ 13 | {"name": "franck", "age": 28}, 14 | {"name": "josh", "age": 32}, 15 | {"name": "marc", "age": 34}, 16 | {"name": "marko", "age": 29}, 17 | {"name": "peter", "age": 35}, 18 | {"name": "vadas", "age": 27} 19 | ] 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/complexQuery.graphql: -------------------------------------------------------------------------------- 1 | query AllPerson { 2 | allPeople { 3 | created { 4 | sort(sort:{ 5 | name: incr 6 | }) { 7 | name { 8 | toList 9 | } 10 | } 11 | } 12 | sort( 13 | sort:{ 14 | name: incr 15 | }) { 16 | toList { 17 | name 18 | age 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/modernSchema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: ModernQueryExecutor 3 | } 4 | 5 | type IntStep { 6 | toList: [Int!]! 7 | head: Int! 8 | headOption: Int 9 | } 10 | 11 | type ModernQueryExecutor { 12 | allSoftwareList: [Software!]! 13 | allPeople: PersonStep! 14 | } 15 | 16 | enum Order { 17 | decr 18 | incr 19 | shuffle 20 | } 21 | 22 | type Person { 23 | name: String! 24 | age: Int! 25 | } 26 | 27 | type PersonStep { 28 | name: StringStep! 29 | age: IntStep! 30 | toList: [Person!]! 31 | head: Person! 32 | headOption: Person 33 | filter(filter: PersonStepsFilter!): PersonStep! 34 | order(order: PersonStepsSort!): PersonStep! 35 | } 36 | 37 | input PersonStepsFilter { 38 | name: String 39 | name_not: String 40 | name_in: [String!] 41 | name_not_in: [String!] 42 | name_lt: String 43 | name_lte: String 44 | name_gt: String 45 | name_gte: String 46 | name_contains: String 47 | name_not_contains: String 48 | name_starts_with: String 49 | name_not_starts_with: String 50 | name_ends_with: String 51 | name_not_ends_with: String 52 | age: Int 53 | age_not: Int 54 | age_in: [Int!] 55 | age_not_in: [Int!] 56 | age_lt: Int 57 | age_lte: Int 58 | age_gt: Int 59 | age_gte: Int 60 | } 61 | 62 | input PersonStepsSort { 63 | name: Order 64 | age: Order 65 | } 66 | 67 | type Software { 68 | name: String! 69 | lang: String! 70 | } 71 | 72 | type StringStep { 73 | toList: [String!]! 74 | head: String! 75 | headOption: String 76 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/queryWithBooleanOperators.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "allPeople": { 4 | "peopleFilteredByName": { 5 | "peopleFilteredByAge": { 6 | "sort": { 7 | "age": { 8 | "toList": [ 9 | 32 10 | ] 11 | } 12 | } 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/queryWithBooleanOperators.graphql: -------------------------------------------------------------------------------- 1 | query filterPerson { 2 | allPeople { 3 | peopleFilteredByName: filter( 4 | filter: { 5 | name_contains: "o" 6 | name_ends_with: "s" 7 | } 8 | ) 9 | { 10 | peopleFilteredByAge: filter( 11 | filter: { 12 | age_gt: 30 13 | } 14 | ) { 15 | sort(sort: { age:incr }) { 16 | age { 17 | toList 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/queryWithFilterObject.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "allPeople": { 4 | "filter": { 5 | "sort": { 6 | "age": { 7 | "toList": [ 8 | 29, 9 | 32 10 | ] 11 | } 12 | } 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/queryWithFilterObject.graphql: -------------------------------------------------------------------------------- 1 | query filterPerson { 2 | allPeople { 3 | filter( 4 | filter: { 5 | name_contains: "o" 6 | } 7 | ) { 8 | sort( 9 | sort:{ 10 | age: incr 11 | }) { 12 | age { 13 | toList 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/queryWithSeveralAttributes.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "allPeople": { 4 | "sort": { 5 | "toList": [ 6 | {"name": "franck", "age": 28}, 7 | {"name": "josh", "age": 32}, 8 | {"name": "marc", "age": 34}, 9 | {"name": "marko", "age": 29}, 10 | {"name": "peter", "age":35}, 11 | {"name": "vadas", "age": 27} 12 | ] 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/queryWithSeveralAttributes.graphql: -------------------------------------------------------------------------------- 1 | query AllPerson { 2 | allPeople { 3 | sort( 4 | sort:{ 5 | name: incr 6 | }) { 7 | toList { 8 | name 9 | age 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/schema.graphqls: -------------------------------------------------------------------------------- 1 | schema { 2 | query: ModernQueryExecutor 3 | } 4 | 5 | type IntStep { 6 | toList: [Int!]! 7 | head: Int! 8 | headOption: Int 9 | } 10 | 11 | type ModernQueryExecutor { 12 | allSoftwareList: [Software!]! 13 | allPeople: PersonStep! 14 | } 15 | 16 | enum Order { 17 | decr 18 | incr 19 | shuffle 20 | } 21 | 22 | type Person { 23 | name: String! 24 | age: Int! 25 | } 26 | 27 | type PersonStep { 28 | created: SoftwareStep! 29 | name: StringStep! 30 | age: IntStep! 31 | toList: [Person!]! 32 | head: Person! 33 | headOption: Person 34 | filter(filter: PersonStepsFilter!): PersonStep! 35 | sort(order: PersonStepsSort!): PersonStep! 36 | } 37 | 38 | input PersonStepsFilter { 39 | name: String 40 | name_not: String 41 | name_in: [String!] 42 | name_not_in: [String!] 43 | name_lt: String 44 | name_lte: String 45 | name_gt: String 46 | name_gte: String 47 | name_contains: String 48 | name_not_contains: String 49 | name_starts_with: String 50 | name_not_starts_with: String 51 | name_ends_with: String 52 | name_not_ends_with: String 53 | age: Int 54 | age_not: Int 55 | age_in: [Int!] 56 | age_not_in: [Int!] 57 | age_lt: Int 58 | age_lte: Int 59 | age_gt: Int 60 | age_gte: Int 61 | } 62 | 63 | input PersonStepsSort { 64 | name: Order 65 | age: Order 66 | } 67 | 68 | type Software { 69 | name: String! 70 | lang: String! 71 | } 72 | 73 | type SoftwareStep { 74 | name: StringStep! 75 | lang: StringStep! 76 | toList: [Software!]! 77 | head: Software! 78 | headOption: Software 79 | filter(filter: SoftwareStepsFilter!): SoftwareStep! 80 | sort(order: SoftwareStepsSort!): SoftwareStep! 81 | } 82 | 83 | input SoftwareStepsFilter { 84 | name: String 85 | name_not: String 86 | name_in: [String!] 87 | name_not_in: [String!] 88 | name_lt: String 89 | name_lte: String 90 | name_gt: String 91 | name_gte: String 92 | name_contains: String 93 | name_not_contains: String 94 | name_starts_with: String 95 | name_not_starts_with: String 96 | name_ends_with: String 97 | name_not_ends_with: String 98 | lang: String 99 | lang_not: String 100 | lang_in: [String!] 101 | lang_not_in: [String!] 102 | lang_lt: String 103 | lang_lte: String 104 | lang_gt: String 105 | lang_gte: String 106 | lang_contains: String 107 | lang_not_contains: String 108 | lang_starts_with: String 109 | lang_not_starts_with: String 110 | lang_ends_with: String 111 | lang_not_ends_with: String 112 | } 113 | 114 | input SoftwareStepsSort { 115 | name: Order 116 | lang: Order 117 | } 118 | 119 | type StringStep { 120 | toList: [String!]! 121 | head: String! 122 | headOption: String 123 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/simpleQuery.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "allPeople": { 4 | "sort": { 5 | "name": { 6 | "toList": [ 7 | "franck", 8 | "josh", 9 | "marc", 10 | "marko", 11 | "peter", 12 | "vadas" 13 | ] 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /graphql/src/test/resources/graphql/simpleQuery.graphql: -------------------------------------------------------------------------------- 1 | query AllPerson { 2 | allPeople { 3 | sort( 4 | sort:{ 5 | name: incr 6 | }) { 7 | name { 8 | toList 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /graphql/src/test/scala/org/thp/scalligraph/graphql/SangriaTest.scala: -------------------------------------------------------------------------------- 1 | package org.thp.scalligraph.graphql 2 | 3 | import java.io.FileNotFoundException 4 | 5 | import org.thp.scalligraph.auth.{AuthContext, AuthContextImpl} 6 | import org.thp.scalligraph.models._ 7 | import org.thp.scalligraph.utils.UnthreadedExecutionContext 8 | import play.api.libs.json.{JsObject, JsValue, Json} 9 | import play.api.{Configuration, Environment} 10 | 11 | import scala.concurrent.duration._ 12 | import scala.concurrent.{Await, ExecutionContext} 13 | import scala.io.Source 14 | import scala.util.control.NonFatal 15 | import scala.util.{Failure, Try} 16 | 17 | class SangriaTest extends PlaySpecification { 18 | (new LogbackLoggerConfigurator).configure(Environment.simple(), Configuration.empty, Map.empty) 19 | implicit val authContext: AuthContext = AuthContextImpl("me", "", "", "", Set.empty) 20 | 21 | def executeQuery(query: Document, expected: JsValue, variables: JsValue = JsObject.empty)( 22 | implicit graph: Graph, 23 | schema: SangriaSchema[AuthGraph, Unit] 24 | ): MatchResult[_] = { 25 | implicit val ec: ExecutionContext = UnthreadedExecutionContext 26 | 27 | val futureResult = Executor.execute(schema, query, AuthGraph(authContext, graph), variables = variables) 28 | val result = Await.result(futureResult, 10.seconds) 29 | result must_=== expected 30 | } 31 | 32 | def readResource(resource: String): Try[String] = 33 | Try(Source.fromResource(resource).mkString) 34 | .recoverWith { case NonFatal(_) => Failure(new FileNotFoundException(resource)) } 35 | 36 | def executeQueryFile( 37 | testName: String, 38 | variables: JsObject = JsObject.empty 39 | )(implicit graph: Graph, schema: SangriaSchema[AuthGraph, Unit]): MatchResult[_] = { 40 | val query = QueryParser.parse(readResource(s"graphql/$testName.graphql").get).get 41 | val expected = Json.parse(readResource(s"graphql/$testName.expected.json").get) 42 | val vars = readResource(s"graphql/$testName.vars.json").fold(_ => variables, Json.parse) 43 | executeQuery(query = query, expected = expected, variables = vars) 44 | } 45 | 46 | Fragments.foreach(new DatabaseProviders().list) { dbProvider => 47 | val app: AppBuilder = AppBuilder() 48 | .bindToProvider(dbProvider) 49 | step(setupDatabase(app)) ^ specs(dbProvider.name, app) ^ step(teardownDatabase(app)) 50 | } 51 | 52 | def setupDatabase(app: AppBuilder): Try[Unit] = 53 | DatabaseBuilder.build(app.instanceOf[ModernSchema])(app.instanceOf[Database], authContext) 54 | 55 | def teardownDatabase(app: AppBuilder): Unit = () //app.instanceOf[Database].drop() 56 | 57 | def specs(name: String, app: AppBuilder): Fragment = { 58 | val db: Database = app.instanceOf[Database] 59 | val executor = new ModernQueryExecutor()(db) 60 | implicit val schema: SangriaSchema[AuthGraph, Unit] = SchemaGenerator(executor) 61 | 62 | s"[$name] Modern graph" should { 63 | "finds all persons" in db.transaction { implicit graph => 64 | val personSteps = app.instanceOf[PersonSrv].initSteps 65 | val r = personSteps.toSet.map(_.name) 66 | r must_=== Set("marko", "vadas", "josh", "peter", "marc", "franck") 67 | } 68 | 69 | "have GraphQL schema" in db.transaction { _ => 70 | val schemaStr = SchemaRenderer.renderSchema(schema) 71 | // println(s"new modern graphql schema is:\n$schemaStr") 72 | 73 | schemaStr must_!== "" 74 | } 75 | 76 | "execute simple query" in db.transaction { implicit graph => 77 | executeQueryFile("simpleQuery") 78 | } 79 | 80 | "filter entity using query object" in db.transaction { implicit graph => 81 | executeQueryFile("queryWithFilterObject") 82 | } 83 | 84 | "filter entity using query object with boolean operator" in db.transaction { implicit graph => 85 | executeQueryFile("queryWithBooleanOperators") 86 | } 87 | 88 | "return several attributes" in db.transaction { implicit graph => 89 | executeQueryFile("queryWithSeveralAttributes") 90 | } 91 | 92 | "execute complex query" in db.transaction { implicit graph => 93 | executeQueryFile("complexQuery") 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.4.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.13") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.3.0") 3 | --------------------------------------------------------------------------------