├── .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 |
--------------------------------------------------------------------------------