├── .credo.exs ├── .dockerignore ├── .formatter.exs ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── docker-compose.yml ├── lib ├── githubist.ex ├── githubist │ ├── application.ex │ ├── argument_parser.ex │ ├── developers │ │ ├── developer.ex │ │ └── developers.ex │ ├── import_helpers.ex │ ├── languages │ │ ├── language.ex │ │ └── languages.ex │ ├── loaders.ex │ ├── locations │ │ ├── location.ex │ │ └── locations.ex │ ├── repo.ex │ └── repositories │ │ ├── repositories.ex │ │ └── repository.ex ├── githubist_web.ex └── githubist_web │ ├── channels │ └── user_socket.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── resolvers │ ├── developer_resolver.ex │ ├── language_resolver.ex │ ├── location_resolver.ex │ ├── repository_resolver.ex │ ├── search_resolver.ex │ └── turkey_resolver.ex │ ├── router.ex │ ├── schema.ex │ ├── schema │ ├── developer_types.ex │ ├── enums.ex │ ├── input_objects.ex │ ├── language_types.ex │ ├── location_types.ex │ ├── repository_types.ex │ ├── scalars.ex │ ├── search_types.ex │ └── turkey_types.ex │ └── views │ ├── error_helpers.ex │ └── error_view.ex ├── mix ├── mix.exs ├── mix.lock ├── priv ├── data │ └── .gitkeep ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ ├── 20180816184210_create_developers.exs │ ├── 20180816200053_create_locations.exs │ ├── 20180816205243_create_languages.exs │ ├── 20180816210317_create_repositories.exs │ ├── 20180816210743_developer_location_rel.exs │ └── 20180816211337_repo_relations.exs │ └── seeds.exs ├── run └── test ├── githubist ├── argument_parser_test.exs ├── developers │ └── developers_test.exs ├── languages │ └── languages_test.exs ├── locations │ └── locations_test.exs └── repositories │ └── repositories_test.exs ├── githubist_web ├── graphql │ └── queries │ │ ├── developer_test.exs │ │ ├── developers_test.exs │ │ ├── language_test.exs │ │ ├── languages_test.exs │ │ ├── location_test.exs │ │ ├── locations_test.exs │ │ ├── repositories_test.exs │ │ ├── repository_test.exs │ │ └── turkey_test.exs └── views │ └── error_view_test.exs ├── support ├── channel_case.ex ├── conn_case.ex ├── data_case.ex ├── developers_helper.ex ├── graphql_helper.ex ├── languages_helper.ex ├── locations_helper.ex └── repositories_helper.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any exec using `mix credo -C `. If no exec name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: ["lib/", "src/", "test/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/"] 26 | }, 27 | # 28 | # If you create your own checks, you must specify the source files for 29 | # them here, so they can be loaded by Credo before running the analysis. 30 | # 31 | requires: [], 32 | # 33 | # If you want to enforce a style guide and need a more traditional linting 34 | # experience, you can change `strict` to `true` below: 35 | # 36 | strict: false, 37 | # 38 | # If you want to use uncolored output by default, you can change `color` 39 | # to `false` below: 40 | # 41 | color: true, 42 | # 43 | # You can customize the parameters of any check by adding a second element 44 | # to the tuple. 45 | # 46 | # To disable a check put `false` as second element: 47 | # 48 | # {Credo.Check.Design.DuplicatedCode, false} 49 | # 50 | checks: [ 51 | # 52 | ## Consistency Checks 53 | # 54 | {Credo.Check.Consistency.ExceptionNames}, 55 | {Credo.Check.Consistency.LineEndings}, 56 | {Credo.Check.Consistency.ParameterPatternMatching}, 57 | {Credo.Check.Consistency.SpaceAroundOperators}, 58 | {Credo.Check.Consistency.SpaceInParentheses}, 59 | {Credo.Check.Consistency.TabsOrSpaces}, 60 | 61 | # 62 | ## Design Checks 63 | # 64 | # You can customize the priority of any check 65 | # Priority values are: `low, normal, high, higher` 66 | # 67 | {Credo.Check.Design.AliasUsage, priority: :low}, 68 | # For some checks, you can also set other parameters 69 | # 70 | # If you don't want the `setup` and `test` macro calls in ExUnit tests 71 | # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just 72 | # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. 73 | # 74 | {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, 75 | # You can also customize the exit_status of each check. 76 | # If you don't want TODO comments to cause `mix credo` to fail, just 77 | # set this value to 0 (zero). 78 | # 79 | {Credo.Check.Design.TagTODO, exit_status: 2}, 80 | {Credo.Check.Design.TagFIXME}, 81 | 82 | # 83 | ## Readability Checks 84 | # 85 | {Credo.Check.Readability.AliasOrder}, 86 | {Credo.Check.Readability.FunctionNames}, 87 | {Credo.Check.Readability.LargeNumbers}, 88 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 120}, 89 | {Credo.Check.Readability.ModuleAttributeNames}, 90 | {Credo.Check.Readability.ModuleDoc}, 91 | {Credo.Check.Readability.ModuleNames}, 92 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, 93 | {Credo.Check.Readability.ParenthesesInCondition}, 94 | {Credo.Check.Readability.PredicateFunctionNames}, 95 | {Credo.Check.Readability.PreferImplicitTry}, 96 | {Credo.Check.Readability.RedundantBlankLines}, 97 | {Credo.Check.Readability.StringSigils}, 98 | {Credo.Check.Readability.TrailingBlankLine}, 99 | {Credo.Check.Readability.TrailingWhiteSpace}, 100 | {Credo.Check.Readability.VariableNames}, 101 | {Credo.Check.Readability.Semicolons}, 102 | {Credo.Check.Readability.SpaceAfterCommas}, 103 | 104 | # 105 | ## Refactoring Opportunities 106 | # 107 | {Credo.Check.Refactor.DoubleBooleanNegation}, 108 | {Credo.Check.Refactor.CondStatements}, 109 | {Credo.Check.Refactor.CyclomaticComplexity}, 110 | {Credo.Check.Refactor.FunctionArity}, 111 | {Credo.Check.Refactor.LongQuoteBlocks}, 112 | {Credo.Check.Refactor.MatchInCondition}, 113 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 114 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 115 | {Credo.Check.Refactor.Nesting}, 116 | {Credo.Check.Refactor.PipeChainStart, 117 | excluded_argument_types: [:atom, :binary, :fn, :keyword], excluded_functions: []}, 118 | {Credo.Check.Refactor.UnlessWithElse}, 119 | 120 | # 121 | ## Warnings 122 | # 123 | {Credo.Check.Warning.BoolOperationOnSameValues}, 124 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, 125 | {Credo.Check.Warning.IExPry}, 126 | {Credo.Check.Warning.IoInspect}, 127 | {Credo.Check.Warning.LazyLogging}, 128 | {Credo.Check.Warning.OperationOnSameValues}, 129 | {Credo.Check.Warning.OperationWithConstantResult}, 130 | {Credo.Check.Warning.UnusedEnumOperation}, 131 | {Credo.Check.Warning.UnusedFileOperation}, 132 | {Credo.Check.Warning.UnusedKeywordOperation}, 133 | {Credo.Check.Warning.UnusedListOperation}, 134 | {Credo.Check.Warning.UnusedPathOperation}, 135 | {Credo.Check.Warning.UnusedRegexOperation}, 136 | {Credo.Check.Warning.UnusedStringOperation}, 137 | {Credo.Check.Warning.UnusedTupleOperation}, 138 | {Credo.Check.Warning.RaiseInsideRescue}, 139 | 140 | # 141 | # Controversial and experimental checks (opt-in, just remove `, false`) 142 | # 143 | {Credo.Check.Refactor.ABCSize, false}, 144 | {Credo.Check.Refactor.AppendSingleItem, false}, 145 | {Credo.Check.Refactor.VariableRebinding, false}, 146 | {Credo.Check.Warning.MapGetUnsafePass, false}, 147 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 148 | 149 | # 150 | # Deprecated checks (these will be deleted after a grace period) 151 | # 152 | {Credo.Check.Readability.Specs, false} 153 | 154 | # 155 | # Custom checks can be created using `mix credo.gen.check`. 156 | # 157 | ] 158 | } 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | _build 3 | .elixir_ls 4 | .vscode 5 | db 6 | deps 7 | *.ez 8 | .DS_Store 9 | .git 10 | 11 | # Generated on crash by the VM 12 | erl_crash.dump 13 | 14 | # Files matching config/*.secret.exs pattern contain sensitive 15 | # data and you should not commit them into version control. 16 | # 17 | # Alternatively, you may comment the line below and commit the 18 | # secrets files as long as you replace their contents by environment 19 | # variables. 20 | config/*.secret.exs 21 | 22 | priv/data/* 23 | !priv/data/.gitkeep 24 | 25 | Dockerfile 26 | docker-compose.yml 27 | .gitignore 28 | LICENCE 29 | readme.md -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | _build 3 | .elixir_ls 4 | .vscode 5 | db 6 | deps 7 | *.ez 8 | .DS_Store 9 | 10 | # Generated on crash by the VM 11 | erl_crash.dump 12 | 13 | # Files matching config/*.secret.exs pattern contain sensitive 14 | # data and you should not commit them into version control. 15 | # 16 | # Alternatively, you may comment the line below and commit the 17 | # secrets files as long as you replace their contents by environment 18 | # variables. 19 | config/*.secret.exs 20 | 21 | priv/data/* 22 | !priv/data/.gitkeep 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.rulers": [120], 4 | "[elixir]": { 5 | "editor.insertSpaces": true, 6 | "editor.tabSize": 2, 7 | "editor.wordBasedSuggestions": false, 8 | "editor.formatOnType": true, 9 | "editor.acceptSuggestionOnEnter": "on", 10 | "editor.trimAutoWhitespace": false, 11 | "files.trimTrailingWhitespace": true, 12 | "files.insertFinalNewline": true 13 | }, 14 | "elixirLinter.useStrict": true, 15 | "elixirLS.suggestSpecs": false, 16 | "elixirLS.dialyzerEnabled": true 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:1.7.4-alpine 2 | MAINTAINER Melih Değiş 3 | 4 | RUN mix local.hex --force \ 5 | && apk --update add build-base postgresql-dev postgresql-client inotify-tools nodejs \ 6 | && mix archive.install --force https://github.com/phoenixframework/archives/raw/master/phx_new-1.3.3.ez \ 7 | && mix local.rebar --force \ 8 | && rm -rf /var/cache/apk/* 9 | 10 | RUN mkdir -p /app 11 | COPY . /app 12 | WORKDIR /app 13 | 14 | RUN mix local.hex --force \ 15 | && mix deps.get 16 | 17 | EXPOSE 4000 18 | 19 | CMD [ "mix", "phx.server" ] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Alpcan AYDIN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github.ist API 2 | 3 | This is the API repo for https://github.ist. You may also want to take a look to [Web](https://github.com/alpcanaydin/githubist) and [Fetcher](https://github.com/alpcanaydin/githubist-fetcher) 4 | 5 | ## Installation 6 | 7 | Before the installation, please provide the seed data via [Fetcher](https://github.com/alpcanaydin/githubist-fetcher). You can find the instructions in the fetcher repo. 8 | 9 | ### Docker 10 | 11 | - Install dependencies with `./mix deps.get` 12 | - Create and migrate your database with `./mix ecto.create && ./mix ecto.migrate` 13 | - Seed the database with `./mix run priv/repo/seeds.exs` 14 | 15 | #### Executing Custom Commands 16 | 17 | To run commands other than mix tasks, you can use the `./run` script. 18 | 19 | `./run iex -S mix` 20 | 21 | ### Traditional Setup 22 | 23 | - Change directory to src with `cd src/` 24 | - Install dependencies with `mix deps.get` 25 | - Create and migrate your database with `mix ecto.create && mix ecto.migrate` 26 | - Seed the database with `mix run priv/repo/seeds.exs` 27 | 28 | # Starting the API 29 | 30 | You can start the API with `mix phx.server` command. You can visit [`http://0.0.0.0:4000`](http://0.0.0.0:4000) from your browser. 31 | 32 | ## Wıth Docker 33 | 34 | You can start the API with `docker-compose up`. You can check it via `curl 'http://localhost:4000/graphql' -H 'content-type: application/json' --data-binary '{"operationName":null,"variables":{"username":"mdegis"},"query":"query ($username: String!) {\n developer(username: $username) {\n ...BasicDeveloper\n bio\n githubUrl\n __typename\n }\n}\n\nfragment BasicDeveloper on Developer {\n id\n name\n username\n avatarUrl\n __typename\n}\n"}'` -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker build -t githubist:latest . 3 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :githubist, 10 | ecto_repos: [Githubist.Repo] 11 | 12 | # Configures the endpoint 13 | config :githubist, GithubistWeb.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "0x1WP1wBkdoyhw9PdEb4mLLfQOjpiJoBu4TSXDYP1hhkBVikdRIndZla0vp4zdH0", 16 | render_errors: [view: GithubistWeb.ErrorView, accepts: ~w(json)], 17 | pubsub: [name: Githubist.PubSub, adapter: Phoenix.PubSub.PG2] 18 | 19 | # Configures Elixir's Logger 20 | config :logger, :console, 21 | format: "$time $metadata[$level] $message\n", 22 | metadata: [:user_id] 23 | 24 | # Import environment specific config. This must remain at the bottom 25 | # of this file so it overrides the configuration defined above. 26 | import_config "#{Mix.env()}.exs" 27 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :githubist, GithubistWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [] 15 | 16 | # ## SSL Support 17 | # 18 | # In order to use HTTPS in development, a self-signed 19 | # certificate can be generated by running the following 20 | # command from your terminal: 21 | # 22 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 23 | # 24 | # The `http:` config above can be replaced with: 25 | # 26 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 27 | # 28 | # If desired, both `http:` and `https:` keys can be 29 | # configured to run both http and https servers on 30 | # different ports. 31 | 32 | # Do not include metadata nor timestamps in development logs 33 | config :logger, :console, format: "[$level] $message\n" 34 | 35 | # Set a higher stacktrace during development. Avoid configuring such 36 | # in production as building large stacktraces may be expensive. 37 | config :phoenix, :stacktrace_depth, 20 38 | 39 | # Configure your database 40 | config :githubist, Githubist.Repo, 41 | adapter: Ecto.Adapters.Postgres, 42 | username: System.get_env("GITHUBIST_DOCKER_DATABASE_USER") || "postgres", 43 | password: System.get_env("GITHUBIST_DOCKER_DATABASE_PASS") || "postgres", 44 | database: System.get_env("GITHUBIST_DOCKER_DATABASE_NAME") || "githubist_dev", 45 | hostname: System.get_env("GITHUBIST_DOCKER_DATABASE_HOST") || "localhost", 46 | pool_size: 10 47 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # GithubistWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :githubist, GithubistWeb.Endpoint, 17 | load_from_system_env: true, 18 | url: [host: "example.com", port: 80], 19 | cache_static_manifest: "priv/static/cache_manifest.json" 20 | 21 | # Do not print debug messages in production 22 | config :logger, level: :info 23 | 24 | # ## SSL Support 25 | # 26 | # To get SSL working, you will need to add the `https` key 27 | # to the previous section and set your `:url` port to 443: 28 | # 29 | # config :githubist, GithubistWeb.Endpoint, 30 | # ... 31 | # url: [host: "example.com", port: 443], 32 | # https: [:inet6, 33 | # port: 443, 34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 36 | # 37 | # Where those two env variables return an absolute path to 38 | # the key and cert in disk or a relative path inside priv, 39 | # for example "priv/ssl/server.key". 40 | # 41 | # We also recommend setting `force_ssl`, ensuring no data is 42 | # ever sent via http, always redirecting to https: 43 | # 44 | # config :githubist, GithubistWeb.Endpoint, 45 | # force_ssl: [hsts: true] 46 | # 47 | # Check `Plug.SSL` for all available options in `force_ssl`. 48 | 49 | # ## Using releases 50 | # 51 | # If you are doing OTP releases, you need to instruct Phoenix 52 | # to start the server for all endpoints: 53 | # 54 | # config :phoenix, :serve_endpoints, true 55 | # 56 | # Alternatively, you can configure exactly which server to 57 | # start per endpoint: 58 | # 59 | # config :githubist, GithubistWeb.Endpoint, server: true 60 | # 61 | 62 | # Finally import the config/prod.secret.exs 63 | # which should be versioned separately. 64 | import_config "prod.secret.exs" 65 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :githubist, GithubistWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :githubist, Githubist.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "githubist_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2' 3 | 4 | services: 5 | 6 | app: 7 | image: githubist:latest 8 | build: . 9 | ports: 10 | - "4000:4000" 11 | depends_on: 12 | - db 13 | environment: 14 | - GITHUBIST_DOCKER_DATABASE_HOST=db 15 | - GITHUBIST_DOCKER_DATABASE_USER=githubist 16 | - GITHUBIST_DOCKER_DATABASE_PASS=githubist 17 | - GITHUBIST_DOCKER_DATABASE_NAME=githubist 18 | 19 | db: 20 | image: postgres:10-alpine 21 | restart: always 22 | ports: 23 | - "5432:5432" 24 | volumes: 25 | - db:/var/lib/postgresql/data 26 | environment: 27 | - POSTGRES_USER=githubist 28 | - POSTGRES_DB=githubist 29 | - POSTGRES_PASSWORD=githubist 30 | 31 | volumes: 32 | db: -------------------------------------------------------------------------------- /lib/githubist.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist do 2 | @moduledoc """ 3 | Githubist keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/githubist/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | alias GithubistWeb.Endpoint 7 | 8 | # See https://hexdocs.pm/elixir/Application.html 9 | # for more information on OTP Applications 10 | def start(_type, _args) do 11 | import Supervisor.Spec 12 | 13 | # Define workers and child supervisors to be supervised 14 | children = [ 15 | # Start the Ecto repository 16 | supervisor(Githubist.Repo, []), 17 | # Start the endpoint when the application starts 18 | supervisor(GithubistWeb.Endpoint, []) 19 | # Start your own worker by calling: Githubist.Worker.start_link(arg1, arg2, arg3) 20 | # worker(Githubist.Worker, [arg1, arg2, arg3]), 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: Githubist.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | def config_change(changed, _new, removed) do 32 | Endpoint.config_change(changed, removed) 33 | :ok 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/githubist/argument_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.GraphQLArgumentParser do 2 | @moduledoc """ 3 | Some helpers to parse limit and order by 4 | """ 5 | 6 | @spec parse_limit(map()) :: map() 7 | def parse_limit(params, opts \\ []) do 8 | Map.update!(params, :limit, fn limit -> 9 | update_limit(limit, Keyword.get(opts, :max_limit, 100)) 10 | end) 11 | end 12 | 13 | @spec parse_order_by(map()) :: map() 14 | def parse_order_by(%{order_by: _} = params) do 15 | Map.update!(params, :order_by, fn order_by -> 16 | {order_by.direction, order_by.field} 17 | end) 18 | end 19 | 20 | def parse_order_by(params) do 21 | params 22 | end 23 | 24 | defp update_limit(limit, max_limit) when is_integer(max_limit) do 25 | max(min(limit, max_limit), 0) 26 | end 27 | 28 | defp update_limit(limit, nil) do 29 | limit 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/githubist/developers/developer.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Developers.Developer do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | import Ecto.Changeset 6 | 7 | alias Ecto.Changeset 8 | alias Githubist.Locations.Location 9 | alias Githubist.Repositories.Repository 10 | 11 | @type t :: %__MODULE__{} 12 | 13 | schema "developers" do 14 | field(:username, :string) 15 | field(:email, :string) 16 | field(:github_id, :integer) 17 | field(:name, :string) 18 | field(:avatar_url, :string) 19 | field(:bio, :string) 20 | field(:company, :string) 21 | field(:github_location, :string) 22 | field(:github_url, :string) 23 | field(:followers, :integer) 24 | field(:following, :integer) 25 | field(:public_repos, :integer) 26 | field(:total_starred, :integer) 27 | field(:score, :float) 28 | field(:github_created_at, :utc_datetime) 29 | 30 | belongs_to(:location, Location) 31 | has_many(:repositories, Repository) 32 | 33 | timestamps() 34 | end 35 | 36 | @doc false 37 | @spec changeset(__MODULE__.t(), map()) :: Changeset.t() 38 | def changeset(developer, attrs) do 39 | developer 40 | |> cast(attrs, [ 41 | :username, 42 | :email, 43 | :github_id, 44 | :name, 45 | :avatar_url, 46 | :bio, 47 | :company, 48 | :github_url, 49 | :github_location, 50 | :followers, 51 | :following, 52 | :public_repos, 53 | :total_starred, 54 | :score, 55 | :github_created_at, 56 | :location_id 57 | ]) 58 | |> validate_required([ 59 | :username, 60 | :github_id, 61 | :avatar_url, 62 | :github_url, 63 | :github_location, 64 | :followers, 65 | :following, 66 | :public_repos, 67 | :total_starred, 68 | :score, 69 | :github_created_at, 70 | :location_id 71 | ]) 72 | |> unique_constraint(:username) 73 | |> foreign_key_constraint(:location_id) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/githubist/developers/developers.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Developers do 2 | @moduledoc """ 3 | The Developers context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias Githubist.Repo 8 | 9 | alias Ecto.Changeset 10 | alias Githubist.Developers.Developer 11 | alias Githubist.Repositories 12 | alias Githubist.Repositories.Repository 13 | 14 | @type order_direction :: :desc | :asc 15 | 16 | @type order_field :: 17 | :name | :username | :score | :total_starred | :followers | :github_created_at 18 | 19 | @type list_params :: %{ 20 | limit: integer(), 21 | offset: integer(), 22 | order_by: {order_direction(), order_field()} 23 | } 24 | 25 | @type search_result :: %{name: String.t(), slug: String.t(), type: :developer} 26 | 27 | @doc """ 28 | Gets a single developer. 29 | """ 30 | @spec get_developer(integer()) :: Developer.t() | nil 31 | def get_developer(id), do: Repo.get(Developer, id) 32 | 33 | @doc """ 34 | Gets a single developer and raise an exception if it does not exist. 35 | """ 36 | @spec get_developer!(integer()) :: Developer.t() | no_return() 37 | def get_developer!(id), do: Repo.get!(Developer, id) 38 | 39 | @doc """ 40 | Gets a single developer by username. 41 | """ 42 | @spec get_developer_by_username(String.t()) :: Developer.t() | nil 43 | def get_developer_by_username(username), do: Repo.get_by(Developer, username: username) 44 | 45 | @doc """ 46 | Gets a single developer by username and raise an exception if it does not exist. 47 | """ 48 | @spec get_developer_by_username!(String.t()) :: Developer.t() | no_return() 49 | def get_developer_by_username!(username), do: Repo.get_by!(Developer, username: username) 50 | 51 | @doc """ 52 | Creates a developer. 53 | """ 54 | @spec create_developer(map()) :: {:ok, Developer.t()} | {:error, Changeset.t()} 55 | def create_developer(attrs \\ %{}) do 56 | %Developer{} 57 | |> Developer.changeset(attrs) 58 | |> Repo.insert() 59 | end 60 | 61 | @doc """ 62 | Get all developers with limit and order 63 | """ 64 | @spec all(list_params()) :: list(Developer.t()) 65 | def all(%{limit: limit, offset: offset, order_by: order_by}) do 66 | query = 67 | from(Developer, order_by: ^order_by, order_by: {:asc, :id}, limit: ^limit, offset: ^offset) 68 | 69 | Repo.all(query) 70 | end 71 | 72 | @doc """ 73 | Get developers count 74 | """ 75 | @spec get_developers_count() :: integer() 76 | def get_developers_count do 77 | query = from(d in Developer, select: count(d.id)) 78 | 79 | Repo.one(query) 80 | end 81 | 82 | @doc """ 83 | Get repositories of a developer with limit and order 84 | """ 85 | @spec get_repositories(Developer.t(), Repositories.list_params()) :: list(Repository.t()) 86 | def get_repositories(%Developer{} = developer, params) do 87 | query = 88 | from(r in Repository, 89 | where: r.developer_id == ^developer.id, 90 | order_by: ^params.order_by, 91 | order_by: {:asc, :id}, 92 | limit: ^params.limit, 93 | offset: ^params.offset 94 | ) 95 | 96 | Repo.all(query) 97 | end 98 | 99 | @doc """ 100 | Get the position of developer 101 | """ 102 | @spec get_rank(Developer.t(), :turkey | :in_location) :: integer() 103 | def get_rank(%Developer{} = developer, type) do 104 | # credo:disable-for-lines:3 105 | rank_query = 106 | (d in Developer) 107 | |> from() 108 | |> select([d], %{id: d.id, rank: fragment("RANK() OVER(ORDER BY ? DESC)", d.score)}) 109 | |> maybe_location_for_rank(developer, type) 110 | 111 | query = from(r in subquery(rank_query), select: r.rank, where: r.id == ^developer.id) 112 | 113 | Repo.one(query) 114 | end 115 | 116 | @doc """ 117 | Search developers for given term 118 | """ 119 | @spec search(String.t(), integer()) :: list(search_result) 120 | def search(term, limit) do 121 | search_term = "%#{term}%" 122 | 123 | query = 124 | from(d in Developer, 125 | where: ilike(d.name, ^search_term), 126 | or_where: ilike(d.username, ^search_term), 127 | order_by: {:desc, d.score}, 128 | limit: ^limit 129 | ) 130 | 131 | mapper = fn developer -> 132 | %{ 133 | name: developer.name, 134 | slug: developer.username, 135 | type: :developer 136 | } 137 | end 138 | 139 | query 140 | |> Repo.all() 141 | |> Enum.map(mapper) 142 | end 143 | 144 | @doc """ 145 | Get repositories count of developer 146 | """ 147 | @spec get_repositories_count(Developer.t()) :: integer() 148 | def get_repositories_count(%Developer{} = developer) do 149 | query = from(r in Repository, select: count(r.id), where: r.developer_id == ^developer.id) 150 | 151 | Repo.one(query) 152 | end 153 | 154 | defp maybe_location_for_rank(query, developer, type) do 155 | case type do 156 | :turkey -> query 157 | :in_location -> query |> where([d], d.location_id == ^developer.location_id) 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/githubist/import_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.ImportHelpers do 2 | @moduledoc """ 3 | JSON import helpers 4 | """ 5 | 6 | @doc """ 7 | Capitalize a word as Turkish sensetive 8 | """ 9 | @spec capitalize(String.t()) :: String.t() 10 | def capitalize(word) do 11 | word 12 | |> String.replace_prefix("i", "İ") 13 | |> String.capitalize() 14 | end 15 | 16 | @doc """ 17 | Create location slug 18 | """ 19 | @spec create_location_slug(String.t()) :: String.t() 20 | def create_location_slug(name) do 21 | name |> capitalize() |> Slug.slugify() 22 | end 23 | 24 | @doc """ 25 | Create language slug 26 | """ 27 | @spec create_language_slug(String.t()) :: String.t() 28 | def create_language_slug(name) do 29 | name 30 | |> String.replace("#", "sharp") 31 | |> String.replace("+", "plus") 32 | |> Slug.slugify() 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/githubist/languages/language.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Languages.Language do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | import Ecto.Changeset 6 | 7 | alias Ecto.Changeset 8 | alias Githubist.Repositories.Repository 9 | 10 | @type t :: %__MODULE__{} 11 | 12 | schema "languages" do 13 | field(:name, :string) 14 | field(:slug, :string) 15 | field(:score, :float) 16 | field(:total_stars, :integer) 17 | field(:total_repositories, :integer) 18 | field(:total_developers, :integer) 19 | 20 | has_many(:repositories, Repository) 21 | 22 | timestamps() 23 | end 24 | 25 | @doc false 26 | @spec changeset(__MODULE__.t(), map()) :: Changeset.t() 27 | def changeset(language, attrs) do 28 | language 29 | |> cast(attrs, [:name, :slug, :score, :total_stars, :total_repositories, :total_developers]) 30 | |> validate_required([ 31 | :name, 32 | :slug, 33 | :score, 34 | :total_stars, 35 | :total_repositories, 36 | :total_developers 37 | ]) 38 | |> unique_constraint(:slug) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/githubist/languages/languages.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Languages do 2 | @moduledoc """ 3 | The Languages context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias Githubist.Repo 8 | 9 | alias Ecto.Changeset 10 | alias Githubist.Developers 11 | alias Githubist.Developers.Developer 12 | alias Githubist.Languages.Language 13 | alias Githubist.Locations 14 | alias Githubist.Locations.Location 15 | alias Githubist.Repositories 16 | alias Githubist.Repositories.Repository 17 | 18 | @type order_direction :: :desc | :asc 19 | 20 | @type order_field :: :name | :score | :total_stars | :total_repositories | :total_developers 21 | 22 | @type usage_params :: %{limit: integer(), offset: integer()} 23 | 24 | @type list_params :: %{ 25 | limit: integer(), 26 | offset: integer(), 27 | order_by: {order_direction(), order_field()} 28 | } 29 | 30 | @type search_result :: %{name: String.t(), slug: String.t(), type: :language} 31 | 32 | @doc """ 33 | Gets a single language. 34 | """ 35 | @spec get_language(integer()) :: Developer.t() | nil 36 | def get_language(id), do: Repo.get(Language, id) 37 | 38 | @doc """ 39 | Gets a single language and raise an exception if it does not exist. 40 | """ 41 | @spec get_language!(integer()) :: Developer.t() | no_return() 42 | def get_language!(id), do: Repo.get!(Language, id) 43 | 44 | @doc """ 45 | Gets a single language by slug. 46 | """ 47 | @spec get_language_by_slug(String.t()) :: Developer.t() | nil 48 | def get_language_by_slug(slug), do: Repo.get_by(Language, slug: slug) 49 | 50 | @doc """ 51 | Gets a single language by slug and raise an exception if it does not exist. 52 | """ 53 | @spec get_language_by_slug!(String.t()) :: Developer.t() | no_return() 54 | def get_language_by_slug!(slug), do: Repo.get_by!(Language, slug: slug) 55 | 56 | @doc """ 57 | Creates a language. 58 | """ 59 | @spec create_language(map()) :: {:ok, Language.t()} | {:error, Changeset.t()} 60 | def create_language(attrs \\ %{}) do 61 | %Language{} 62 | |> Language.changeset(attrs) 63 | |> Repo.insert() 64 | end 65 | 66 | @doc """ 67 | Get all languages with limit and order 68 | """ 69 | @spec all(list_params()) :: list(Developer.t()) 70 | def all(%{limit: limit, offset: offset, order_by: order_by}) do 71 | query = 72 | from(Language, order_by: ^order_by, order_by: {:asc, :id}, limit: ^limit, offset: ^offset) 73 | 74 | Repo.all(query) 75 | end 76 | 77 | @doc """ 78 | Get languages count 79 | """ 80 | @spec get_languages_count() :: integer() 81 | def get_languages_count do 82 | query = from(l in Language, select: count(l.id)) 83 | 84 | Repo.one(query) 85 | end 86 | 87 | @doc """ 88 | Get repositories in a language with limit and order 89 | """ 90 | @spec get_repositories(Language.t(), Repositories.list_params()) :: list(Repository.t()) 91 | def get_repositories(%Language{} = language, params) do 92 | query = 93 | from(r in Repository, 94 | where: r.language_id == ^language.id, 95 | order_by: ^params.order_by, 96 | order_by: {:asc, :id}, 97 | limit: ^params.limit, 98 | offset: ^params.offset 99 | ) 100 | 101 | Repo.all(query) 102 | end 103 | 104 | @doc """ 105 | Get the postion of language 106 | """ 107 | @spec get_rank(Language.t()) :: integer() 108 | def get_rank(%Language{} = language) do 109 | rank_query = 110 | from(l in Language, 111 | select: %{id: l.id, rank: fragment("RANK() OVER(ORDER BY ? DESC)", l.score)} 112 | ) 113 | 114 | query = from(r in subquery(rank_query), select: r.rank, where: r.id == ^language.id) 115 | 116 | Repo.one(query) 117 | end 118 | 119 | @doc """ 120 | Get the postion of language according to repositories count 121 | """ 122 | @spec get_repositories_count_rank(Language.t()) :: integer() 123 | def get_repositories_count_rank(%Language{} = language) do 124 | rank_query = 125 | from(l in Language, 126 | select: %{id: l.id, rank: fragment("RANK() OVER(ORDER BY ? DESC)", l.total_repositories)} 127 | ) 128 | 129 | query = from(r in subquery(rank_query), select: r.rank, where: r.id == ^language.id) 130 | 131 | Repo.one(query) 132 | end 133 | 134 | @doc """ 135 | Get the postion of language according to developers count 136 | """ 137 | @spec get_developers_count_rank(Language.t()) :: integer() 138 | def get_developers_count_rank(%Language{} = language) do 139 | rank_query = 140 | from(l in Language, 141 | select: %{id: l.id, rank: fragment("RANK() OVER(ORDER BY ? DESC)", l.total_developers)} 142 | ) 143 | 144 | query = from(r in subquery(rank_query), select: r.rank, where: r.id == ^language.id) 145 | 146 | Repo.one(query) 147 | end 148 | 149 | @doc """ 150 | Get repositories count which uses the given language 151 | """ 152 | @spec get_repositories_count(Language.t()) :: integer() 153 | def get_repositories_count(%Language{} = language) do 154 | query = from(r in Repository, select: count(r.id), where: r.language_id == ^language.id) 155 | 156 | Repo.one(query) 157 | end 158 | 159 | @doc """ 160 | Get repositories count which uses the given language 161 | """ 162 | @spec get_developers_count(Language.t()) :: integer() 163 | def get_developers_count(%Language{} = language) do 164 | query = 165 | from(r in Repository, 166 | select: count(r.developer_id, :distinct), 167 | where: r.language_id == ^language.id 168 | ) 169 | 170 | Repo.one(query) 171 | end 172 | 173 | @doc """ 174 | Get language usage in a location 175 | """ 176 | @spec get_location_usage(Location.t(), usage_params()) :: 177 | list(%{language: Language.t(), repositories_count: integer()}) 178 | def get_location_usage(%Location{} = location, %{limit: limit, offset: offset}) do 179 | query = 180 | from(r in Repository, 181 | select: %{repositories_count: count(r.id), language_id: r.language_id}, 182 | join: d in Developer, 183 | on: d.id == r.developer_id, 184 | where: d.location_id == ^location.id, 185 | group_by: r.language_id, 186 | order_by: [desc: count(r.id)], 187 | order_by: {:asc, r.language_id}, 188 | limit: ^limit, 189 | offset: ^offset 190 | ) 191 | 192 | results = Repo.all(query) 193 | 194 | Enum.map(results, fn item -> 195 | %{ 196 | language: get_language(item.language_id), 197 | repositories_count: item.repositories_count 198 | } 199 | end) 200 | end 201 | 202 | @doc """ 203 | Get language usage for a developer 204 | """ 205 | @spec get_developer_usage(Developer.t(), usage_params()) :: 206 | list(%{language: Language.t(), repositories_count: integer()}) 207 | def get_developer_usage(%Developer{} = developer, %{limit: limit, offset: offset}) do 208 | query = 209 | from(r in Repository, 210 | select: %{repositories_count: count(r.id), language_id: r.language_id}, 211 | where: r.developer_id == ^developer.id, 212 | group_by: r.language_id, 213 | order_by: [desc: count(r.id)], 214 | order_by: {:asc, r.language_id}, 215 | limit: ^limit, 216 | offset: ^offset 217 | ) 218 | 219 | results = Repo.all(query) 220 | 221 | Enum.map(results, fn item -> 222 | %{ 223 | language: get_language(item.language_id), 224 | repositories_count: item.repositories_count 225 | } 226 | end) 227 | end 228 | 229 | @doc """ 230 | Get location stats for a language 231 | """ 232 | @spec get_location_stats(Language.t(), usage_params()) :: 233 | list(%{location: Location.t(), repositories_count: integer()}) 234 | def get_location_stats(%Language{} = language, %{limit: limit, offset: offset}) do 235 | query = 236 | from(r in Repository, 237 | join: d in Developer, 238 | on: d.id == r.developer_id, 239 | select: %{repositories_count: count(r.id), location_id: d.location_id}, 240 | where: r.language_id == ^language.id, 241 | group_by: d.location_id, 242 | order_by: [desc: count(r.id)], 243 | order_by: {:asc, d.location_id}, 244 | limit: ^limit, 245 | offset: ^offset 246 | ) 247 | 248 | results = Repo.all(query) 249 | 250 | Enum.map(results, fn item -> 251 | %{ 252 | location: Locations.get_location(item.location_id), 253 | repositories_count: item.repositories_count 254 | } 255 | end) 256 | end 257 | 258 | @doc """ 259 | Get developer stats for a language 260 | """ 261 | @spec get_developer_stats(Language.t(), usage_params()) :: 262 | list(%{developer: Developer.t(), repositories_count: integer()}) 263 | def get_developer_stats(%Language{} = language, %{limit: limit, offset: offset}) do 264 | query = 265 | from(r in Repository, 266 | select: %{repositories_count: count(r.id), developer_id: r.developer_id}, 267 | where: r.language_id == ^language.id, 268 | group_by: r.developer_id, 269 | order_by: [desc: count(r.id)], 270 | order_by: {:asc, r.developer_id}, 271 | limit: ^limit, 272 | offset: ^offset 273 | ) 274 | 275 | results = Repo.all(query) 276 | 277 | Enum.map(results, fn item -> 278 | %{ 279 | developer: Developers.get_developer(item.developer_id), 280 | repositories_count: item.repositories_count 281 | } 282 | end) 283 | end 284 | 285 | @doc """ 286 | Search languages for given term 287 | """ 288 | @spec search(String.t(), integer()) :: list(search_result) 289 | def search(term, limit) do 290 | search_term = "%#{term}%" 291 | 292 | query = 293 | from(l in Language, 294 | where: ilike(l.name, ^search_term), 295 | order_by: {:desc, l.score}, 296 | limit: ^limit 297 | ) 298 | 299 | mapper = fn language -> 300 | %{ 301 | name: language.name, 302 | slug: language.slug, 303 | type: :language 304 | } 305 | end 306 | 307 | query 308 | |> Repo.all() 309 | |> Enum.map(mapper) 310 | end 311 | end 312 | -------------------------------------------------------------------------------- /lib/githubist/loaders.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Loaders do 2 | @moduledoc false 3 | 4 | import Ecto.Query, warn: false 5 | 6 | alias Dataloader.Ecto, as: DataloaderEcto 7 | 8 | def data do 9 | DataloaderEcto.new(Githubist.Repo, query: &query/2) 10 | end 11 | 12 | def query(queryable, _params) do 13 | queryable 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/githubist/locations/location.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Locations.Location do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | import Ecto.Changeset 6 | 7 | alias Ecto.Changeset 8 | alias Githubist.Developers.Developer 9 | 10 | @type t :: %__MODULE__{} 11 | 12 | schema "locations" do 13 | field(:name, :string) 14 | field(:slug, :string) 15 | field(:score, :float) 16 | field(:total_repositories, :integer) 17 | field(:total_developers, :integer) 18 | 19 | has_many(:developers, Developer) 20 | 21 | timestamps() 22 | end 23 | 24 | @doc false 25 | @spec changeset(__MODULE__.t(), map()) :: Changeset.t() 26 | def changeset(location, attrs) do 27 | location 28 | |> cast(attrs, [:name, :slug, :score, :total_repositories, :total_developers]) 29 | |> validate_required([:name, :slug, :score, :total_repositories, :total_developers]) 30 | |> unique_constraint(:slug) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/githubist/locations/locations.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Locations do 2 | @moduledoc """ 3 | The Locations context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias Githubist.Repo 8 | 9 | alias Ecto.Changeset 10 | alias Githubist.Developers 11 | alias Githubist.Developers.Developer 12 | alias Githubist.Locations.Location 13 | alias Githubist.Repositories 14 | alias Githubist.Repositories.Repository 15 | 16 | @type order_direction :: :desc | :asc 17 | 18 | @type order_field :: :name | :score | :total_repositories | :total_developers 19 | 20 | @type list_params :: %{ 21 | limit: integer(), 22 | offset: integer(), 23 | order_by: {order_direction(), order_field()} 24 | } 25 | 26 | @type search_result :: %{name: String.t(), slug: String.t(), type: :location} 27 | 28 | @doc """ 29 | Gets a single location. 30 | """ 31 | @spec get_location(integer()) :: Location.t() | nil 32 | def get_location(id), do: Repo.get(Location, id) 33 | 34 | @doc """ 35 | Gets a single location and raise an exception if it does not exist. 36 | """ 37 | @spec get_location!(integer()) :: Location.t() | no_return() 38 | def get_location!(id), do: Repo.get!(Location, id) 39 | 40 | @doc """ 41 | Gets a single location by slug. 42 | """ 43 | @spec get_location_by_slug(String.t()) :: Location.t() | nil 44 | def get_location_by_slug(slug), do: Repo.get_by(Location, slug: slug) 45 | 46 | @doc """ 47 | Gets a single location by slug and raise an exception if it does not exist. 48 | """ 49 | @spec get_location_by_slug!(String.t()) :: Location.t() | no_return() 50 | def get_location_by_slug!(slug), do: Repo.get_by!(Location, slug: slug) 51 | 52 | @doc """ 53 | Creates a location. 54 | """ 55 | @spec create_location(map()) :: {:ok, Location.t()} | {:error, Changeset.t()} 56 | def create_location(attrs \\ %{}) do 57 | %Location{} 58 | |> Location.changeset(attrs) 59 | |> Repo.insert() 60 | end 61 | 62 | @doc """ 63 | Get all locations with limit and order 64 | """ 65 | @spec all(list_params()) :: list(Location.t()) 66 | def all(%{limit: limit, order_by: order_by, offset: offset}) do 67 | query = 68 | from(Location, order_by: ^order_by, order_by: {:asc, :id}, limit: ^limit, offset: ^offset) 69 | 70 | Repo.all(query) 71 | end 72 | 73 | @doc """ 74 | Get locations count 75 | """ 76 | @spec get_locations_count() :: integer() 77 | def get_locations_count do 78 | query = from(l in Location, select: count(l.id)) 79 | 80 | Repo.one(query) 81 | end 82 | 83 | @doc """ 84 | Get developers at the location with limit and order 85 | """ 86 | @spec get_developers(Location.t(), Developers.list_params()) :: list(Developer.t()) 87 | def get_developers(%Location{} = location, params) do 88 | query = 89 | from(d in Developer, 90 | where: d.location_id == ^location.id, 91 | order_by: ^params.order_by, 92 | order_by: {:asc, :id}, 93 | limit: ^params.limit, 94 | offset: ^params.offset 95 | ) 96 | 97 | Repo.all(query) 98 | end 99 | 100 | @doc """ 101 | Get repositories at the location 102 | """ 103 | @spec get_repositories(Location.t(), Repositories.list_params()) :: list(Repository.t()) 104 | def get_repositories(%Location{} = location, params) do 105 | query = 106 | from(r in Repository, 107 | select: r, 108 | join: d in Developer, 109 | on: d.id == r.developer_id, 110 | where: d.location_id == ^location.id, 111 | order_by: ^params.order_by, 112 | order_by: {:asc, :id}, 113 | limit: ^params.limit, 114 | offset: ^params.offset 115 | ) 116 | 117 | Repo.all(query) 118 | end 119 | 120 | @doc """ 121 | Get the postion of location 122 | """ 123 | @spec get_rank(Location.t()) :: integer() 124 | def get_rank(%Location{} = location) do 125 | rank_query = 126 | from(l in Location, 127 | select: %{id: l.id, rank: fragment("RANK() OVER(ORDER BY ? DESC)", l.score)} 128 | ) 129 | 130 | query = from(r in subquery(rank_query), select: r.rank, where: r.id == ^location.id) 131 | 132 | Repo.one(query) 133 | end 134 | 135 | @doc """ 136 | Get count of developers in a location 137 | """ 138 | @spec get_developers_count(Location.t()) :: integer() 139 | def get_developers_count(%Location{} = location) do 140 | query = from(d in Developer, select: count(d.id), where: d.location_id == ^location.id) 141 | 142 | Repo.one(query) 143 | end 144 | 145 | @doc """ 146 | Get repositories count of developers who lives in a location 147 | """ 148 | @spec get_repositories_count(Location.t()) :: integer() 149 | def get_repositories_count(%Location{} = location) do 150 | query = 151 | from(r in Repository, 152 | select: count(r.id), 153 | join: d in Developer, 154 | on: d.id == r.developer_id, 155 | where: d.location_id == ^location.id 156 | ) 157 | 158 | Repo.one(query) 159 | end 160 | 161 | @doc """ 162 | Search locations for given term 163 | """ 164 | @spec search(String.t(), integer()) :: list(search_result) 165 | def search(term, limit) do 166 | search_term = "%#{term}%" 167 | 168 | query = 169 | from(l in Location, where: ilike(l.name, ^search_term), order_by: l.score, limit: ^limit) 170 | 171 | mapper = fn location -> 172 | %{ 173 | name: location.name, 174 | slug: location.slug, 175 | type: :location 176 | } 177 | end 178 | 179 | query 180 | |> Repo.all() 181 | |> Enum.map(mapper) 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/githubist/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo do 2 | use Ecto.Repo, otp_app: :githubist 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/githubist/repositories/repositories.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repositories do 2 | @moduledoc """ 3 | The Repositories context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | 8 | alias Ecto.Changeset 9 | alias Githubist.Repo 10 | alias Githubist.Repositories.Repository 11 | 12 | @type order_direction :: :desc | :asc 13 | 14 | @type order_field :: :name | :stars | :forks | :github_created_at 15 | 16 | @type list_params :: %{ 17 | limit: integer(), 18 | offset: integer(), 19 | order_by: {order_direction(), order_field()} 20 | } 21 | 22 | @type search_result :: %{name: String.t(), slug: String.t(), type: :repository} 23 | 24 | @doc """ 25 | Gets a single repository. 26 | """ 27 | @spec get_repository(integer()) :: Repository.t() | nil 28 | def get_repository(id), do: Repo.get(Repository, id) 29 | 30 | @doc """ 31 | Gets a single repository and raise an exception if it does not exist. 32 | """ 33 | @spec get_repository!(integer()) :: Repository.t() | no_return() 34 | def get_repository!(id), do: Repo.get!(Repository, id) 35 | 36 | @doc """ 37 | Gets a single repository by slug. 38 | """ 39 | @spec get_repository_by_slug(String.t()) :: Repository.t() | nil 40 | def get_repository_by_slug(slug), do: Repo.get_by(Repository, slug: slug) 41 | 42 | @doc """ 43 | Gets a single repository by slug and raise an exception if it does not exist. 44 | """ 45 | @spec get_repository_by_slug!(String.t()) :: Repository.t() | no_return() 46 | def get_repository_by_slug!(slug), do: Repo.get_by!(Repository, slug: slug) 47 | 48 | @doc """ 49 | Creates a repository. 50 | """ 51 | @spec create_repository(map()) :: {:ok, Repository.t()} | {:error, Changeset.t()} 52 | def create_repository(attrs \\ %{}) do 53 | %Repository{} 54 | |> Repository.changeset(attrs) 55 | |> Repo.insert() 56 | end 57 | 58 | @doc """ 59 | Get all repositories with limit and order 60 | """ 61 | @spec all(list_params()) :: list(Repository.t()) 62 | def all(%{limit: limit, order_by: order_by, offset: offset}) do 63 | query = 64 | from(Repository, order_by: ^order_by, order_by: {:asc, :id}, limit: ^limit, offset: ^offset) 65 | 66 | Repo.all(query) 67 | end 68 | 69 | @doc """ 70 | Get repositories count 71 | """ 72 | @spec get_repositories_count() :: integer() 73 | def get_repositories_count do 74 | query = from(r in Repository, select: count(r.id)) 75 | 76 | Repo.one(query) 77 | end 78 | 79 | @doc """ 80 | Search repositories for given term 81 | """ 82 | @spec search(String.t(), integer()) :: list(search_result) 83 | def search(term, limit) do 84 | search_term = "%#{term}%" 85 | 86 | query = 87 | from(r in Repository, 88 | where: ilike(r.name, ^search_term), 89 | order_by: {:desc, r.stars}, 90 | limit: ^limit 91 | ) 92 | 93 | mapper = fn repository -> 94 | %{ 95 | name: repository.name, 96 | slug: repository.slug, 97 | type: :repository 98 | } 99 | end 100 | 101 | query 102 | |> Repo.all() 103 | |> Enum.map(mapper) 104 | end 105 | 106 | @doc """ 107 | Get the position of repository in Turkey 108 | """ 109 | @spec get_rank(Repository.t(), :turkey | :in_language) :: integer() 110 | def get_rank(%Repository{} = repository, type) do 111 | # credo:disable-for-lines:3 112 | rank_query = 113 | (r in Repository) 114 | |> from() 115 | |> select([r], %{id: r.id, rank: fragment("RANK() OVER(ORDER BY ? DESC)", r.stars)}) 116 | |> maybe_language_for_rank(repository, type) 117 | 118 | query = from(r in subquery(rank_query), select: r.rank, where: r.id == ^repository.id) 119 | 120 | Repo.one(query) 121 | end 122 | 123 | defp maybe_language_for_rank(query, repository, type) do 124 | case type do 125 | :turkey -> query 126 | :in_language -> query |> where([d], d.language_id == ^repository.language_id) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/githubist/repositories/repository.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repositories.Repository do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | import Ecto.Changeset 6 | 7 | alias Ecto.Changeset 8 | alias Githubist.Developers.Developer 9 | alias Githubist.Languages.Language 10 | 11 | @type t :: %__MODULE__{} 12 | 13 | schema "repositories" do 14 | field(:name, :string) 15 | field(:slug, :string) 16 | field(:description, :string) 17 | field(:github_id, :integer) 18 | field(:github_url, :string) 19 | field(:stars, :integer) 20 | field(:forks, :integer) 21 | field(:github_created_at, :utc_datetime) 22 | 23 | belongs_to(:developer, Developer) 24 | belongs_to(:language, Language) 25 | 26 | timestamps() 27 | end 28 | 29 | @doc false 30 | @spec changeset(__MODULE__.t(), map()) :: Changeset.t() 31 | def changeset(repository, attrs) do 32 | repository 33 | |> cast(attrs, [ 34 | :name, 35 | :slug, 36 | :description, 37 | :github_id, 38 | :github_url, 39 | :stars, 40 | :forks, 41 | :github_created_at, 42 | :developer_id, 43 | :language_id 44 | ]) 45 | |> validate_required([ 46 | :name, 47 | :slug, 48 | :github_id, 49 | :github_url, 50 | :stars, 51 | :forks, 52 | :github_created_at, 53 | :developer_id, 54 | :language_id 55 | ]) 56 | |> unique_constraint(:slug) 57 | |> foreign_key_constraint(:developer_id) 58 | |> foreign_key_constraint(:language_id) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/githubist_web.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use GithubistWeb, :controller 9 | use GithubistWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: GithubistWeb 23 | import Plug.Conn 24 | import GithubistWeb.Router.Helpers 25 | import GithubistWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/githubist_web/templates", 33 | namespace: GithubistWeb 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 37 | 38 | import GithubistWeb.Router.Helpers 39 | import GithubistWeb.ErrorHelpers 40 | import GithubistWeb.Gettext 41 | end 42 | end 43 | 44 | def router do 45 | quote do 46 | use Phoenix.Router 47 | import Plug.Conn 48 | import Phoenix.Controller 49 | end 50 | end 51 | 52 | def channel do 53 | quote do 54 | use Phoenix.Channel 55 | import GithubistWeb.Gettext 56 | end 57 | end 58 | 59 | @doc """ 60 | When used, dispatch to the appropriate controller/view/etc. 61 | """ 62 | defmacro __using__(which) when is_atom(which) do 63 | apply(__MODULE__, which, []) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/githubist_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", GithubistWeb.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # GithubistWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /lib/githubist_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :githubist 3 | 4 | socket("/socket", GithubistWeb.UserSocket) 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug(Plug.Static, 11 | at: "/", 12 | from: :githubist, 13 | gzip: false, 14 | only: ~w(css fonts images js favicon.ico robots.txt) 15 | ) 16 | 17 | # Code reloading can be explicitly enabled under the 18 | # :code_reloader configuration of your endpoint. 19 | if code_reloading? do 20 | plug(Phoenix.CodeReloader) 21 | end 22 | 23 | plug(Plug.Logger) 24 | 25 | plug(Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | ) 30 | 31 | plug(Plug.MethodOverride) 32 | plug(Plug.Head) 33 | 34 | # The session will be stored in the cookie and signed, 35 | # this means its contents can be read but not tampered with. 36 | # Set :encryption_salt if you would also like to encrypt it. 37 | plug(Plug.Session, 38 | store: :cookie, 39 | key: "_githubist_key", 40 | signing_salt: "QHr0uHEv" 41 | ) 42 | 43 | plug(GithubistWeb.Router) 44 | 45 | @doc """ 46 | Callback invoked for dynamically configuring the endpoint. 47 | 48 | It receives the endpoint configuration and checks if 49 | configuration should be loaded from the system environment. 50 | """ 51 | def init(_key, config) do 52 | if config[:load_from_system_env] do 53 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 54 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 55 | else 56 | {:ok, config} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/githubist_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import GithubistWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :githubist 24 | end 25 | -------------------------------------------------------------------------------- /lib/githubist_web/resolvers/developer_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Resolvers.DeveloperResolver do 2 | @moduledoc false 3 | 4 | alias Githubist.Developers 5 | alias Githubist.Developers.Developer 6 | alias Githubist.GraphQLArgumentParser 7 | alias Githubist.Locations 8 | alias Githubist.Locations.Location 9 | 10 | def all(%Location{} = location, params, _resolution) do 11 | params = 12 | params 13 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 14 | |> GraphQLArgumentParser.parse_order_by() 15 | 16 | developers = Locations.get_developers(location, params) 17 | 18 | {:ok, developers} 19 | end 20 | 21 | def all(_parent, params, _resolution) do 22 | params = 23 | params 24 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 25 | |> GraphQLArgumentParser.parse_order_by() 26 | 27 | developers = Developers.all(params) 28 | 29 | {:ok, developers} 30 | end 31 | 32 | def get(_parent, %{username: username}, _resolution) do 33 | case Developers.get_developer_by_username(username) do 34 | nil -> 35 | {:error, message: "Developer with username #{username} could not be found.", code: 404} 36 | 37 | developer -> 38 | {:ok, developer} 39 | end 40 | end 41 | 42 | def get_stats(%Developer{} = developer, _params, _resolution) do 43 | stats = %{ 44 | rank: Developers.get_rank(developer, :turkey), 45 | location_rank: Developers.get_rank(developer, :in_location), 46 | repositories_count: Developers.get_repositories_count(developer) 47 | } 48 | 49 | {:ok, stats} 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/githubist_web/resolvers/language_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Resolvers.LanguageResolver do 2 | @moduledoc false 3 | 4 | alias Githubist.Developers.Developer 5 | alias Githubist.GraphQLArgumentParser 6 | alias Githubist.Languages 7 | alias Githubist.Languages.Language 8 | alias Githubist.Locations.Location 9 | 10 | def all(_parent, params, _resolution) do 11 | params = 12 | params 13 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 14 | |> GraphQLArgumentParser.parse_order_by() 15 | 16 | languages = Languages.all(params) 17 | 18 | {:ok, languages} 19 | end 20 | 21 | def get(_parent, %{slug: slug}, _resolution) do 22 | case Languages.get_language_by_slug(slug) do 23 | nil -> {:error, message: "Language with slug #{slug} could not be found.", code: 404} 24 | language -> {:ok, language} 25 | end 26 | end 27 | 28 | def get_stats(%Language{} = language, _params, _resolution) do 29 | stats = %{ 30 | rank: Languages.get_rank(language), 31 | repositories_count_rank: Languages.get_repositories_count_rank(language), 32 | developers_count_rank: Languages.get_developers_count_rank(language) 33 | } 34 | 35 | {:ok, stats} 36 | end 37 | 38 | def get_usage(%Location{} = location, params, _resolution) do 39 | params = 40 | params 41 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 42 | 43 | usage = Languages.get_location_usage(location, params) 44 | 45 | {:ok, usage} 46 | end 47 | 48 | def get_usage(%Developer{} = developer, params, _resolution) do 49 | params = 50 | params 51 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 52 | 53 | usage = Languages.get_developer_usage(developer, params) 54 | 55 | {:ok, usage} 56 | end 57 | 58 | def get_location_usage(%Language{} = language, params, _resolution) do 59 | params = 60 | params 61 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 62 | 63 | usage = Languages.get_location_stats(language, params) 64 | 65 | {:ok, usage} 66 | end 67 | 68 | def get_developer_usage(%Language{} = language, params, _resolution) do 69 | params = 70 | params 71 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 72 | 73 | usage = Languages.get_developer_stats(language, params) 74 | 75 | {:ok, usage} 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/githubist_web/resolvers/location_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Resolvers.LocationResolver do 2 | @moduledoc false 3 | 4 | alias Githubist.GraphQLArgumentParser 5 | alias Githubist.Locations 6 | alias Githubist.Locations.Location 7 | 8 | def all(_parent, params, _resolution) do 9 | params = 10 | params 11 | |> GraphQLArgumentParser.parse_limit(max_limit: 81) 12 | |> GraphQLArgumentParser.parse_order_by() 13 | 14 | locations = Locations.all(params) 15 | 16 | {:ok, locations} 17 | end 18 | 19 | def get(_parent, %{slug: slug}, _resolution) do 20 | case Locations.get_location_by_slug(slug) do 21 | nil -> {:error, message: "Location with slug #{slug} could not be found.", code: 404} 22 | location -> {:ok, location} 23 | end 24 | end 25 | 26 | def get_stats(%Location{} = location, _params, _resolution) do 27 | stats = %{ 28 | rank: Locations.get_rank(location) 29 | } 30 | 31 | {:ok, stats} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/githubist_web/resolvers/repository_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Resolvers.RepositoryResolver do 2 | @moduledoc false 3 | 4 | alias Githubist.Developers 5 | alias Githubist.Developers.Developer 6 | alias Githubist.GraphQLArgumentParser 7 | alias Githubist.Languages 8 | alias Githubist.Languages.Language 9 | alias Githubist.Locations 10 | alias Githubist.Locations.Location 11 | alias Githubist.Repositories 12 | alias Githubist.Repositories.Repository 13 | 14 | def all(%Developer{} = developer, params, _resolution) do 15 | params = 16 | params 17 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 18 | |> GraphQLArgumentParser.parse_order_by() 19 | 20 | repositories = Developers.get_repositories(developer, params) 21 | 22 | {:ok, repositories} 23 | end 24 | 25 | def all(%Language{} = language, params, _resolution) do 26 | params = 27 | params 28 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 29 | |> GraphQLArgumentParser.parse_order_by() 30 | 31 | repositories = Languages.get_repositories(language, params) 32 | 33 | {:ok, repositories} 34 | end 35 | 36 | def all(%Location{} = location, params, _resolution) do 37 | params = 38 | params 39 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 40 | |> GraphQLArgumentParser.parse_order_by() 41 | 42 | repositories = Locations.get_repositories(location, params) 43 | 44 | {:ok, repositories} 45 | end 46 | 47 | def all(_parent, params, _resolution) do 48 | params = 49 | params 50 | |> GraphQLArgumentParser.parse_limit(max_limit: 100) 51 | |> GraphQLArgumentParser.parse_order_by() 52 | 53 | repositories = Repositories.all(params) 54 | 55 | {:ok, repositories} 56 | end 57 | 58 | def get(_parent, %{slug: slug}, _resolution) do 59 | case Repositories.get_repository_by_slug(slug) do 60 | nil -> {:error, message: "Repository with slug #{slug} could not be found.", code: 404} 61 | repository -> {:ok, repository} 62 | end 63 | end 64 | 65 | def get_stats(%Repository{} = repository, _params, _resolution) do 66 | stats = %{ 67 | rank: Repositories.get_rank(repository, :turkey), 68 | language_rank: Repositories.get_rank(repository, :in_language) 69 | } 70 | 71 | {:ok, stats} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/githubist_web/resolvers/search_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Resolvers.SearchResolver do 2 | @moduledoc false 3 | 4 | alias Githubist.Developers 5 | alias Githubist.Languages 6 | alias Githubist.Locations 7 | alias Githubist.Repositories 8 | 9 | def search(_parent, %{query: query}, _resolution) do 10 | limit = 5 11 | 12 | developer_results = Developers.search(query, limit) 13 | language_results = Languages.search(query, limit) 14 | location_results = Locations.search(query, limit) 15 | repository_results = Repositories.search(query, limit) 16 | 17 | results = 18 | developer_results 19 | |> Enum.concat(language_results) 20 | |> Enum.concat(location_results) 21 | |> Enum.concat(repository_results) 22 | |> Enum.take(limit) 23 | 24 | {:ok, results} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/githubist_web/resolvers/turkey_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Resolvers.TurkeyResolver do 2 | @moduledoc false 3 | 4 | alias Githubist.Developers 5 | alias Githubist.Languages 6 | alias Githubist.Locations 7 | alias Githubist.Repositories 8 | 9 | def get(_parent, _params, _resolution) do 10 | total_developers = Developers.get_developers_count() 11 | total_languages = Languages.get_languages_count() 12 | total_locations = Locations.get_locations_count() 13 | total_repositories = Repositories.get_repositories_count() 14 | 15 | {:ok, 16 | %{ 17 | total_developers: total_developers, 18 | total_languages: total_languages, 19 | total_locations: total_locations, 20 | total_repositories: total_repositories 21 | }} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/githubist_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Router do 2 | use GithubistWeb, :router 3 | 4 | pipeline :graphql do 5 | plug(CORSPlug, origin: ["http://localhost:3000", "https://github.ist"]) 6 | plug(:accepts, ["json"]) 7 | end 8 | 9 | scope "/" do 10 | pipe_through(:graphql) 11 | 12 | forward("/graphql", Absinthe.Plug, schema: GithubistWeb.Schema) 13 | 14 | forward("/graphiql", Absinthe.Plug.GraphiQL, 15 | schema: GithubistWeb.Schema, 16 | interface: :playground 17 | ) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/githubist_web/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema do 2 | @moduledoc """ 3 | Github Stats GraphQL Schema 4 | """ 5 | 6 | use Absinthe.Schema 7 | 8 | alias Absinthe.Middleware.Dataloader, as: DataloaderMiddleware 9 | alias Absinthe.Plugin 10 | 11 | alias Githubist.Loaders 12 | alias GithubistWeb.Resolvers.DeveloperResolver 13 | alias GithubistWeb.Resolvers.LanguageResolver 14 | alias GithubistWeb.Resolvers.LocationResolver 15 | alias GithubistWeb.Resolvers.RepositoryResolver 16 | alias GithubistWeb.Resolvers.SearchResolver 17 | alias GithubistWeb.Resolvers.TurkeyResolver 18 | 19 | import_types(GithubistWeb.Schema.Scalars) 20 | import_types(GithubistWeb.Schema.Enums) 21 | import_types(GithubistWeb.Schema.InputObjects) 22 | import_types(GithubistWeb.Schema.DeveloperTypes) 23 | import_types(GithubistWeb.Schema.RepositoryTypes) 24 | import_types(GithubistWeb.Schema.LanguageTypes) 25 | import_types(GithubistWeb.Schema.LocationTypes) 26 | import_types(GithubistWeb.Schema.SearchTypes) 27 | import_types(GithubistWeb.Schema.TurkeyTypes) 28 | 29 | def context(ctx) do 30 | loader = 31 | Dataloader.new() 32 | |> Dataloader.add_source(:db, Loaders.data()) 33 | 34 | Map.put(ctx, :loader, loader) 35 | end 36 | 37 | def plugins do 38 | [DataloaderMiddleware] ++ Plugin.defaults() 39 | end 40 | 41 | query do 42 | @desc "Get the all Turkey stats" 43 | field :turkey, :turkey do 44 | resolve(&TurkeyResolver.get/3) 45 | end 46 | 47 | field :search, list_of(:search_item) do 48 | @desc "Term to search" 49 | arg(:query, non_null(:string)) 50 | 51 | resolve(&SearchResolver.search/3) 52 | end 53 | 54 | @desc "Get all locations" 55 | field :locations, list_of(:location) do 56 | @desc "Order type" 57 | arg(:order_by, non_null(:location_order)) 58 | 59 | @desc "Limit of results" 60 | arg(:limit, :integer, default_value: 81) 61 | 62 | @desc "Offset for pagination" 63 | arg(:offset, :integer, default_value: 0) 64 | 65 | resolve(&LocationResolver.all/3) 66 | end 67 | 68 | @desc "Get a specific location by slug" 69 | field :location, :location do 70 | @desc "Slug of location" 71 | arg(:slug, non_null(:string)) 72 | 73 | resolve(&LocationResolver.get/3) 74 | end 75 | 76 | @desc "Get all languages" 77 | field :languages, list_of(:language) do 78 | @desc "Order type" 79 | arg(:order_by, non_null(:language_order)) 80 | 81 | @desc "Limit of results" 82 | arg(:limit, :integer, default_value: 25) 83 | 84 | @desc "Offset for pagination" 85 | arg(:offset, :integer, default_value: 0) 86 | 87 | resolve(&LanguageResolver.all/3) 88 | end 89 | 90 | @desc "Get a specific language by slug" 91 | field :language, :language do 92 | @desc "Slug of language" 93 | arg(:slug, non_null(:string)) 94 | 95 | resolve(&LanguageResolver.get/3) 96 | end 97 | 98 | @desc "Get all developers" 99 | field :developers, list_of(:developer) do 100 | @desc "Order type" 101 | arg(:order_by, non_null(:developer_order)) 102 | 103 | @desc "Limit of results" 104 | arg(:limit, :integer, default_value: 25) 105 | 106 | @desc "Offset for pagination" 107 | arg(:offset, :integer, default_value: 0) 108 | 109 | resolve(&DeveloperResolver.all/3) 110 | end 111 | 112 | @desc "Get a specific developer by their username" 113 | field :developer, :developer do 114 | @desc "Github username of developer" 115 | arg(:username, non_null(:string)) 116 | 117 | resolve(&DeveloperResolver.get/3) 118 | end 119 | 120 | @desc "Get all repositories" 121 | field :repositories, list_of(:repository) do 122 | @desc "Order type" 123 | arg(:order_by, non_null(:repository_order)) 124 | 125 | @desc "Limit of results" 126 | arg(:limit, :integer, default_value: 25) 127 | 128 | @desc "Offset for pagination" 129 | arg(:offset, :integer, default_value: 0) 130 | 131 | resolve(&RepositoryResolver.all/3) 132 | end 133 | 134 | @desc "Get a specific repository by slug" 135 | field :repository, :repository do 136 | @desc "Github path of repository. Ex: alpcanaydin/turkiye" 137 | arg(:slug, non_null(:string)) 138 | 139 | resolve(&RepositoryResolver.get/3) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/developer_types.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.DeveloperTypes do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | import Absinthe.Resolution.Helpers, only: [dataloader: 1] 7 | 8 | alias GithubistWeb.Resolvers.DeveloperResolver 9 | alias GithubistWeb.Resolvers.LanguageResolver 10 | alias GithubistWeb.Resolvers.RepositoryResolver 11 | 12 | @desc "Developer stats" 13 | object :developer_stats do 14 | @desc "Position of this developer in Github Turkey stats" 15 | field(:rank, non_null(:integer)) 16 | 17 | @desc "Position of this developer at her/his location" 18 | field(:location_rank, non_null(:integer)) 19 | 20 | @desc "Total repositories count of this developer" 21 | field(:repositories_count, non_null(:integer)) 22 | end 23 | 24 | @desc "Developer usage" 25 | object :developer_usage do 26 | @desc "Developer" 27 | field(:developer, non_null(:developer)) 28 | 29 | @desc "Repositories count" 30 | field(:repositories_count, non_null(:integer)) 31 | end 32 | 33 | @desc "Developer" 34 | object :developer do 35 | @desc "Developer ID" 36 | field(:id, non_null(:id)) 37 | 38 | @desc "Github username of developer" 39 | field(:username, non_null(:string)) 40 | 41 | @desc "Github ID of developer" 42 | field(:github_id, non_null(:integer)) 43 | 44 | @desc "Full name of developer" 45 | field(:name, :string) 46 | 47 | @desc "Avatar url of developer" 48 | field(:avatar_url, non_null(:string)) 49 | 50 | @desc "Bio of developer" 51 | field(:bio, :string) 52 | 53 | @desc "Company of developer" 54 | field(:company, :string) 55 | 56 | @desc "Github location of developer. This field is the raw value of developer's location" 57 | field(:github_location, non_null(:string)) 58 | 59 | @desc "Github URL of developer" 60 | field(:github_url, non_null(:string)) 61 | 62 | @desc "Followers count of developer" 63 | field(:followers, non_null(:integer)) 64 | 65 | @desc "Count of the people who followed by this developer" 66 | field(:following, non_null(:integer)) 67 | 68 | @desc "Repository count of this developer. This count just includes only public repositories" 69 | field(:public_repos, non_null(:integer)) 70 | 71 | @desc "Stars count for user's all repositories" 72 | field(:total_starred, non_null(:integer)) 73 | 74 | @desc "Github stats score of this developer" 75 | field(:score, non_null(:float)) 76 | 77 | @desc "Developer's Github registration date" 78 | field(:github_created_at, non_null(:time)) 79 | 80 | @desc "Developer stats" 81 | field(:stats, non_null(:developer_stats), resolve: &DeveloperResolver.get_stats/3) 82 | 83 | @desc "Github stats location of this developer" 84 | field(:location, non_null(:location), resolve: dataloader(:db)) 85 | 86 | @desc "Repositories of developer" 87 | field(:repositories, list_of(:repository)) do 88 | @desc "Order type" 89 | arg(:order_by, non_null(:repository_order)) 90 | 91 | @desc "Limit of results" 92 | arg(:limit, :integer, default_value: 25) 93 | 94 | @desc "Offset for pagination" 95 | arg(:offset, :integer, default_value: 0) 96 | 97 | resolve(&RepositoryResolver.all/3) 98 | end 99 | 100 | @desc "Language usage of this developer" 101 | field(:language_usage, list_of(:language_usage)) do 102 | @desc "Limit of results" 103 | arg(:limit, :integer, default_value: 25) 104 | 105 | @desc "Offset for pagination" 106 | arg(:offset, :integer, default_value: 0) 107 | 108 | resolve(&LanguageResolver.get_usage/3) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/enums.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.Enums do 2 | @moduledoc """ 3 | Enums that used in all across the schema 4 | """ 5 | 6 | use Absinthe.Schema.Notation 7 | 8 | enum :developer_order_field do 9 | value(:name) 10 | value(:username) 11 | value(:score) 12 | value(:total_starred) 13 | value(:followers) 14 | value(:github_created_at) 15 | end 16 | 17 | enum :language_order_field do 18 | value(:name) 19 | value(:score) 20 | value(:total_stars) 21 | value(:total_repositories) 22 | value(:total_developers) 23 | end 24 | 25 | enum :location_order_field do 26 | value(:name) 27 | value(:score) 28 | value(:total_repositories) 29 | value(:total_developers) 30 | end 31 | 32 | enum :repository_order_field do 33 | value(:name) 34 | value(:stars) 35 | value(:forks) 36 | value(:github_created_at) 37 | end 38 | 39 | enum :order_direction do 40 | value(:asc) 41 | value(:desc) 42 | end 43 | 44 | enum :search_result_type do 45 | value(:developer) 46 | value(:location) 47 | value(:language) 48 | value(:repository) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/input_objects.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.InputObjects do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | @desc "The field and direction to order developers" 7 | input_object :developer_order do 8 | @desc "The order field" 9 | field(:field, non_null(:developer_order_field)) 10 | 11 | @desc "The order direction" 12 | field(:direction, non_null(:order_direction)) 13 | end 14 | 15 | @desc "The field and direction to order languages" 16 | input_object :language_order do 17 | @desc "The order field" 18 | field(:field, non_null(:language_order_field)) 19 | 20 | @desc "The order direction" 21 | field(:direction, non_null(:order_direction)) 22 | end 23 | 24 | @desc "The field and direction to order locations" 25 | input_object :location_order do 26 | @desc "The order field" 27 | field(:field, non_null(:location_order_field)) 28 | 29 | @desc "The order direction" 30 | field(:direction, non_null(:order_direction)) 31 | end 32 | 33 | @desc "The field and direction to order repositories" 34 | input_object :repository_order do 35 | @desc "The order field" 36 | field(:field, non_null(:repository_order_field)) 37 | 38 | @desc "The order direction" 39 | field(:direction, non_null(:order_direction)) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/language_types.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.LanguageTypes do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | alias GithubistWeb.Resolvers.LanguageResolver 7 | alias GithubistWeb.Resolvers.RepositoryResolver 8 | 9 | @desc "Language stats" 10 | object :language_stats do 11 | @desc "Position of this language in Github Turkey stats" 12 | field(:rank, non_null(:integer)) 13 | 14 | @desc "Position of this language in Github Turkey stats according to repositories count" 15 | field(:repositories_count_rank, non_null(:integer)) 16 | 17 | @desc "Position of this language in Github Turkey stats according to developers count" 18 | field(:developers_count_rank, non_null(:integer)) 19 | end 20 | 21 | @desc "Language usage" 22 | object :language_usage do 23 | @desc "Language" 24 | field(:language, non_null(:language)) 25 | 26 | @desc "Repositories count" 27 | field(:repositories_count, non_null(:integer)) 28 | end 29 | 30 | @desc "Language" 31 | object :language do 32 | @desc "Language ID" 33 | field(:id, non_null(:id)) 34 | 35 | @desc "Language name" 36 | field(:name, non_null(:string)) 37 | 38 | @desc "Language slug to use in URLs" 39 | field(:slug, non_null(:string)) 40 | 41 | @desc "Github stats score of this language" 42 | field(:score, non_null(:float)) 43 | 44 | @desc "Total stars of the language that populated by repositories" 45 | field(:total_stars, non_null(:integer)) 46 | 47 | @desc "Total repos of the language" 48 | field(:total_repositories, non_null(:integer)) 49 | 50 | @desc "Total developers for the language" 51 | field(:total_developers, non_null(:integer)) 52 | 53 | @desc "Language stats" 54 | field(:stats, non_null(:language_stats), resolve: &LanguageResolver.get_stats/3) 55 | 56 | @desc "Repositories that use this language" 57 | field(:repositories, list_of(:repository)) do 58 | @desc "Order type" 59 | arg(:order_by, non_null(:repository_order)) 60 | 61 | @desc "Limit of results" 62 | arg(:limit, :integer, default_value: 25) 63 | 64 | @desc "Offset for pagination" 65 | arg(:offset, :integer, default_value: 0) 66 | 67 | resolve(&RepositoryResolver.all/3) 68 | end 69 | 70 | @desc "Location usage of this language" 71 | field(:location_usage, list_of(:location_usage)) do 72 | @desc "Limit of results" 73 | arg(:limit, :integer, default_value: 25) 74 | 75 | @desc "Offset for pagination" 76 | arg(:offset, :integer, default_value: 0) 77 | 78 | resolve(&LanguageResolver.get_location_usage/3) 79 | end 80 | 81 | @desc "Developer usage of this language" 82 | field(:developer_usage, list_of(:developer_usage)) do 83 | @desc "Limit of results" 84 | arg(:limit, :integer, default_value: 25) 85 | 86 | @desc "Offset for pagination" 87 | arg(:offset, :integer, default_value: 0) 88 | 89 | resolve(&LanguageResolver.get_developer_usage/3) 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/location_types.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.LocationTypes do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | alias GithubistWeb.Resolvers.DeveloperResolver 7 | alias GithubistWeb.Resolvers.LanguageResolver 8 | alias GithubistWeb.Resolvers.LocationResolver 9 | alias GithubistWeb.Resolvers.RepositoryResolver 10 | 11 | @desc "Location stats" 12 | object :location_stats do 13 | @desc "Position of this location in Github Turkey stats" 14 | field(:rank, non_null(:integer)) 15 | end 16 | 17 | @desc "Location usage" 18 | object :location_usage do 19 | @desc "Location" 20 | field(:location, non_null(:location)) 21 | 22 | @desc "Repositories count" 23 | field(:repositories_count, non_null(:integer)) 24 | end 25 | 26 | @desc "A city from Turkey" 27 | object :location do 28 | @desc "Location ID" 29 | field(:id, non_null(:id)) 30 | 31 | @desc "Location name" 32 | field(:name, non_null(:string)) 33 | 34 | @desc "Location slug to use in URLs" 35 | field(:slug, non_null(:string)) 36 | 37 | @desc "Github stats score of this location" 38 | field(:score, non_null(:float)) 39 | 40 | @desc "Total developers count in this location" 41 | field(:total_developers, non_null(:integer)) 42 | 43 | @desc "Total repositories count in this location" 44 | field(:total_repositories, non_null(:integer)) 45 | 46 | @desc "Location stats" 47 | field(:stats, non_null(:location_stats), resolve: &LocationResolver.get_stats/3) 48 | 49 | @desc "Developers in this location" 50 | field(:developers, list_of(:developer)) do 51 | @desc "Order type" 52 | arg(:order_by, non_null(:developer_order)) 53 | 54 | @desc "Limit of results" 55 | arg(:limit, :integer, default_value: 25) 56 | 57 | @desc "Offset for pagination" 58 | arg(:offset, :integer, default_value: 0) 59 | 60 | resolve(&DeveloperResolver.all/3) 61 | end 62 | 63 | @desc "Repositories in this location" 64 | field(:repositories, list_of(:repository)) do 65 | @desc "Order type" 66 | arg(:order_by, non_null(:repository_order)) 67 | 68 | @desc "Limit of results" 69 | arg(:limit, :integer, default_value: 25) 70 | 71 | @desc "Offset for pagination" 72 | arg(:offset, :integer, default_value: 0) 73 | 74 | resolve(&RepositoryResolver.all/3) 75 | end 76 | 77 | @desc "Language usage of this location" 78 | field(:language_usage, list_of(:language_usage)) do 79 | @desc "Limit of results" 80 | arg(:limit, :integer, default_value: 25) 81 | 82 | @desc "Offset for pagination" 83 | arg(:offset, :integer, default_value: 0) 84 | 85 | resolve(&LanguageResolver.get_usage/3) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/repository_types.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.RepositoryTypes do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | import Absinthe.Resolution.Helpers, only: [dataloader: 1] 7 | 8 | alias GithubistWeb.Resolvers.RepositoryResolver 9 | 10 | @desc "Repository stats" 11 | object :repository_stats do 12 | @desc "Position of this repository in Github Turkey stats" 13 | field(:rank, non_null(:integer)) 14 | 15 | @desc "Position of this repository in the developed language" 16 | field(:language_rank, non_null(:integer)) 17 | end 18 | 19 | @desc "Repository" 20 | object :repository do 21 | @desc "Repository ID" 22 | field(:id, non_null(:id)) 23 | 24 | @desc "Repository name" 25 | field(:name, non_null(:string)) 26 | 27 | @desc "Repository slug" 28 | field(:slug, non_null(:string)) 29 | 30 | @desc "Description for the repo" 31 | field(:description, :string) 32 | 33 | @desc "Github ID of repository" 34 | field(:github_id, non_null(:integer)) 35 | 36 | @desc "Gtihub URL of repository" 37 | field(:github_url, non_null(:string)) 38 | 39 | @desc "Total stars of this repository" 40 | field(:stars, non_null(:integer)) 41 | 42 | @desc "Total forks of this repository" 43 | field(:forks, non_null(:integer)) 44 | 45 | @desc "Repository creation date on Github" 46 | field(:github_created_at, non_null(:time)) 47 | 48 | @desc "Repository stats" 49 | field(:stats, non_null(:repository_stats), resolve: &RepositoryResolver.get_stats/3) 50 | 51 | @desc "Owner of this repository" 52 | field(:developer, non_null(:developer), resolve: dataloader(:db)) 53 | 54 | @desc "Main language of this repository" 55 | field(:language, non_null(:language), resolve: dataloader(:db)) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/scalars.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.Scalars do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | @desc """ 7 | This scalar type represents date and time as ISO 8601 format 8 | """ 9 | scalar :time do 10 | parse(&Timex.parse(&1.value, "{ISO:Extended:Z}")) 11 | serialize(&Timex.format!(&1, "{ISO:Extended:Z}")) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/search_types.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.SearchTypes do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | object :search_item do 7 | @desc "Human readable version of search result" 8 | field(:name, non_null(:string)) 9 | 10 | @desc "Slug of search result" 11 | field(:slug, non_null(:string)) 12 | 13 | @desc "Type of search result" 14 | field(:type, non_null(:search_result_type)) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/githubist_web/schema/turkey_types.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.Schema.TurkeyTypes do 2 | @moduledoc false 3 | 4 | use Absinthe.Schema.Notation 5 | 6 | object :turkey do 7 | @desc "Count of total developers" 8 | field(:total_developers, non_null(:integer)) 9 | 10 | @desc "Count of total languages" 11 | field(:total_languages, non_null(:integer)) 12 | 13 | @desc "Count of total locations" 14 | field(:total_locations, non_null(:integer)) 15 | 16 | @desc "Count of total repositories" 17 | field(:total_repositories, non_null(:integer)) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/githubist_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | @doc """ 7 | Translates an error message using gettext. 8 | """ 9 | def translate_error({msg, opts}) do 10 | # When using gettext, we typically pass the strings we want 11 | # to translate as a static argument: 12 | # 13 | # # Translate "is invalid" in the "errors" domain 14 | # dgettext "errors", "is invalid" 15 | # 16 | # # Translate the number of files with plural rules 17 | # dngettext "errors", "1 file", "%{count} files", count 18 | # 19 | # Because the error messages we show in our forms and APIs 20 | # are defined inside Ecto, we need to translate them dynamically. 21 | # This requires us to call the Gettext module passing our gettext 22 | # backend as first argument. 23 | # 24 | # Note we use the "errors" domain, which means translations 25 | # should be written to the errors.po file. The :count option is 26 | # set by Ecto and indicates we should also apply plural rules. 27 | if count = opts[:count] do 28 | Gettext.dngettext(GithubistWeb.Gettext, "errors", msg, msg, count, opts) 29 | else 30 | Gettext.dgettext(GithubistWeb.Gettext, "errors", msg, opts) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/githubist_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.ErrorView do 2 | use GithubistWeb, :view 3 | 4 | alias Phoenix.Controller 5 | 6 | # If you want to customize a particular status code 7 | # for a certain format, you may uncomment below. 8 | # def render("500.json", _assigns) do 9 | # %{errors: %{detail: "Internal Server Error"}} 10 | # end 11 | 12 | # By default, Phoenix returns the status message from 13 | # the template name. For example, "404.json" becomes 14 | # "Not Found". 15 | def template_not_found(template, _assigns) do 16 | %{errors: %{detail: Controller.status_message_from_template(template)}} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /mix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose run --rm app mix "$@" 3 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :githubist, 7 | version: "0.0.1", 8 | elixir: "~> 1.4", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps(), 14 | dialyzer: [plt_add_deps: :transitive] 15 | ] 16 | end 17 | 18 | # Configuration for the OTP application. 19 | # 20 | # Type `mix help compile.app` for more information. 21 | def application do 22 | [ 23 | mod: {Githubist.Application, []}, 24 | extra_applications: [:logger, :runtime_tools, :timex] 25 | ] 26 | end 27 | 28 | # Specifies which paths to compile per environment. 29 | defp elixirc_paths(:test), do: ["lib", "test/support"] 30 | defp elixirc_paths(_), do: ["lib"] 31 | 32 | # Specifies your project dependencies. 33 | # 34 | # Type `mix help deps` for examples and options. 35 | defp deps do 36 | [ 37 | {:phoenix, "~> 1.3.3"}, 38 | {:phoenix_pubsub, "~> 1.0"}, 39 | {:phoenix_ecto, "~> 3.2"}, 40 | {:postgrex, ">= 0.0.0"}, 41 | {:gettext, "~> 0.11"}, 42 | {:cowboy, "~> 1.0"}, 43 | {:poison, "~> 3.0"}, 44 | {:slugify, "~> 1.1"}, 45 | {:absinthe, "~> 1.4.0"}, 46 | {:absinthe_plug, "~> 1.4"}, 47 | {:dataloader, "~> 1.0.0"}, 48 | {:timex, "~> 3.0"}, 49 | {:cors_plug, "~> 1.5"}, 50 | {:credo, "~> 0.10.0", only: [:dev, :test], runtime: false}, 51 | {:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false} 52 | ] 53 | end 54 | 55 | # Aliases are shortcuts or tasks specific to the current project. 56 | # For example, to create, migrate and run the seeds file at once: 57 | # 58 | # $ mix ecto.setup 59 | # 60 | # See the documentation for `Mix` for more info on aliases. 61 | defp aliases do 62 | [ 63 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 64 | "ecto.reset": ["ecto.drop", "ecto.setup"], 65 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 66 | ] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "absinthe": {:hex, :absinthe, "1.4.13", "81eb2ff41f1b62cd6e992955f62c22c042d1079b7936c27f5f7c2c806b8fc436", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 3 | "absinthe_plug": {:hex, :absinthe_plug, "1.4.5", "f63d52a76c870cd5f11d4bed8f61351ab5c5f572c5eb0479a0137f9f730ba33d", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 5 | "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, 7 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 8 | "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, 11 | "credo": {:hex, :credo, "0.10.0", "66234a95effaf9067edb19fc5d0cd5c6b461ad841baac42467afed96c78e5e9e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "dataloader": {:hex, :dataloader, "1.0.2", "90e788dc507bb80f7281f2d3d6c6e4bb1d18709c8351392365b6a0950bfe9f7d", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, 13 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 14 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, 15 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, 16 | "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 17 | "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, 18 | "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 19 | "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 21 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 22 | "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, 23 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 24 | "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, 25 | "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 26 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 27 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"}, 28 | "plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 29 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 30 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, 31 | "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 32 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, 33 | "slugify": {:hex, :slugify, "1.1.0", "945165e32c7ad22226c63101ac57a64d105fd4681d503c3302ae2c734a0e26ee", [:mix], [], "hexpm"}, 34 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 35 | "timex": {:hex, :timex, "3.3.0", "e0695aa0ddb37d460d93a2db34d332c2c95a40c27edf22fbfea22eb8910a9c8d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, 36 | "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 37 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 38 | } 39 | -------------------------------------------------------------------------------- /priv/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alpcanaydin/githubist-api/6481f8177c5b8573da2d5df52ffaff41340b25d0/priv/data/.gitkeep -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180816184210_create_developers.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo.Migrations.CreateDevelopers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute("CREATE EXTENSION IF NOT EXISTS citext") 6 | 7 | create table(:developers) do 8 | add(:username, :citext) 9 | add(:email, :citext) 10 | add(:github_id, :integer) 11 | add(:name, :string) 12 | add(:avatar_url, :string) 13 | add(:bio, :text) 14 | add(:company, :string) 15 | add(:github_location, :string) 16 | add(:github_url, :string) 17 | add(:public_repos, :integer) 18 | add(:followers, :integer) 19 | add(:following, :integer) 20 | add(:total_starred, :integer) 21 | add(:score, :float) 22 | add(:github_created_at, :utc_datetime) 23 | 24 | timestamps() 25 | end 26 | 27 | create(unique_index(:developers, [:username])) 28 | create(index(:developers, [:email])) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180816200053_create_locations.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo.Migrations.CreateLocations do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute("CREATE EXTENSION IF NOT EXISTS citext") 6 | 7 | create table(:locations) do 8 | add(:name, :string) 9 | add(:slug, :citext) 10 | add(:score, :float) 11 | add(:total_repositories, :integer) 12 | add(:total_developers, :integer) 13 | 14 | timestamps() 15 | end 16 | 17 | create(unique_index(:locations, [:slug])) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180816205243_create_languages.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo.Migrations.CreateLanguages do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute("CREATE EXTENSION IF NOT EXISTS citext") 6 | 7 | create table(:languages) do 8 | add(:name, :string) 9 | add(:slug, :citext) 10 | add(:score, :float) 11 | add(:total_stars, :integer) 12 | add(:total_repositories, :integer) 13 | add(:total_developers, :integer) 14 | 15 | timestamps() 16 | end 17 | 18 | create(unique_index(:languages, [:slug])) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180816210317_create_repositories.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo.Migrations.CreateRepositories do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute("CREATE EXTENSION IF NOT EXISTS citext") 6 | 7 | create table(:repositories) do 8 | add(:name, :string) 9 | add(:slug, :citext) 10 | add(:description, :text) 11 | add(:github_id, :integer) 12 | add(:github_url, :string) 13 | add(:stars, :integer) 14 | add(:forks, :integer) 15 | add(:github_created_at, :utc_datetime) 16 | 17 | timestamps() 18 | end 19 | 20 | create(unique_index(:repositories, [:slug])) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180816210743_developer_location_rel.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo.Migrations.DeveloperLocationRel do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:developers) do 6 | add(:location_id, references(:locations)) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20180816211337_repo_relations.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.Repo.Migrations.RepoRelations do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:repositories) do 6 | add(:developer_id, references(:developers)) 7 | add(:language_id, references(:languages)) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Githubist.Repo.insert!(%Githubist.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | alias Githubist.Developers 14 | alias Githubist.ImportHelpers 15 | alias Githubist.Locations 16 | alias Githubist.Languages 17 | alias Githubist.Repositories 18 | 19 | # Import locations 20 | locationsAndScores = 21 | "priv/data/locationScores.json" 22 | |> File.read!() 23 | |> Poison.decode!() 24 | 25 | Enum.each(locationsAndScores, fn item -> 26 | {name, data} = item 27 | 28 | {:ok, _location} = 29 | Locations.create_location(%{ 30 | name: ImportHelpers.capitalize(name), 31 | slug: ImportHelpers.create_location_slug(name), 32 | score: data["score"], 33 | total_repositories: data["repos"], 34 | total_developers: data["developers"] 35 | }) 36 | end) 37 | 38 | # Import languages 39 | languagesAndScores = 40 | "priv/data/languageScores.json" 41 | |> File.read!() 42 | |> Poison.decode!() 43 | 44 | Enum.each(languagesAndScores, fn item -> 45 | {name, data} = item 46 | 47 | {:ok, _language} = 48 | Languages.create_language(%{ 49 | name: name, 50 | slug: ImportHelpers.create_language_slug(name), 51 | score: data["score"], 52 | total_stars: data["stars"], 53 | total_repositories: data["repos"], 54 | total_developers: data["developers"] 55 | }) 56 | end) 57 | 58 | # Import developers and repos 59 | developersAndRepos = 60 | "priv/data/normalized.json" 61 | |> File.read!() 62 | |> Poison.decode!() 63 | 64 | Enum.each(developersAndRepos, fn item -> 65 | location = Locations.get_location_by_slug!(ImportHelpers.create_location_slug(item["location"])) 66 | 67 | # Import developer 68 | developer_attrs = %{ 69 | username: item["username"], 70 | email: item["email"], 71 | github_id: item["github_id"], 72 | name: item["name"], 73 | avatar_url: item["avatar_url"], 74 | bio: item["bio"], 75 | company: item["company"], 76 | github_location: item["github_location"], 77 | github_url: item["github_url"], 78 | followers: item["followers"], 79 | following: item["following"], 80 | public_repos: item["public_repos"], 81 | total_starred: item["total_starred"], 82 | score: item["score"], 83 | github_created_at: item["github_created_at"], 84 | location_id: location.id 85 | } 86 | 87 | {:ok, developer} = Developers.create_developer(developer_attrs) 88 | 89 | Enum.each(item["repos"], fn repository_item -> 90 | languageSlug = ImportHelpers.create_language_slug(repository_item["language"]) 91 | language = Languages.get_language_by_slug(languageSlug) 92 | 93 | # Import repository 94 | repository_attrs = %{ 95 | name: repository_item["name"], 96 | slug: repository_item["slug"], 97 | description: repository_item["description"], 98 | github_id: repository_item["github_id"], 99 | github_url: repository_item["github_url"], 100 | stars: repository_item["stars"], 101 | forks: repository_item["forks"], 102 | github_created_at: repository_item["github_created_at"], 103 | developer_id: developer.id, 104 | language_id: language.id 105 | } 106 | 107 | {:ok, _repository} = Repositories.create_repository(repository_attrs) 108 | end) 109 | end) 110 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose run --rm app "$@" 3 | -------------------------------------------------------------------------------- /test/githubist/argument_parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.GraphQLArgumentParserTests do 2 | @moduledoc false 3 | 4 | use Githubist.DataCase 5 | 6 | alias Githubist.GraphQLArgumentParser 7 | 8 | describe "parse_limit/2" do 9 | test "updates limit if it bigger then max" do 10 | params = %{limit: 200} 11 | 12 | assert %{limit: 100} = GraphQLArgumentParser.parse_limit(params, max_limit: 100) 13 | end 14 | 15 | test "doesn't do anything if limit is lower than max" do 16 | params = %{limit: 20} 17 | 18 | assert %{limit: 20} = GraphQLArgumentParser.parse_limit(params, max_limit: 100) 19 | end 20 | end 21 | 22 | describe "parse_order_by/1" do 23 | test "converts map to tuple" do 24 | params = %{order_by: %{direction: :desc, field: :id}} 25 | 26 | assert %{order_by: {:desc, :id}} = GraphQLArgumentParser.parse_order_by(params) 27 | end 28 | 29 | test "doesn't do anything if order_by does not exist" do 30 | params = %{limit: 10} 31 | 32 | assert %{limit: 10} = GraphQLArgumentParser.parse_order_by(params) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/githubist/developers/developers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.DevelopersTest do 2 | use Githubist.DataCase 3 | 4 | alias Ecto.{Changeset, NoResultsError} 5 | alias Githubist.Developers 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @developer_attrs %{ 12 | username: "alpcanaydin", 13 | github_id: 123, 14 | name: "Alpcan Aydin", 15 | avatar_url: "https://example.com/avatar.jpg", 16 | bio: "Developer at Atolye15", 17 | company: "Atolye15", 18 | github_location: "Izmir, Turkey", 19 | github_url: "https://github.com/alpcanaydin", 20 | followers: 100, 21 | following: 100, 22 | public_repos: 50, 23 | total_starred: 500, 24 | score: 600.0, 25 | github_created_at: DateTime.utc_now() 26 | } 27 | 28 | def developer_fixture(attrs \\ %{}) do 29 | location = LocationsHelper.create_location() 30 | 31 | merged_attrs = 32 | @developer_attrs 33 | |> Map.put(:location_id, location.id) 34 | |> Map.merge(attrs) 35 | 36 | {:ok, developer} = Developers.create_developer(merged_attrs) 37 | 38 | developer 39 | end 40 | 41 | describe "get_developer/1" do 42 | test "returns developer by id" do 43 | developer = developer_fixture() 44 | 45 | fetched_developer = Developers.get_developer(developer.id) 46 | 47 | assert developer.id === fetched_developer.id 48 | end 49 | 50 | test "returns nil if developer does not exist" do 51 | assert nil === Developers.get_developer(2) 52 | end 53 | end 54 | 55 | describe "get_developer!/1" do 56 | test "returns developer by id" do 57 | developer = developer_fixture() 58 | 59 | fetched_developer = Developers.get_developer!(developer.id) 60 | 61 | assert developer.id === fetched_developer.id 62 | end 63 | 64 | test "raise an exception if developer does not exist" do 65 | assert_raise NoResultsError, fn -> 66 | Developers.get_developer!(2) 67 | end 68 | end 69 | end 70 | 71 | describe "get_developer_by_username/1" do 72 | test "returns developer by username" do 73 | developer = developer_fixture() 74 | 75 | fetched_developer = Developers.get_developer_by_username(developer.username) 76 | 77 | assert developer.id === fetched_developer.id 78 | end 79 | 80 | test "returns nil if developer does not exist" do 81 | assert nil === Developers.get_developer_by_username("none") 82 | end 83 | end 84 | 85 | describe "get_developer_by_username!/1" do 86 | test "returns developer by username" do 87 | developer = developer_fixture() 88 | 89 | fetched_developer = Developers.get_developer_by_username!(developer.username) 90 | 91 | assert developer.id === fetched_developer.id 92 | end 93 | 94 | test "raise an exception if developer does not exist" do 95 | assert_raise NoResultsError, fn -> 96 | Developers.get_developer_by_username!("none") 97 | end 98 | end 99 | end 100 | 101 | describe "create_developer/1" do 102 | test "creates a developer successfully" do 103 | developer = developer_fixture() 104 | 105 | assert developer.username === @developer_attrs.username 106 | assert developer.github_id === @developer_attrs.github_id 107 | assert developer.name === @developer_attrs.name 108 | assert developer.avatar_url === @developer_attrs.avatar_url 109 | assert developer.bio === @developer_attrs.bio 110 | assert developer.company === @developer_attrs.company 111 | assert developer.github_location === @developer_attrs.github_location 112 | assert developer.github_url === @developer_attrs.github_url 113 | assert developer.followers === @developer_attrs.followers 114 | assert developer.following === @developer_attrs.following 115 | assert developer.public_repos === @developer_attrs.public_repos 116 | assert developer.total_starred === @developer_attrs.total_starred 117 | assert developer.score === @developer_attrs.score 118 | assert developer.github_created_at === @developer_attrs.github_created_at 119 | assert developer.location_id !== nil 120 | end 121 | 122 | test "returns error if validation fails" do 123 | invalid_attrs = @developer_attrs |> Map.merge(%{location_id: nil, followers: nil}) 124 | 125 | assert {:error, %Changeset{errors: errors}} = Developers.create_developer(invalid_attrs) 126 | 127 | assert {_, [validation: :required]} = errors[:location_id] 128 | assert {_, [validation: :required]} = errors[:followers] 129 | end 130 | 131 | test "returns error if username is already in use" do 132 | developer_fixture() 133 | 134 | new_developer_attrs = 135 | @developer_attrs 136 | |> Map.put(:name, "Alp Can Aydin") 137 | |> Map.put(:location_id, 1) 138 | 139 | {:error, %Changeset{errors: errors}} = Developers.create_developer(new_developer_attrs) 140 | 141 | assert errors === [username: {"has already been taken", []}] 142 | end 143 | 144 | test "returns error if location is invalid" do 145 | invalid_attrs = @developer_attrs |> Map.put(:location_id, 1234) 146 | 147 | assert {:error, %Changeset{errors: errors}} = Developers.create_developer(invalid_attrs) 148 | 149 | assert errors === [location_id: {"does not exist", []}] 150 | end 151 | end 152 | 153 | describe "all/1" do 154 | setup do 155 | location = LocationsHelper.create_location() 156 | 157 | for i <- 1..3 do 158 | attrs = 159 | @developer_attrs 160 | |> Map.update!(:username, fn username -> "#{username}-#{i}" end) 161 | |> Map.update!(:score, fn _ -> i end) 162 | |> Map.put(:location_id, location.id) 163 | 164 | Developers.create_developer(attrs) 165 | end 166 | end 167 | 168 | test "returns developers as given limit" do 169 | developers = Developers.all(%{limit: 2, offset: 0, order_by: {:desc, :id}}) 170 | 171 | assert length(developers) === 2 172 | end 173 | 174 | test "returns developers as starts from given offset" do 175 | developers = Developers.all(%{limit: 2, offset: 2, order_by: {:desc, :id}}) 176 | 177 | assert length(developers) === 1 178 | end 179 | 180 | test "returns developers as given order" do 181 | developers = Developers.all(%{limit: 3, offset: 0, order_by: {:desc, :score}}) 182 | 183 | assert Map.get(List.first(developers), :score) === 3.0 184 | assert Map.get(List.last(developers), :score) === 1.0 185 | end 186 | end 187 | 188 | describe "get_developers_count/0" do 189 | setup do 190 | location = LocationsHelper.create_location() 191 | 192 | for i <- 1..3 do 193 | attrs = 194 | @developer_attrs 195 | |> Map.update!(:username, fn username -> "#{username}-#{i}" end) 196 | |> Map.update!(:score, fn _ -> i end) 197 | |> Map.put(:location_id, location.id) 198 | 199 | Developers.create_developer(attrs) 200 | end 201 | end 202 | 203 | test "returns developers count" do 204 | count = Developers.get_developers_count() 205 | 206 | assert count === 3 207 | end 208 | end 209 | 210 | describe "get_rank/2" do 211 | setup do 212 | location1 = LocationsHelper.create_location() 213 | 214 | location2 = 215 | LocationsHelper.create_location(%{name: "Ankara", slug: "ankara", score: 1000.0}) 216 | 217 | developer1 = 218 | @developer_attrs 219 | |> Map.update!(:username, fn _ -> "test1" end) 220 | |> Map.update!(:score, fn _ -> 1000.0 end) 221 | |> Map.put(:location_id, location1.id) 222 | 223 | developer2 = 224 | @developer_attrs 225 | |> Map.update!(:username, fn _ -> "test2" end) 226 | |> Map.update!(:score, fn _ -> 500.0 end) 227 | |> Map.put(:location_id, location2.id) 228 | 229 | Developers.create_developer(developer1) 230 | Developers.create_developer(developer2) 231 | 232 | :ok 233 | end 234 | 235 | test "returns rank of developer in turkey" do 236 | developer = Developers.get_developer_by_username!("test2") 237 | assert Developers.get_rank(developer, :turkey) === 2 238 | end 239 | 240 | test "returns rank of developer in his/her location" do 241 | developer = Developers.get_developer_by_username!("test2") 242 | assert Developers.get_rank(developer, :in_location) === 1 243 | end 244 | end 245 | 246 | describe "get_repositories/2" do 247 | setup do 248 | location = LocationsHelper.create_location() 249 | 250 | developer = 251 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username"}) 252 | 253 | language = LanguagesHelper.create_language() 254 | 255 | for i <- 1..3 do 256 | attrs = 257 | RepositoriesHelper.get_repository_attrs() 258 | |> Map.put(:developer_id, developer.id) 259 | |> Map.put(:language_id, language.id) 260 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 261 | |> Map.update!(:stars, fn _ -> i end) 262 | 263 | RepositoriesHelper.create_repository(attrs) 264 | end 265 | 266 | # Add another repo 267 | developer2 = 268 | DevelopersHelper.create_developer(%{ 269 | username: "test2", 270 | github_id: 126, 271 | location_id: location.id 272 | }) 273 | 274 | attrs = 275 | RepositoriesHelper.get_repository_attrs() 276 | |> Map.put(:developer_id, developer2.id) 277 | |> Map.put(:language_id, language.id) 278 | |> Map.update!(:slug, fn _ -> "another-slug" end) 279 | 280 | RepositoriesHelper.create_repository(attrs) 281 | 282 | :ok 283 | end 284 | 285 | test "returns developer's repositories" do 286 | developer = Developers.get_developer_by_username!("username") 287 | 288 | repositories = 289 | Developers.get_repositories(developer, %{limit: 4, offset: 0, order_by: {:desc, :id}}) 290 | 291 | assert length(repositories) === 3 292 | 293 | assert Map.get(List.first(repositories), :developer_id) === developer.id 294 | assert Map.get(List.last(repositories), :developer_id) === developer.id 295 | end 296 | end 297 | 298 | describe "get_repositories_count/1" do 299 | setup do 300 | location = LocationsHelper.create_location() 301 | 302 | developer = 303 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username"}) 304 | 305 | language = LanguagesHelper.create_language() 306 | 307 | for i <- 1..3 do 308 | attrs = 309 | RepositoriesHelper.get_repository_attrs() 310 | |> Map.put(:developer_id, developer.id) 311 | |> Map.put(:language_id, language.id) 312 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 313 | |> Map.update!(:stars, fn _ -> i end) 314 | 315 | RepositoriesHelper.create_repository(attrs) 316 | end 317 | 318 | # Add another repo 319 | developer2 = 320 | DevelopersHelper.create_developer(%{ 321 | username: "test2", 322 | github_id: 1236, 323 | location_id: location.id 324 | }) 325 | 326 | attrs = 327 | RepositoriesHelper.get_repository_attrs() 328 | |> Map.put(:developer_id, developer2.id) 329 | |> Map.put(:language_id, language.id) 330 | |> Map.update!(:slug, fn _ -> "another-slug" end) 331 | 332 | RepositoriesHelper.create_repository(attrs) 333 | 334 | :ok 335 | end 336 | 337 | test "returns repositories count of developer" do 338 | developer = Developers.get_developer_by_username!("username") 339 | 340 | assert Developers.get_repositories_count(developer) === 3 341 | end 342 | end 343 | end 344 | -------------------------------------------------------------------------------- /test/githubist/languages/languages_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.LanguagesTest do 2 | use Githubist.DataCase 3 | 4 | alias Ecto.{Changeset, NoResultsError} 5 | alias Githubist.Languages 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @language_attrs %{ 12 | name: "Elixir", 13 | slug: "elixir", 14 | score: 100.0, 15 | total_stars: 100, 16 | total_repositories: 100, 17 | total_developers: 100 18 | } 19 | 20 | def language_fixture(attrs \\ %{}) do 21 | merged_attrs = 22 | @language_attrs 23 | |> Map.merge(attrs) 24 | 25 | {:ok, language} = Languages.create_language(merged_attrs) 26 | 27 | language 28 | end 29 | 30 | describe "get_language/1" do 31 | test "returns language by id" do 32 | language = language_fixture() 33 | 34 | fetched_language = Languages.get_language(language.id) 35 | 36 | assert language.id === fetched_language.id 37 | end 38 | 39 | test "returns nil if language does not exist" do 40 | assert nil === Languages.get_language(2) 41 | end 42 | end 43 | 44 | describe "get_language!/1" do 45 | test "returns language by id" do 46 | language = language_fixture() 47 | 48 | fetched_language = Languages.get_language!(language.id) 49 | 50 | assert language.id === fetched_language.id 51 | end 52 | 53 | test "raise an exception if language does not exist" do 54 | assert_raise NoResultsError, fn -> 55 | Languages.get_language!(2) 56 | end 57 | end 58 | end 59 | 60 | describe "get_language_by_slug/1" do 61 | test "returns language by slug" do 62 | language = language_fixture() 63 | 64 | fetched_language = Languages.get_language_by_slug(language.slug) 65 | 66 | assert language.id === fetched_language.id 67 | end 68 | 69 | test "returns nil if language does not exist" do 70 | assert nil === Languages.get_language_by_slug("none") 71 | end 72 | end 73 | 74 | describe "get_language_by_slug!/1" do 75 | test "returns language by slug" do 76 | language = language_fixture() 77 | 78 | fetched_language = Languages.get_language_by_slug!(language.slug) 79 | 80 | assert language.id === fetched_language.id 81 | end 82 | 83 | test "raise an exception if language does not exist" do 84 | assert_raise NoResultsError, fn -> 85 | Languages.get_language_by_slug!("none") 86 | end 87 | end 88 | end 89 | 90 | describe "create_language/1" do 91 | test "creates a language successfully" do 92 | language = language_fixture() 93 | 94 | assert language.name === @language_attrs.name 95 | assert language.slug === @language_attrs.slug 96 | assert language.score === @language_attrs.score 97 | assert language.total_stars === @language_attrs.total_stars 98 | assert language.total_repositories === @language_attrs.total_repositories 99 | assert language.total_developers === @language_attrs.total_developers 100 | end 101 | 102 | test "returns error if validation fails" do 103 | invalid_attrs = @language_attrs |> Map.merge(%{name: nil}) 104 | 105 | assert {:error, %Changeset{errors: errors}} = Languages.create_language(invalid_attrs) 106 | 107 | assert {_, [validation: :required]} = errors[:name] 108 | end 109 | 110 | test "returns error if slug is already in use" do 111 | language_fixture() 112 | 113 | new_language_attrs = 114 | @language_attrs 115 | |> Map.put(:name, "PHP") 116 | 117 | {:error, %Changeset{errors: errors}} = Languages.create_language(new_language_attrs) 118 | 119 | assert errors === [slug: {"has already been taken", []}] 120 | end 121 | end 122 | 123 | describe "all/1" do 124 | setup do 125 | for i <- 1..3 do 126 | attrs = 127 | @language_attrs 128 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 129 | |> Map.update!(:score, fn _ -> i end) 130 | 131 | Languages.create_language(attrs) 132 | end 133 | end 134 | 135 | test "returns languages as given limit" do 136 | languages = Languages.all(%{limit: 2, offset: 0, order_by: {:desc, :id}}) 137 | 138 | assert length(languages) === 2 139 | end 140 | 141 | test "returns languages as starts from given offset" do 142 | languages = Languages.all(%{limit: 2, offset: 2, order_by: {:desc, :id}}) 143 | 144 | assert length(languages) === 1 145 | end 146 | 147 | test "returns languages as given order" do 148 | languages = Languages.all(%{limit: 3, offset: 0, order_by: {:desc, :score}}) 149 | 150 | assert Map.get(List.first(languages), :score) === 3.0 151 | assert Map.get(List.last(languages), :score) === 1.0 152 | end 153 | end 154 | 155 | describe "get_languages_count/0" do 156 | setup do 157 | for i <- 1..3 do 158 | attrs = 159 | @language_attrs 160 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 161 | |> Map.update!(:score, fn _ -> i end) 162 | 163 | Languages.create_language(attrs) 164 | end 165 | end 166 | 167 | test "returns languages count" do 168 | count = Languages.get_languages_count() 169 | 170 | assert count === 3 171 | end 172 | end 173 | 174 | describe "get_rank/2" do 175 | setup do 176 | language1 = 177 | @language_attrs 178 | |> Map.update!(:slug, fn _ -> "slug1" end) 179 | |> Map.update!(:score, fn _ -> 1000.0 end) 180 | 181 | language2 = 182 | @language_attrs 183 | |> Map.update!(:slug, fn _ -> "slug2" end) 184 | |> Map.update!(:score, fn _ -> 500.0 end) 185 | 186 | Languages.create_language(language1) 187 | Languages.create_language(language2) 188 | 189 | :ok 190 | end 191 | 192 | test "returns rank of language in turkey" do 193 | language = Languages.get_language_by_slug!("slug2") 194 | assert Languages.get_rank(language) === 2 195 | end 196 | end 197 | 198 | describe "get_repositories/2" do 199 | test "returns the repositories for the given language" do 200 | language1 = LanguagesHelper.create_language(%{slug: "language1"}) 201 | language2 = LanguagesHelper.create_language(%{slug: "language2"}) 202 | 203 | location = LocationsHelper.create_location() 204 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 205 | 206 | for i <- 1..3 do 207 | RepositoriesHelper.create_repository(%{ 208 | developer_id: developer.id, 209 | language_id: language1.id, 210 | slug: "slug#{i}" 211 | }) 212 | end 213 | 214 | for i <- 4..6 do 215 | RepositoriesHelper.create_repository(%{ 216 | developer_id: developer.id, 217 | language_id: language2.id, 218 | slug: "slug#{i}" 219 | }) 220 | end 221 | 222 | repositories = 223 | Languages.get_repositories(language1, %{limit: 10, offset: 0, order_by: {:desc, :id}}) 224 | 225 | assert length(repositories) === 3 226 | assert Map.get(List.first(repositories), :slug) === "slug3" 227 | assert Map.get(List.last(repositories), :slug) === "slug1" 228 | end 229 | end 230 | 231 | describe "get_repositories_count/1" do 232 | test "returns the count of repositories for the given language" do 233 | language1 = LanguagesHelper.create_language(%{slug: "language1"}) 234 | language2 = LanguagesHelper.create_language(%{slug: "language2"}) 235 | 236 | location = LocationsHelper.create_location() 237 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 238 | 239 | for i <- 1..3 do 240 | RepositoriesHelper.create_repository(%{ 241 | developer_id: developer.id, 242 | language_id: language1.id, 243 | slug: "slug#{i}" 244 | }) 245 | end 246 | 247 | for i <- 4..6 do 248 | RepositoriesHelper.create_repository(%{ 249 | developer_id: developer.id, 250 | language_id: language2.id, 251 | slug: "slug#{i}" 252 | }) 253 | end 254 | 255 | repositories_count = Languages.get_repositories_count(language1) 256 | 257 | assert repositories_count === 3 258 | end 259 | end 260 | 261 | describe "get_repositories_count_rank/1" do 262 | test "returns rank of the given language according to repository counts" do 263 | language1 = LanguagesHelper.create_language(%{slug: "language1"}) 264 | language2 = LanguagesHelper.create_language(%{slug: "language2"}) 265 | 266 | location = LocationsHelper.create_location() 267 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 268 | 269 | for i <- 1..3 do 270 | RepositoriesHelper.create_repository(%{ 271 | developer_id: developer.id, 272 | language_id: language1.id, 273 | slug: "slug#{i}" 274 | }) 275 | end 276 | 277 | for i <- 4..7 do 278 | RepositoriesHelper.create_repository(%{ 279 | developer_id: developer.id, 280 | language_id: language2.id, 281 | slug: "slug#{i}" 282 | }) 283 | end 284 | 285 | assert Languages.get_repositories_count_rank(language2) === 1 286 | end 287 | end 288 | 289 | describe "get_developers_count_rank/1" do 290 | test "returns rank of the given language according to repository counts" do 291 | language1 = LanguagesHelper.create_language(%{slug: "language1"}) 292 | language2 = LanguagesHelper.create_language(%{slug: "language2"}) 293 | 294 | location = LocationsHelper.create_location() 295 | 296 | for i <- 1..3 do 297 | DevelopersHelper.create_developer(%{ 298 | location_id: location.id, 299 | language_id: language1.id, 300 | username: "username#{i}", 301 | github_id: i 302 | }) 303 | end 304 | 305 | for i <- 4..7 do 306 | DevelopersHelper.create_developer(%{ 307 | location_id: location.id, 308 | language_id: language2.id, 309 | username: "username#{i}", 310 | github_id: i 311 | }) 312 | end 313 | 314 | assert Languages.get_developers_count_rank(language2) === 1 315 | end 316 | end 317 | 318 | describe "get_developers_count/1" do 319 | setup do 320 | language = LanguagesHelper.create_language(%{slug: "language1"}) 321 | 322 | location = LocationsHelper.create_location() 323 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 324 | 325 | developer2 = 326 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username2"}) 327 | 328 | for i <- 1..3 do 329 | RepositoriesHelper.create_repository(%{ 330 | developer_id: developer.id, 331 | language_id: language.id, 332 | slug: "slug#{i}" 333 | }) 334 | end 335 | 336 | for i <- 4..6 do 337 | RepositoriesHelper.create_repository(%{ 338 | developer_id: developer2.id, 339 | language_id: language.id, 340 | slug: "slug#{i}" 341 | }) 342 | end 343 | 344 | :ok 345 | end 346 | 347 | test "returns the count of repositories for the given language" do 348 | language = Languages.get_language_by_slug!("language1") 349 | 350 | repositories_count = Languages.get_developers_count(language) 351 | 352 | assert repositories_count === 2 353 | end 354 | end 355 | 356 | describe "get_location_usage/2" do 357 | test "returns location usage" do 358 | language1 = LanguagesHelper.create_language(%{slug: "language1"}) 359 | language2 = LanguagesHelper.create_language(%{slug: "language2"}) 360 | 361 | location = LocationsHelper.create_location(%{slug: "location"}) 362 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 363 | 364 | for i <- 1..3 do 365 | RepositoriesHelper.create_repository(%{ 366 | developer_id: developer.id, 367 | language_id: language1.id, 368 | slug: "slug#{i}" 369 | }) 370 | end 371 | 372 | for i <- 4..7 do 373 | RepositoriesHelper.create_repository(%{ 374 | developer_id: developer.id, 375 | language_id: language2.id, 376 | slug: "slug#{i}" 377 | }) 378 | end 379 | 380 | params = %{limit: 10, offset: 0} 381 | usage = Languages.get_location_usage(location, params) 382 | 383 | assert %{language: language2, repositories_count: 4} === Enum.at(usage, 0) 384 | assert %{language: language1, repositories_count: 3} === Enum.at(usage, 1) 385 | end 386 | end 387 | 388 | describe "get_developer_usage/2" do 389 | test "returns developer usage" do 390 | language1 = LanguagesHelper.create_language(%{slug: "language1"}) 391 | language2 = LanguagesHelper.create_language(%{slug: "language2"}) 392 | 393 | location = LocationsHelper.create_location(%{slug: "location"}) 394 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 395 | 396 | for i <- 1..3 do 397 | RepositoriesHelper.create_repository(%{ 398 | developer_id: developer.id, 399 | language_id: language1.id, 400 | slug: "slug#{i}" 401 | }) 402 | end 403 | 404 | for i <- 4..7 do 405 | RepositoriesHelper.create_repository(%{ 406 | developer_id: developer.id, 407 | language_id: language2.id, 408 | slug: "slug#{i}" 409 | }) 410 | end 411 | 412 | params = %{limit: 10, offset: 0} 413 | usage = Languages.get_developer_usage(developer, params) 414 | 415 | assert %{language: language2, repositories_count: 4} === Enum.at(usage, 0) 416 | assert %{language: language1, repositories_count: 3} === Enum.at(usage, 1) 417 | end 418 | end 419 | 420 | describe "get_location_stats/2" do 421 | test "returns location stats" do 422 | language = LanguagesHelper.create_language() 423 | 424 | location1 = LocationsHelper.create_location(%{slug: "location1"}) 425 | location2 = LocationsHelper.create_location(%{slug: "location2"}) 426 | 427 | developer1 = 428 | DevelopersHelper.create_developer(%{location_id: location1.id, username: "developer1"}) 429 | 430 | developer2 = 431 | DevelopersHelper.create_developer(%{location_id: location2.id, username: "developer2"}) 432 | 433 | for i <- 1..3 do 434 | RepositoriesHelper.create_repository(%{ 435 | developer_id: developer1.id, 436 | language_id: language.id, 437 | slug: "slug#{i}" 438 | }) 439 | end 440 | 441 | for i <- 4..7 do 442 | RepositoriesHelper.create_repository(%{ 443 | developer_id: developer2.id, 444 | language_id: language.id, 445 | slug: "slug#{i}" 446 | }) 447 | end 448 | 449 | params = %{limit: 10, offset: 0} 450 | usage = Languages.get_location_stats(language, params) 451 | 452 | assert %{location: location2, repositories_count: 4} === Enum.at(usage, 0) 453 | assert %{location: location1, repositories_count: 3} === Enum.at(usage, 1) 454 | end 455 | end 456 | 457 | describe "get_develoepr_stats/2" do 458 | test "returns developer stats" do 459 | language = LanguagesHelper.create_language() 460 | 461 | location1 = LocationsHelper.create_location(%{slug: "location1"}) 462 | location2 = LocationsHelper.create_location(%{slug: "location2"}) 463 | 464 | developer1 = 465 | DevelopersHelper.create_developer(%{location_id: location1.id, username: "developer1"}) 466 | 467 | developer2 = 468 | DevelopersHelper.create_developer(%{location_id: location2.id, username: "developer2"}) 469 | 470 | for i <- 1..3 do 471 | RepositoriesHelper.create_repository(%{ 472 | developer_id: developer1.id, 473 | language_id: language.id, 474 | slug: "slug#{i}" 475 | }) 476 | end 477 | 478 | for i <- 4..7 do 479 | RepositoriesHelper.create_repository(%{ 480 | developer_id: developer2.id, 481 | language_id: language.id, 482 | slug: "slug#{i}" 483 | }) 484 | end 485 | 486 | params = %{limit: 10, offset: 0} 487 | usage = Languages.get_developer_stats(language, params) 488 | 489 | assert %{developer: developer2, repositories_count: 4} === Enum.at(usage, 0) 490 | assert %{developer: developer1, repositories_count: 3} === Enum.at(usage, 1) 491 | end 492 | end 493 | end 494 | -------------------------------------------------------------------------------- /test/githubist/locations/locations_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.LocationsTest do 2 | use Githubist.DataCase 3 | 4 | alias Ecto.{Changeset, NoResultsError} 5 | alias Githubist.Locations 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @location_attrs %{ 12 | name: "Izmir", 13 | slug: "izmir", 14 | score: 100.0, 15 | total_repositories: 100, 16 | total_developers: 100 17 | } 18 | 19 | def location_fixture(attrs \\ %{}) do 20 | merged_attrs = 21 | @location_attrs 22 | |> Map.merge(attrs) 23 | 24 | {:ok, location} = Locations.create_location(merged_attrs) 25 | 26 | location 27 | end 28 | 29 | describe "get_location/1" do 30 | test "returns location by id" do 31 | location = location_fixture() 32 | 33 | fetched_location = Locations.get_location(location.id) 34 | 35 | assert location.id === fetched_location.id 36 | end 37 | 38 | test "returns nil if location does not exist" do 39 | assert nil === Locations.get_location(2) 40 | end 41 | end 42 | 43 | describe "get_location!/1" do 44 | test "returns location by id" do 45 | location = location_fixture() 46 | 47 | fetched_location = Locations.get_location!(location.id) 48 | 49 | assert location.id === fetched_location.id 50 | end 51 | 52 | test "raise an exception if location does not exist" do 53 | assert_raise NoResultsError, fn -> 54 | Locations.get_location!(2) 55 | end 56 | end 57 | end 58 | 59 | describe "get_location_by_slug/1" do 60 | test "returns location by slug" do 61 | location = location_fixture() 62 | 63 | fetched_location = Locations.get_location_by_slug(location.slug) 64 | 65 | assert location.id === fetched_location.id 66 | end 67 | 68 | test "returns nil if location does not exist" do 69 | assert nil === Locations.get_location_by_slug("none") 70 | end 71 | end 72 | 73 | describe "get_location_by_slug!/1" do 74 | test "returns location by slug" do 75 | location = location_fixture() 76 | 77 | fetched_location = Locations.get_location_by_slug!(location.slug) 78 | 79 | assert location.id === fetched_location.id 80 | end 81 | 82 | test "raise an exception if location does not exist" do 83 | assert_raise NoResultsError, fn -> 84 | Locations.get_location_by_slug!("none") 85 | end 86 | end 87 | end 88 | 89 | describe "create_location/1" do 90 | test "creates a location successfully" do 91 | location = location_fixture() 92 | 93 | assert location.name === @location_attrs.name 94 | assert location.slug === @location_attrs.slug 95 | assert location.score === @location_attrs.score 96 | assert location.total_repositories === @location_attrs.total_repositories 97 | assert location.total_developers === @location_attrs.total_developers 98 | end 99 | 100 | test "returns error if validation fails" do 101 | invalid_attrs = @location_attrs |> Map.merge(%{name: nil}) 102 | 103 | assert {:error, %Changeset{errors: errors}} = Locations.create_location(invalid_attrs) 104 | 105 | assert {_, [validation: :required]} = errors[:name] 106 | end 107 | 108 | test "returns error if slug is already in use" do 109 | location_fixture() 110 | 111 | new_location_attrs = 112 | @location_attrs 113 | |> Map.put(:name, "PHP") 114 | 115 | {:error, %Changeset{errors: errors}} = Locations.create_location(new_location_attrs) 116 | 117 | assert errors === [slug: {"has already been taken", []}] 118 | end 119 | end 120 | 121 | describe "all/1" do 122 | setup do 123 | for i <- 1..3 do 124 | attrs = 125 | @location_attrs 126 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 127 | |> Map.update!(:score, fn _ -> i end) 128 | 129 | Locations.create_location(attrs) 130 | end 131 | end 132 | 133 | test "returns locations as given limit" do 134 | locations = Locations.all(%{limit: 2, offset: 0, order_by: {:desc, :id}}) 135 | 136 | assert length(locations) === 2 137 | end 138 | 139 | test "returns locations as starts from given offset" do 140 | locations = Locations.all(%{limit: 2, offset: 2, order_by: {:desc, :id}}) 141 | 142 | assert length(locations) === 1 143 | end 144 | 145 | test "returns locations as given order" do 146 | locations = Locations.all(%{limit: 3, offset: 0, order_by: {:desc, :score}}) 147 | 148 | assert Map.get(List.first(locations), :score) === 3.0 149 | assert Map.get(List.last(locations), :score) === 1.0 150 | end 151 | end 152 | 153 | describe "get_locations_count/0" do 154 | setup do 155 | for i <- 1..3 do 156 | attrs = 157 | @location_attrs 158 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 159 | |> Map.update!(:score, fn _ -> i end) 160 | 161 | Locations.create_location(attrs) 162 | end 163 | end 164 | 165 | test "returns locations count" do 166 | count = Locations.get_locations_count() 167 | 168 | assert count === 3 169 | end 170 | end 171 | 172 | describe "get_rank/2" do 173 | setup do 174 | location1 = 175 | @location_attrs 176 | |> Map.update!(:slug, fn _ -> "slug1" end) 177 | |> Map.update!(:score, fn _ -> 1000.0 end) 178 | 179 | location2 = 180 | @location_attrs 181 | |> Map.update!(:slug, fn _ -> "slug2" end) 182 | |> Map.update!(:score, fn _ -> 500.0 end) 183 | 184 | Locations.create_location(location1) 185 | Locations.create_location(location2) 186 | 187 | :ok 188 | end 189 | 190 | test "returns rank of location" do 191 | location = Locations.get_location_by_slug!("slug2") 192 | assert Locations.get_rank(location) === 2 193 | end 194 | end 195 | 196 | describe "get_developers/2" do 197 | setup do 198 | location = LocationsHelper.create_location(%{slug: "location"}) 199 | location2 = LocationsHelper.create_location(%{slug: "location2"}) 200 | 201 | for i <- 1..3 do 202 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username#{i}"}) 203 | end 204 | 205 | for i <- 4..6 do 206 | DevelopersHelper.create_developer(%{ 207 | location_id: location2.id, 208 | username: "username#{i}" 209 | }) 210 | end 211 | 212 | :ok 213 | end 214 | 215 | test "returns the developers in the given location" do 216 | location = Locations.get_location_by_slug!("location") 217 | 218 | developers = 219 | Locations.get_developers(location, %{limit: 10, offset: 0, order_by: {:desc, :id}}) 220 | 221 | assert length(developers) === 3 222 | assert Map.get(List.first(developers), :username) === "username3" 223 | assert Map.get(List.last(developers), :username) === "username1" 224 | end 225 | end 226 | 227 | describe "get_repositories/2" do 228 | setup do 229 | location = LocationsHelper.create_location(%{slug: "location"}) 230 | location2 = LocationsHelper.create_location(%{slug: "location2"}) 231 | 232 | developer = 233 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username1"}) 234 | 235 | developer2 = 236 | DevelopersHelper.create_developer(%{location_id: location2.id, username: "username2"}) 237 | 238 | language = LanguagesHelper.create_language() 239 | 240 | for i <- 1..3 do 241 | RepositoriesHelper.create_repository(%{ 242 | developer_id: developer.id, 243 | language_id: language.id, 244 | slug: "slug#{i}" 245 | }) 246 | end 247 | 248 | for i <- 4..6 do 249 | RepositoriesHelper.create_repository(%{ 250 | developer_id: developer2.id, 251 | language_id: language.id, 252 | slug: "slug#{i}" 253 | }) 254 | end 255 | 256 | :ok 257 | end 258 | 259 | test "returns the repositories in the given location" do 260 | location = Locations.get_location_by_slug!("location") 261 | 262 | repositories = 263 | Locations.get_repositories(location, %{limit: 10, offset: 0, order_by: {:desc, :id}}) 264 | 265 | assert length(repositories) === 3 266 | assert Map.get(List.first(repositories), :slug) === "slug3" 267 | assert Map.get(List.last(repositories), :slug) === "slug1" 268 | end 269 | end 270 | 271 | describe "get_developers_count/1" do 272 | setup do 273 | location = LocationsHelper.create_location(%{slug: "location"}) 274 | location2 = LocationsHelper.create_location(%{slug: "location2"}) 275 | 276 | for i <- 1..3 do 277 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username#{i}"}) 278 | end 279 | 280 | for i <- 4..6 do 281 | DevelopersHelper.create_developer(%{ 282 | location_id: location2.id, 283 | username: "username#{i}" 284 | }) 285 | end 286 | 287 | :ok 288 | end 289 | 290 | test "returns the count of developers in the given location" do 291 | location = Locations.get_location_by_slug!("location") 292 | 293 | developers_count = Locations.get_developers_count(location) 294 | 295 | assert developers_count === 3 296 | end 297 | end 298 | 299 | describe "get_repositories_count/1" do 300 | setup do 301 | location = LocationsHelper.create_location(%{slug: "location"}) 302 | location2 = LocationsHelper.create_location(%{slug: "location2"}) 303 | 304 | developer = 305 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username1"}) 306 | 307 | developer2 = 308 | DevelopersHelper.create_developer(%{location_id: location2.id, username: "username2"}) 309 | 310 | language = LanguagesHelper.create_language() 311 | 312 | for i <- 1..3 do 313 | RepositoriesHelper.create_repository(%{ 314 | developer_id: developer.id, 315 | language_id: language.id, 316 | slug: "slug#{i}" 317 | }) 318 | end 319 | 320 | for i <- 4..6 do 321 | RepositoriesHelper.create_repository(%{ 322 | developer_id: developer2.id, 323 | language_id: language.id, 324 | slug: "slug#{i}" 325 | }) 326 | end 327 | 328 | :ok 329 | end 330 | 331 | test "returns the count of repositories in the given location" do 332 | location = Locations.get_location_by_slug!("location") 333 | 334 | repositories_count = Locations.get_repositories_count(location) 335 | 336 | assert repositories_count === 3 337 | end 338 | end 339 | end 340 | -------------------------------------------------------------------------------- /test/githubist/repositories/repositories_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Githubist.RepositoriesTest do 2 | use Githubist.DataCase 3 | 4 | alias Ecto.{Changeset, NoResultsError} 5 | alias Githubist.Repositories 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | 10 | @repository_attrs %{ 11 | name: "repo", 12 | slug: "username/repo", 13 | description: "Lorem ipsum dolar sit amet.", 14 | github_id: 123, 15 | github_url: "https://github.com/username/repo", 16 | stars: 100, 17 | forks: 100, 18 | github_created_at: DateTime.utc_now() 19 | } 20 | 21 | def repository_fixture(attrs \\ %{}) do 22 | location = LocationsHelper.create_location() 23 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 24 | language = LanguagesHelper.create_language() 25 | 26 | merged_attrs = 27 | @repository_attrs 28 | |> Map.put(:developer_id, developer.id) 29 | |> Map.put(:language_id, language.id) 30 | |> Map.merge(attrs) 31 | 32 | {:ok, repository} = Repositories.create_repository(merged_attrs) 33 | 34 | repository 35 | end 36 | 37 | describe "get_repository/1" do 38 | test "returns repository by id" do 39 | repository = repository_fixture() 40 | 41 | fetched_repository = Repositories.get_repository(repository.id) 42 | 43 | assert repository.id === fetched_repository.id 44 | end 45 | 46 | test "returns nil if repository does not exist" do 47 | assert nil === Repositories.get_repository(2) 48 | end 49 | end 50 | 51 | describe "get_repository!/1" do 52 | test "returns repository by id" do 53 | repository = repository_fixture() 54 | 55 | fetched_repository = Repositories.get_repository!(repository.id) 56 | 57 | assert repository.id === fetched_repository.id 58 | end 59 | 60 | test "raise an exception if repository does not exist" do 61 | assert_raise NoResultsError, fn -> 62 | Repositories.get_repository!(2) 63 | end 64 | end 65 | end 66 | 67 | describe "get_repository_by_slug/1" do 68 | test "returns repository by slug" do 69 | repository = repository_fixture() 70 | 71 | fetched_repository = Repositories.get_repository_by_slug(repository.slug) 72 | 73 | assert repository.id === fetched_repository.id 74 | end 75 | 76 | test "returns nil if repository does not exist" do 77 | assert nil === Repositories.get_repository_by_slug("none") 78 | end 79 | end 80 | 81 | describe "get_repository_by_slug!/1" do 82 | test "returns repository by slug" do 83 | repository = repository_fixture() 84 | 85 | fetched_repository = Repositories.get_repository_by_slug!(repository.slug) 86 | 87 | assert repository.id === fetched_repository.id 88 | end 89 | 90 | test "raise an exception if repository does not exist" do 91 | assert_raise NoResultsError, fn -> 92 | Repositories.get_repository_by_slug!("none") 93 | end 94 | end 95 | end 96 | 97 | describe "create_repository/1" do 98 | test "creates a repository successfully" do 99 | repository = repository_fixture() 100 | 101 | assert repository.name === @repository_attrs.name 102 | assert repository.slug === @repository_attrs.slug 103 | assert repository.description === @repository_attrs.description 104 | assert repository.github_id === @repository_attrs.github_id 105 | assert repository.github_url === @repository_attrs.github_url 106 | assert repository.stars === @repository_attrs.stars 107 | assert repository.forks === @repository_attrs.forks 108 | assert repository.github_created_at === @repository_attrs.github_created_at 109 | end 110 | 111 | test "returns error if validation fails" do 112 | location = LocationsHelper.create_location() 113 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 114 | language = LanguagesHelper.create_language() 115 | 116 | invalid_attrs = 117 | @repository_attrs 118 | |> Map.merge(%{name: nil, developer_id: developer.id, language_id: language.id}) 119 | 120 | assert {:error, %Changeset{errors: errors}} = Repositories.create_repository(invalid_attrs) 121 | 122 | assert {_, [validation: :required]} = errors[:name] 123 | end 124 | 125 | test "returns error if slug is already in use" do 126 | repository_fixture() 127 | 128 | new_repository_attrs = 129 | @repository_attrs 130 | |> Map.put(:name, "Test 2") 131 | |> Map.put(:developer_id, 1) 132 | |> Map.put(:language_id, 1) 133 | 134 | {:error, %Changeset{errors: errors}} = Repositories.create_repository(new_repository_attrs) 135 | 136 | assert errors === [slug: {"has already been taken", []}] 137 | end 138 | 139 | test "returns error if developer is invalid" do 140 | language = LanguagesHelper.create_language() 141 | 142 | invalid_attrs = 143 | @repository_attrs |> Map.put(:developer_id, 1234) |> Map.put(:language_id, language.id) 144 | 145 | assert {:error, %Changeset{errors: errors}} = Repositories.create_repository(invalid_attrs) 146 | 147 | assert errors === [developer_id: {"does not exist", []}] 148 | end 149 | 150 | test "returns error if language is invalid" do 151 | location = LocationsHelper.create_location() 152 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 153 | 154 | invalid_attrs = 155 | @repository_attrs |> Map.put(:language_id, 1234) |> Map.put(:developer_id, developer.id) 156 | 157 | assert {:error, %Changeset{errors: errors}} = Repositories.create_repository(invalid_attrs) 158 | 159 | assert errors === [language_id: {"does not exist", []}] 160 | end 161 | end 162 | 163 | describe "all/1" do 164 | setup do 165 | location = LocationsHelper.create_location() 166 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 167 | language = LanguagesHelper.create_language() 168 | 169 | for i <- 1..3 do 170 | attrs = 171 | @repository_attrs 172 | |> Map.put(:developer_id, developer.id) 173 | |> Map.put(:language_id, language.id) 174 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 175 | |> Map.update!(:stars, fn _ -> i end) 176 | 177 | Repositories.create_repository(attrs) 178 | end 179 | end 180 | 181 | test "returns repositories as given limit" do 182 | repositories = Repositories.all(%{limit: 2, offset: 0, order_by: {:desc, :id}}) 183 | 184 | assert length(repositories) === 2 185 | end 186 | 187 | test "returns repositories as starts from given offset" do 188 | repositories = Repositories.all(%{limit: 2, offset: 2, order_by: {:desc, :id}}) 189 | 190 | assert length(repositories) === 1 191 | end 192 | 193 | test "returns repositories as given order" do 194 | repositories = Repositories.all(%{limit: 3, offset: 0, order_by: {:desc, :stars}}) 195 | 196 | assert Map.get(List.first(repositories), :stars) === 3 197 | assert Map.get(List.last(repositories), :stars) === 1 198 | end 199 | end 200 | 201 | describe "get_repositories_count/0" do 202 | setup do 203 | location = LocationsHelper.create_location() 204 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 205 | language = LanguagesHelper.create_language() 206 | 207 | for i <- 1..3 do 208 | attrs = 209 | @repository_attrs 210 | |> Map.put(:developer_id, developer.id) 211 | |> Map.put(:language_id, language.id) 212 | |> Map.update!(:slug, fn slug -> "#{slug}-#{i}" end) 213 | 214 | Repositories.create_repository(attrs) 215 | end 216 | end 217 | 218 | test "returns repositories count" do 219 | count = Repositories.get_repositories_count() 220 | 221 | assert count === 3 222 | end 223 | end 224 | 225 | describe "get_rank/2" do 226 | setup do 227 | location = LocationsHelper.create_location() 228 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 229 | language = LanguagesHelper.create_language() 230 | language2 = LanguagesHelper.create_language(%{name: "Haskell", slug: "haskell"}) 231 | 232 | repository1 = 233 | @repository_attrs 234 | |> Map.put(:developer_id, developer.id) 235 | |> Map.put(:language_id, language.id) 236 | |> Map.update!(:slug, fn _ -> "slug1" end) 237 | |> Map.update!(:stars, fn _ -> 1000 end) 238 | 239 | repository2 = 240 | @repository_attrs 241 | |> Map.put(:developer_id, developer.id) 242 | |> Map.put(:language_id, language2.id) 243 | |> Map.update!(:slug, fn _ -> "slug2" end) 244 | |> Map.update!(:stars, fn _ -> 500 end) 245 | 246 | Repositories.create_repository(repository1) 247 | Repositories.create_repository(repository2) 248 | 249 | :ok 250 | end 251 | 252 | test "returns rank of repository in turkey" do 253 | repository = Repositories.get_repository_by_slug!("slug2") 254 | assert Repositories.get_rank(repository, :turkey) === 2 255 | end 256 | 257 | test "returns rank of repository for a language" do 258 | repository = Repositories.get_repository_by_slug!("slug2") 259 | assert Repositories.get_rank(repository, :in_language) === 1 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/developer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.DeveloperTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query($username: String!) { 13 | developer(username: $username) { 14 | id 15 | username 16 | github_id 17 | name 18 | avatar_url 19 | bio 20 | company 21 | github_location 22 | github_url 23 | followers 24 | following 25 | public_repos 26 | total_starred 27 | score 28 | github_created_at 29 | stats { 30 | rank 31 | locationRank 32 | repositoriesCount 33 | } 34 | } 35 | } 36 | """ 37 | 38 | @with_repositories_query """ 39 | query($username: String!) { 40 | developer(username: $username) { 41 | id 42 | repositories(limit: 10, offset: 0, orderBy: {direction: DESC, field: NAME}) { 43 | name 44 | } 45 | } 46 | } 47 | """ 48 | 49 | @with_language_usage_query """ 50 | query($username: String!) { 51 | developer(username: $username) { 52 | id 53 | languageUsage(limit: 10, offset: 0) { 54 | language { 55 | name 56 | } 57 | 58 | repositoriesCount 59 | } 60 | } 61 | } 62 | """ 63 | 64 | setup do 65 | language = LanguagesHelper.create_language(%{name: "language", slug: "language-1"}) 66 | 67 | location = 68 | LocationsHelper.create_location(%{ 69 | name: "Location 1", 70 | slug: "location-1" 71 | }) 72 | 73 | developer = 74 | DevelopersHelper.create_developer(%{ 75 | location_id: location.id, 76 | name: "Developer 1", 77 | username: "username" 78 | }) 79 | 80 | RepositoriesHelper.create_repository(%{ 81 | name: "repository", 82 | language_id: language.id, 83 | developer_id: developer.id 84 | }) 85 | 86 | {:ok, %{developer: developer}} 87 | end 88 | 89 | test "returns basic data", %{conn: conn, developer: developer} do 90 | conn = 91 | conn 92 | |> put_graphql_headers() 93 | |> post("/graphql", %{query: @basic_query, variables: %{username: "username"}}) 94 | 95 | assert json_response(conn, 200) === %{ 96 | "data" => %{ 97 | "developer" => %{ 98 | "id" => to_string(developer.id), 99 | "username" => developer.username, 100 | "github_id" => developer.github_id, 101 | "name" => developer.name, 102 | "avatar_url" => developer.avatar_url, 103 | "bio" => developer.bio, 104 | "company" => developer.company, 105 | "github_location" => developer.github_location, 106 | "github_url" => developer.github_url, 107 | "followers" => developer.followers, 108 | "following" => developer.following, 109 | "public_repos" => developer.public_repos, 110 | "total_starred" => developer.total_starred, 111 | "score" => developer.score, 112 | "github_created_at" => DateTime.to_iso8601(developer.github_created_at), 113 | "stats" => %{ 114 | "rank" => 1, 115 | "locationRank" => 1, 116 | "repositoriesCount" => 1 117 | } 118 | } 119 | } 120 | } 121 | end 122 | 123 | test "returns repositories for developer", %{conn: conn, developer: developer} do 124 | conn = 125 | conn 126 | |> put_graphql_headers() 127 | |> post("/graphql", %{query: @with_repositories_query, variables: %{username: "username"}}) 128 | 129 | assert json_response(conn, 200) === %{ 130 | "data" => %{ 131 | "developer" => %{ 132 | "id" => to_string(developer.id), 133 | "repositories" => [ 134 | %{"name" => "repository"} 135 | ] 136 | } 137 | } 138 | } 139 | end 140 | 141 | test "returns language usage for developer", %{conn: conn, developer: developer} do 142 | conn = 143 | conn 144 | |> put_graphql_headers() 145 | |> post("/graphql", %{query: @with_language_usage_query, variables: %{username: "username"}}) 146 | 147 | assert json_response(conn, 200) === %{ 148 | "data" => %{ 149 | "developer" => %{ 150 | "id" => to_string(developer.id), 151 | "languageUsage" => [ 152 | %{"language" => %{"name" => "language"}, "repositoriesCount" => 1} 153 | ] 154 | } 155 | } 156 | } 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/developers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.DevelopersTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query { 13 | developers(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 14 | id 15 | username 16 | github_id 17 | name 18 | avatar_url 19 | bio 20 | company 21 | github_location 22 | github_url 23 | followers 24 | following 25 | public_repos 26 | total_starred 27 | score 28 | github_created_at 29 | stats { 30 | rank 31 | locationRank 32 | repositoriesCount 33 | } 34 | } 35 | } 36 | """ 37 | 38 | @with_repositories_query """ 39 | query { 40 | developers(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 41 | name 42 | repositories(limit: 2, offset: 0, orderBy: {direction: ASC, field: NAME}) { 43 | name 44 | } 45 | } 46 | } 47 | """ 48 | 49 | @with_language_usage_query """ 50 | query { 51 | developers(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 52 | name 53 | languageUsage(limit: 2, offset: 0) { 54 | language { 55 | name 56 | } 57 | 58 | repositoriesCount 59 | } 60 | } 61 | } 62 | """ 63 | 64 | setup do 65 | location = LocationsHelper.create_location(%{name: "Location 1"}) 66 | language = LanguagesHelper.create_language(%{name: "Language 1"}) 67 | 68 | developer1 = 69 | DevelopersHelper.create_developer(%{ 70 | name: "Developer 1", 71 | username: "developer-1", 72 | github_id: 1, 73 | score: 1.0, 74 | location_id: location.id 75 | }) 76 | 77 | developer2 = 78 | DevelopersHelper.create_developer(%{ 79 | name: "Developer 2", 80 | username: "developer-2", 81 | github_id: 2, 82 | score: 2.0, 83 | location_id: location.id 84 | }) 85 | 86 | developer3 = 87 | DevelopersHelper.create_developer(%{ 88 | name: "Developer 3", 89 | username: "developer-3", 90 | github_id: 3, 91 | score: 3.0, 92 | location_id: location.id 93 | }) 94 | 95 | RepositoriesHelper.create_repository(%{ 96 | name: "Repository 1", 97 | slug: "repository-1", 98 | developer_id: developer3.id, 99 | language_id: language.id 100 | }) 101 | 102 | RepositoriesHelper.create_repository(%{ 103 | name: "Repository 2", 104 | slug: "repository-2", 105 | developer_id: developer3.id, 106 | language_id: language.id 107 | }) 108 | 109 | RepositoriesHelper.create_repository(%{ 110 | name: "Repository 3", 111 | slug: "repository-3", 112 | developer_id: developer2.id, 113 | language_id: language.id 114 | }) 115 | 116 | {:ok, %{developers: {developer1, developer2, developer3}}} 117 | end 118 | 119 | test "returns basic data", %{conn: conn, developers: {_, developer2, developer3}} do 120 | conn = 121 | conn 122 | |> put_graphql_headers() 123 | |> post("/graphql", @basic_query) 124 | 125 | assert json_response(conn, 200) === %{ 126 | "data" => %{ 127 | "developers" => [ 128 | %{ 129 | "id" => to_string(developer3.id), 130 | "username" => developer3.username, 131 | "github_id" => developer3.github_id, 132 | "name" => developer3.name, 133 | "avatar_url" => developer3.avatar_url, 134 | "bio" => developer3.bio, 135 | "company" => developer3.company, 136 | "github_location" => developer3.github_location, 137 | "github_url" => developer3.github_url, 138 | "followers" => developer3.followers, 139 | "following" => developer3.following, 140 | "public_repos" => developer3.public_repos, 141 | "total_starred" => developer3.total_starred, 142 | "score" => developer3.score, 143 | "github_created_at" => DateTime.to_iso8601(developer3.github_created_at), 144 | "stats" => %{ 145 | "rank" => 1, 146 | "locationRank" => 1, 147 | "repositoriesCount" => 2 148 | } 149 | }, 150 | %{ 151 | "id" => to_string(developer2.id), 152 | "username" => developer2.username, 153 | "github_id" => developer2.github_id, 154 | "name" => developer2.name, 155 | "avatar_url" => developer2.avatar_url, 156 | "bio" => developer2.bio, 157 | "company" => developer2.company, 158 | "github_location" => developer2.github_location, 159 | "github_url" => developer2.github_url, 160 | "followers" => developer2.followers, 161 | "following" => developer2.following, 162 | "public_repos" => developer2.public_repos, 163 | "total_starred" => developer2.total_starred, 164 | "score" => developer2.score, 165 | "github_created_at" => DateTime.to_iso8601(developer2.github_created_at), 166 | "stats" => %{ 167 | "rank" => 2, 168 | "locationRank" => 2, 169 | "repositoriesCount" => 1 170 | } 171 | } 172 | ] 173 | } 174 | } 175 | end 176 | 177 | test "returns repositories of developer", %{conn: conn} do 178 | conn = 179 | conn 180 | |> put_graphql_headers() 181 | |> post("/graphql", @with_repositories_query) 182 | 183 | assert json_response(conn, 200) === %{ 184 | "data" => %{ 185 | "developers" => [ 186 | %{ 187 | "name" => "Developer 3", 188 | "repositories" => [ 189 | %{"name" => "Repository 1"}, 190 | %{"name" => "Repository 2"} 191 | ] 192 | }, 193 | %{ 194 | "name" => "Developer 2", 195 | "repositories" => [ 196 | %{"name" => "Repository 3"} 197 | ] 198 | } 199 | ] 200 | } 201 | } 202 | end 203 | 204 | test "returns language usage of developer", %{conn: conn} do 205 | conn = 206 | conn 207 | |> put_graphql_headers() 208 | |> post("/graphql", @with_language_usage_query) 209 | 210 | assert json_response(conn, 200) === %{ 211 | "data" => %{ 212 | "developers" => [ 213 | %{ 214 | "name" => "Developer 3", 215 | "languageUsage" => [ 216 | %{"language" => %{"name" => "Language 1"}, "repositoriesCount" => 2} 217 | ] 218 | }, 219 | %{ 220 | "name" => "Developer 2", 221 | "languageUsage" => [ 222 | %{"language" => %{"name" => "Language 1"}, "repositoriesCount" => 1} 223 | ] 224 | } 225 | ] 226 | } 227 | } 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/language_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.LanguageTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query($slug: String!) { 13 | language(slug: $slug) { 14 | id 15 | name 16 | slug 17 | score 18 | totalStars 19 | totalRepositories 20 | totalDevelopers 21 | stats { 22 | rank 23 | repositoriesCountRank 24 | } 25 | } 26 | } 27 | """ 28 | 29 | @with_repositories_query """ 30 | query($slug: String!) { 31 | language(slug: $slug) { 32 | id 33 | repositories(limit: 10, offset: 0, orderBy: {direction: DESC, field: NAME}) { 34 | name 35 | } 36 | } 37 | } 38 | """ 39 | 40 | @with_location_usage_query """ 41 | query($slug: String!) { 42 | language(slug: $slug) { 43 | id 44 | locationUsage(limit: 10, offset: 0) { 45 | location { 46 | name 47 | } 48 | 49 | repositoriesCount 50 | } 51 | } 52 | } 53 | """ 54 | 55 | @with_developer_usage_query """ 56 | query($slug: String!) { 57 | language(slug: $slug) { 58 | id 59 | developerUsage(limit: 10, offset: 0) { 60 | developer { 61 | name 62 | } 63 | 64 | repositoriesCount 65 | } 66 | } 67 | } 68 | """ 69 | 70 | setup do 71 | language = LanguagesHelper.create_language(%{name: "language", slug: "language-1"}) 72 | 73 | location = 74 | LocationsHelper.create_location(%{ 75 | name: "Location 1", 76 | slug: "location-1", 77 | score: 1.0 78 | }) 79 | 80 | developer = 81 | DevelopersHelper.create_developer(%{ 82 | location_id: location.id, 83 | name: "Developer 1", 84 | username: "username" 85 | }) 86 | 87 | RepositoriesHelper.create_repository(%{ 88 | name: "repository", 89 | language_id: language.id, 90 | developer_id: developer.id 91 | }) 92 | 93 | {:ok, %{language: language}} 94 | end 95 | 96 | test "returns basic data", %{conn: conn, language: language} do 97 | conn = 98 | conn 99 | |> put_graphql_headers() 100 | |> post("/graphql", %{query: @basic_query, variables: %{slug: "language-1"}}) 101 | 102 | assert json_response(conn, 200) === %{ 103 | "data" => %{ 104 | "language" => %{ 105 | "id" => to_string(language.id), 106 | "name" => language.name, 107 | "slug" => language.slug, 108 | "score" => language.score, 109 | "totalStars" => language.total_stars, 110 | "totalRepositories" => language.total_repositories, 111 | "totalDevelopers" => language.total_developers, 112 | "stats" => %{ 113 | "rank" => 1, 114 | "repositoriesCountRank" => 1 115 | } 116 | } 117 | } 118 | } 119 | end 120 | 121 | test "returns repositories for language", %{conn: conn, language: language} do 122 | conn = 123 | conn 124 | |> put_graphql_headers() 125 | |> post("/graphql", %{query: @with_repositories_query, variables: %{slug: "language-1"}}) 126 | 127 | assert json_response(conn, 200) === %{ 128 | "data" => %{ 129 | "language" => %{ 130 | "id" => to_string(language.id), 131 | "repositories" => [ 132 | %{"name" => "repository"} 133 | ] 134 | } 135 | } 136 | } 137 | end 138 | 139 | test "returns location usage for language", %{conn: conn, language: language} do 140 | conn = 141 | conn 142 | |> put_graphql_headers() 143 | |> post("/graphql", %{query: @with_location_usage_query, variables: %{slug: "language-1"}}) 144 | 145 | assert json_response(conn, 200) === %{ 146 | "data" => %{ 147 | "language" => %{ 148 | "id" => to_string(language.id), 149 | "locationUsage" => [ 150 | %{"location" => %{"name" => "Location 1"}, "repositoriesCount" => 1} 151 | ] 152 | } 153 | } 154 | } 155 | end 156 | 157 | test "returns developer usage for language", %{conn: conn, language: language} do 158 | conn = 159 | conn 160 | |> put_graphql_headers() 161 | |> post("/graphql", %{query: @with_developer_usage_query, variables: %{slug: "language-1"}}) 162 | 163 | assert json_response(conn, 200) === %{ 164 | "data" => %{ 165 | "language" => %{ 166 | "id" => to_string(language.id), 167 | "developerUsage" => [ 168 | %{"developer" => %{"name" => "Developer 1"}, "repositoriesCount" => 1} 169 | ] 170 | } 171 | } 172 | } 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/languages_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.LanguagesTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query { 13 | languages(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 14 | id 15 | name 16 | slug 17 | score 18 | totalStars 19 | totalRepositories 20 | totalDevelopers 21 | stats { 22 | rank 23 | repositoriesCountRank 24 | developersCountRank 25 | } 26 | } 27 | } 28 | """ 29 | 30 | @with_repositories_query """ 31 | query { 32 | languages(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 33 | name 34 | repositories(limit: 2, offset: 0, orderBy: {direction: ASC, field: NAME}) { 35 | name 36 | } 37 | } 38 | } 39 | """ 40 | 41 | @with_location_usage_query """ 42 | query { 43 | languages(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 44 | name 45 | locationUsage(limit: 2, offset: 0) { 46 | location { 47 | name 48 | } 49 | 50 | repositoriesCount 51 | } 52 | } 53 | } 54 | """ 55 | 56 | @with_developer_usage_query """ 57 | query { 58 | languages(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 59 | name 60 | developerUsage(limit: 2, offset: 0) { 61 | developer { 62 | name 63 | } 64 | 65 | repositoriesCount 66 | } 67 | } 68 | } 69 | """ 70 | 71 | setup do 72 | language1 = 73 | LanguagesHelper.create_language(%{ 74 | name: "Language 1", 75 | slug: "language-1", 76 | score: 1.0, 77 | total_repositories: 0, 78 | total_developers: 0 79 | }) 80 | 81 | language2 = 82 | LanguagesHelper.create_language(%{ 83 | name: "Language 2", 84 | slug: "language-2", 85 | score: 2.0, 86 | total_repositories: 1, 87 | total_developers: 1 88 | }) 89 | 90 | language3 = 91 | LanguagesHelper.create_language(%{ 92 | name: "Language 3", 93 | slug: "language-3", 94 | score: 3.0, 95 | total_repositories: 2, 96 | total_developers: 1 97 | }) 98 | 99 | location = LocationsHelper.create_location(%{name: "Location 1"}) 100 | 101 | developer = 102 | DevelopersHelper.create_developer(%{location_id: location.id, name: "Developer 1"}) 103 | 104 | RepositoriesHelper.create_repository(%{ 105 | name: "Repository 1", 106 | slug: "repository-1", 107 | developer_id: developer.id, 108 | language_id: language3.id 109 | }) 110 | 111 | RepositoriesHelper.create_repository(%{ 112 | name: "Repository 2", 113 | slug: "repository-2", 114 | developer_id: developer.id, 115 | language_id: language3.id 116 | }) 117 | 118 | {:ok, %{languages: {language1, language2, language3}}} 119 | end 120 | 121 | test "returns basic data", %{conn: conn, languages: {_, language2, language3}} do 122 | conn = 123 | conn 124 | |> put_graphql_headers() 125 | |> post("/graphql", @basic_query) 126 | 127 | assert json_response(conn, 200) === %{ 128 | "data" => %{ 129 | "languages" => [ 130 | %{ 131 | "id" => to_string(language3.id), 132 | "name" => language3.name, 133 | "slug" => language3.slug, 134 | "score" => language3.score, 135 | "totalStars" => language3.total_stars, 136 | "totalRepositories" => language3.total_repositories, 137 | "totalDevelopers" => language3.total_developers, 138 | "stats" => %{ 139 | "rank" => 1, 140 | "repositoriesCountRank" => 1, 141 | "developersCountRank" => 1 142 | } 143 | }, 144 | %{ 145 | "id" => to_string(language2.id), 146 | "name" => language2.name, 147 | "slug" => language2.slug, 148 | "score" => language2.score, 149 | "totalStars" => language2.total_stars, 150 | "totalRepositories" => language2.total_repositories, 151 | "totalDevelopers" => language2.total_developers, 152 | "stats" => %{ 153 | "rank" => 2, 154 | "repositoriesCountRank" => 2, 155 | "developersCountRank" => 1 156 | } 157 | } 158 | ] 159 | } 160 | } 161 | end 162 | 163 | test "returns repositories of language", %{conn: conn} do 164 | conn = 165 | conn 166 | |> put_graphql_headers() 167 | |> post("/graphql", @with_repositories_query) 168 | 169 | assert json_response(conn, 200) === %{ 170 | "data" => %{ 171 | "languages" => [ 172 | %{ 173 | "name" => "Language 3", 174 | "repositories" => [ 175 | %{"name" => "Repository 1"}, 176 | %{"name" => "Repository 2"} 177 | ] 178 | }, 179 | %{ 180 | "name" => "Language 2", 181 | "repositories" => [] 182 | } 183 | ] 184 | } 185 | } 186 | end 187 | 188 | test "returns location usage of language", %{conn: conn} do 189 | conn = 190 | conn 191 | |> put_graphql_headers() 192 | |> post("/graphql", @with_location_usage_query) 193 | 194 | assert json_response(conn, 200) === %{ 195 | "data" => %{ 196 | "languages" => [ 197 | %{ 198 | "name" => "Language 3", 199 | "locationUsage" => [ 200 | %{"location" => %{"name" => "Location 1"}, "repositoriesCount" => 2} 201 | ] 202 | }, 203 | %{ 204 | "name" => "Language 2", 205 | "locationUsage" => [] 206 | } 207 | ] 208 | } 209 | } 210 | end 211 | 212 | test "returns developer usage of language", %{conn: conn} do 213 | conn = 214 | conn 215 | |> put_graphql_headers() 216 | |> post("/graphql", @with_developer_usage_query) 217 | 218 | assert json_response(conn, 200) === %{ 219 | "data" => %{ 220 | "languages" => [ 221 | %{ 222 | "name" => "Language 3", 223 | "developerUsage" => [ 224 | %{"developer" => %{"name" => "Developer 1"}, "repositoriesCount" => 2} 225 | ] 226 | }, 227 | %{ 228 | "name" => "Language 2", 229 | "developerUsage" => [] 230 | } 231 | ] 232 | } 233 | } 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/location_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.LocationTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query($slug: String!) { 13 | location(slug: $slug) { 14 | id 15 | name 16 | slug 17 | score 18 | totalRepositories 19 | totalDevelopers 20 | stats { 21 | rank 22 | } 23 | } 24 | } 25 | """ 26 | 27 | @with_developers_query """ 28 | query($slug: String!) { 29 | location(slug: $slug) { 30 | id 31 | developers(limit: 10, offset: 0, orderBy: {direction: DESC, field: NAME}) { 32 | username 33 | } 34 | } 35 | } 36 | """ 37 | 38 | @with_repositories_query """ 39 | query($slug: String!) { 40 | location(slug: $slug) { 41 | id 42 | repositories(limit: 10, offset: 0, orderBy: {direction: DESC, field: NAME}) { 43 | name 44 | } 45 | } 46 | } 47 | """ 48 | 49 | @with_language_usage_query """ 50 | query($slug: String!) { 51 | location(slug: $slug) { 52 | id 53 | languageUsage(limit: 10, offset: 0) { 54 | language { 55 | name 56 | } 57 | 58 | repositoriesCount 59 | } 60 | } 61 | } 62 | """ 63 | 64 | setup do 65 | location = 66 | LocationsHelper.create_location(%{ 67 | name: "Location 1", 68 | slug: "location-1", 69 | score: 1.0, 70 | totalRepositories: 1, 71 | totalDevelopers: 1 72 | }) 73 | 74 | language = LanguagesHelper.create_language(%{name: "language"}) 75 | 76 | developer = 77 | DevelopersHelper.create_developer(%{location_id: location.id, username: "username"}) 78 | 79 | RepositoriesHelper.create_repository(%{ 80 | name: "repository", 81 | language_id: language.id, 82 | developer_id: developer.id 83 | }) 84 | 85 | {:ok, %{location: location}} 86 | end 87 | 88 | test "returns basic data", %{conn: conn, location: location} do 89 | conn = 90 | conn 91 | |> put_graphql_headers() 92 | |> post("/graphql", %{query: @basic_query, variables: %{slug: "location-1"}}) 93 | 94 | assert json_response(conn, 200) === %{ 95 | "data" => %{ 96 | "location" => %{ 97 | "id" => to_string(location.id), 98 | "name" => location.name, 99 | "slug" => location.slug, 100 | "score" => location.score, 101 | "totalRepositories" => location.total_repositories, 102 | "totalDevelopers" => location.total_developers, 103 | "stats" => %{ 104 | "rank" => 1 105 | } 106 | } 107 | } 108 | } 109 | end 110 | 111 | test "returns developers for location", %{conn: conn, location: location} do 112 | conn = 113 | conn 114 | |> put_graphql_headers() 115 | |> post("/graphql", %{query: @with_developers_query, variables: %{slug: "location-1"}}) 116 | 117 | assert json_response(conn, 200) === %{ 118 | "data" => %{ 119 | "location" => %{ 120 | "id" => to_string(location.id), 121 | "developers" => [ 122 | %{"username" => "username"} 123 | ] 124 | } 125 | } 126 | } 127 | end 128 | 129 | test "returns repositories for location", %{conn: conn, location: location} do 130 | conn = 131 | conn 132 | |> put_graphql_headers() 133 | |> post("/graphql", %{query: @with_repositories_query, variables: %{slug: "location-1"}}) 134 | 135 | assert json_response(conn, 200) === %{ 136 | "data" => %{ 137 | "location" => %{ 138 | "id" => to_string(location.id), 139 | "repositories" => [ 140 | %{"name" => "repository"} 141 | ] 142 | } 143 | } 144 | } 145 | end 146 | 147 | test "returns language usage for location", %{conn: conn, location: location} do 148 | conn = 149 | conn 150 | |> put_graphql_headers() 151 | |> post("/graphql", %{query: @with_language_usage_query, variables: %{slug: "location-1"}}) 152 | 153 | assert json_response(conn, 200) === %{ 154 | "data" => %{ 155 | "location" => %{ 156 | "id" => to_string(location.id), 157 | "languageUsage" => [ 158 | %{"language" => %{"name" => "language"}, "repositoriesCount" => 1} 159 | ] 160 | } 161 | } 162 | } 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/locations_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.LocationsTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query { 13 | locations(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 14 | id 15 | name 16 | slug 17 | score 18 | totalRepositories 19 | totalDevelopers 20 | stats { 21 | rank 22 | } 23 | } 24 | } 25 | """ 26 | 27 | @with_developers_query """ 28 | query { 29 | locations(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 30 | id 31 | developers(limit: 1, offset: 0, orderBy:{direction: DESC, field: NAME}) { 32 | username 33 | } 34 | } 35 | } 36 | """ 37 | 38 | @with_repositories_query """ 39 | query { 40 | locations(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 41 | id 42 | repositories(limit: 1, offset: 0, orderBy:{direction: DESC, field: NAME}) { 43 | slug 44 | } 45 | } 46 | } 47 | """ 48 | 49 | @with_language_usage_query """ 50 | query { 51 | locations(limit: 2, offset: 0, orderBy:{direction: DESC, field: SCORE}) { 52 | id 53 | languageUsage(limit: 1, offset: 0) { 54 | language { 55 | slug 56 | } 57 | repositoriesCount 58 | } 59 | } 60 | } 61 | """ 62 | 63 | setup do 64 | location1 = 65 | LocationsHelper.create_location(%{ 66 | name: "Location 1", 67 | slug: "location-1", 68 | score: 1.0, 69 | totalRepositories: 1, 70 | totalDevelopers: 1 71 | }) 72 | 73 | location2 = 74 | LocationsHelper.create_location(%{ 75 | name: "Location 2", 76 | slug: "location-2", 77 | score: 2.0, 78 | totalRepositories: 2, 79 | totalDevelopers: 1 80 | }) 81 | 82 | location3 = 83 | LocationsHelper.create_location(%{ 84 | name: "Location 3", 85 | slug: "location-3", 86 | score: 3.0, 87 | totalRepositories: 3, 88 | totalDevelopers: 1 89 | }) 90 | 91 | language = LanguagesHelper.create_language(%{slug: "language-slug"}) 92 | 93 | developer1 = 94 | DevelopersHelper.create_developer(%{ 95 | location_id: location2.id, 96 | username: "username1" 97 | }) 98 | 99 | developer2 = 100 | DevelopersHelper.create_developer(%{ 101 | location_id: location3.id, 102 | username: "username2" 103 | }) 104 | 105 | RepositoriesHelper.create_repository(%{ 106 | language_id: language.id, 107 | developer_id: developer1.id, 108 | slug: "slug1" 109 | }) 110 | 111 | RepositoriesHelper.create_repository(%{ 112 | language_id: language.id, 113 | developer_id: developer2.id, 114 | slug: "slug2" 115 | }) 116 | 117 | {:ok, %{locations: {location1, location2, location3}, language: language}} 118 | end 119 | 120 | test "returns basic data", %{conn: conn, locations: {_, location2, location3}} do 121 | conn = 122 | conn 123 | |> put_graphql_headers() 124 | |> post("/graphql", @basic_query) 125 | 126 | assert json_response(conn, 200) === %{ 127 | "data" => %{ 128 | "locations" => [ 129 | %{ 130 | "id" => to_string(location3.id), 131 | "name" => location3.name, 132 | "slug" => location3.slug, 133 | "score" => location3.score, 134 | "totalRepositories" => location3.total_repositories, 135 | "totalDevelopers" => location3.total_developers, 136 | "stats" => %{ 137 | "rank" => 1 138 | } 139 | }, 140 | %{ 141 | "id" => to_string(location2.id), 142 | "name" => location2.name, 143 | "slug" => location2.slug, 144 | "score" => location2.score, 145 | "totalRepositories" => location2.total_repositories, 146 | "totalDevelopers" => location2.total_developers, 147 | "stats" => %{ 148 | "rank" => 2 149 | } 150 | } 151 | ] 152 | } 153 | } 154 | end 155 | 156 | test "returns developers in this location", %{conn: conn, locations: {_, location2, location3}} do 157 | conn = 158 | conn 159 | |> put_graphql_headers() 160 | |> post("/graphql", @with_developers_query) 161 | 162 | assert json_response(conn, 200) === %{ 163 | "data" => %{ 164 | "locations" => [ 165 | %{ 166 | "id" => to_string(location3.id), 167 | "developers" => [ 168 | %{"username" => "username2"} 169 | ] 170 | }, 171 | %{ 172 | "id" => to_string(location2.id), 173 | "developers" => [ 174 | %{"username" => "username1"} 175 | ] 176 | } 177 | ] 178 | } 179 | } 180 | end 181 | 182 | test "returns repositories in this location", %{ 183 | conn: conn, 184 | locations: {_, location2, location3} 185 | } do 186 | conn = 187 | conn 188 | |> put_graphql_headers() 189 | |> post("/graphql", @with_repositories_query) 190 | 191 | assert json_response(conn, 200) === %{ 192 | "data" => %{ 193 | "locations" => [ 194 | %{ 195 | "id" => to_string(location3.id), 196 | "repositories" => [ 197 | %{"slug" => "slug2"} 198 | ] 199 | }, 200 | %{ 201 | "id" => to_string(location2.id), 202 | "repositories" => [ 203 | %{"slug" => "slug1"} 204 | ] 205 | } 206 | ] 207 | } 208 | } 209 | end 210 | 211 | test "returns language usage in this location", %{ 212 | conn: conn, 213 | locations: {_, location2, location3}, 214 | language: language 215 | } do 216 | conn = 217 | conn 218 | |> put_graphql_headers() 219 | |> post("/graphql", @with_language_usage_query) 220 | 221 | assert json_response(conn, 200) === %{ 222 | "data" => %{ 223 | "locations" => [ 224 | %{ 225 | "id" => to_string(location3.id), 226 | "languageUsage" => [ 227 | %{"language" => %{"slug" => language.slug}, "repositoriesCount" => 1} 228 | ] 229 | }, 230 | %{ 231 | "id" => to_string(location2.id), 232 | "languageUsage" => [ 233 | %{"language" => %{"slug" => language.slug}, "repositoriesCount" => 1} 234 | ] 235 | } 236 | ] 237 | } 238 | } 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/repositories_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.RepositoriesTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query { 13 | repositories(limit: 2, offset: 0, orderBy:{direction: DESC, field: STARS}) { 14 | id 15 | name 16 | slug 17 | description 18 | github_id 19 | github_url 20 | stars 21 | forks 22 | github_created_at 23 | stats { 24 | rank 25 | languageRank 26 | } 27 | } 28 | } 29 | """ 30 | 31 | @with_developer_query """ 32 | query { 33 | repositories(limit: 2, offset: 0, orderBy:{direction: DESC, field: STARS}) { 34 | name 35 | developer { 36 | name 37 | } 38 | } 39 | } 40 | """ 41 | 42 | @with_language_query """ 43 | query { 44 | repositories(limit: 2, offset: 0, orderBy:{direction: DESC, field: STARS}) { 45 | name 46 | language { 47 | name 48 | } 49 | } 50 | } 51 | """ 52 | 53 | setup do 54 | language = LanguagesHelper.create_language(%{name: "Language 1"}) 55 | location = LocationsHelper.create_location(%{name: "Location 1"}) 56 | 57 | developer = 58 | DevelopersHelper.create_developer(%{location_id: location.id, name: "Developer 1"}) 59 | 60 | repository1 = 61 | RepositoriesHelper.create_repository(%{ 62 | name: "Repository 1", 63 | slug: "repository-1", 64 | description: "Lorem ipsum", 65 | developer_id: developer.id, 66 | language_id: language.id, 67 | stars: 1 68 | }) 69 | 70 | repository2 = 71 | RepositoriesHelper.create_repository(%{ 72 | name: "Repository 2", 73 | slug: "repository-2", 74 | description: "Lorem ipsum", 75 | developer_id: developer.id, 76 | language_id: language.id, 77 | stars: 2 78 | }) 79 | 80 | repository3 = 81 | RepositoriesHelper.create_repository(%{ 82 | name: "Repository 3", 83 | slug: "repository-3", 84 | description: "Lorem ipsum", 85 | developer_id: developer.id, 86 | language_id: language.id, 87 | stars: 3 88 | }) 89 | 90 | {:ok, %{repositories: {repository1, repository2, repository3}}} 91 | end 92 | 93 | test "returns basic data", %{conn: conn, repositories: {_, repository2, repository3}} do 94 | conn = 95 | conn 96 | |> put_graphql_headers() 97 | |> post("/graphql", @basic_query) 98 | 99 | assert json_response(conn, 200) === %{ 100 | "data" => %{ 101 | "repositories" => [ 102 | %{ 103 | "id" => to_string(repository3.id), 104 | "name" => repository3.name, 105 | "slug" => repository3.slug, 106 | "description" => repository3.description, 107 | "github_id" => repository3.github_id, 108 | "github_url" => repository3.github_url, 109 | "stars" => repository3.stars, 110 | "forks" => repository3.forks, 111 | "github_created_at" => DateTime.to_iso8601(repository3.github_created_at), 112 | "stats" => %{ 113 | "rank" => 1, 114 | "languageRank" => 1 115 | } 116 | }, 117 | %{ 118 | "id" => to_string(repository2.id), 119 | "name" => repository2.name, 120 | "slug" => repository2.slug, 121 | "description" => repository2.description, 122 | "github_id" => repository2.github_id, 123 | "github_url" => repository2.github_url, 124 | "stars" => repository2.stars, 125 | "forks" => repository2.forks, 126 | "github_created_at" => DateTime.to_iso8601(repository2.github_created_at), 127 | "stats" => %{ 128 | "rank" => 2, 129 | "languageRank" => 2 130 | } 131 | } 132 | ] 133 | } 134 | } 135 | end 136 | 137 | test "returns developer of repository", %{conn: conn} do 138 | conn = 139 | conn 140 | |> put_graphql_headers() 141 | |> post("/graphql", @with_developer_query) 142 | 143 | assert json_response(conn, 200) === %{ 144 | "data" => %{ 145 | "repositories" => [ 146 | %{ 147 | "name" => "Repository 3", 148 | "developer" => %{"name" => "Developer 1"} 149 | }, 150 | %{ 151 | "name" => "Repository 2", 152 | "developer" => %{"name" => "Developer 1"} 153 | } 154 | ] 155 | } 156 | } 157 | end 158 | 159 | test "returns language of repository", %{conn: conn} do 160 | conn = 161 | conn 162 | |> put_graphql_headers() 163 | |> post("/graphql", @with_language_query) 164 | 165 | assert json_response(conn, 200) === %{ 166 | "data" => %{ 167 | "repositories" => [ 168 | %{ 169 | "name" => "Repository 3", 170 | "language" => %{"name" => "Language 1"} 171 | }, 172 | %{ 173 | "name" => "Repository 2", 174 | "language" => %{"name" => "Language 1"} 175 | } 176 | ] 177 | } 178 | } 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/repository_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.RepositoryTest do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @basic_query """ 12 | query($slug: String!) { 13 | repository(slug: $slug) { 14 | id 15 | name 16 | slug 17 | description 18 | github_id 19 | github_url 20 | stars 21 | forks 22 | github_created_at 23 | stats { 24 | rank 25 | languageRank 26 | } 27 | } 28 | } 29 | """ 30 | 31 | @with_developer_query """ 32 | query($slug: String!) { 33 | repository(slug: $slug) { 34 | id 35 | developer { 36 | name 37 | } 38 | } 39 | } 40 | """ 41 | 42 | @with_language_query """ 43 | query($slug: String!) { 44 | repository(slug: $slug) { 45 | id 46 | language { 47 | name 48 | } 49 | } 50 | } 51 | """ 52 | 53 | setup do 54 | language = LanguagesHelper.create_language(%{name: "Language 1"}) 55 | location = LocationsHelper.create_location(%{name: "Location 1"}) 56 | 57 | developer = 58 | DevelopersHelper.create_developer(%{location_id: location.id, name: "Developer 1"}) 59 | 60 | repository = 61 | RepositoriesHelper.create_repository(%{ 62 | name: "repository", 63 | slug: "repository-1", 64 | description: "Lorem ipsum", 65 | language_id: language.id, 66 | developer_id: developer.id 67 | }) 68 | 69 | {:ok, %{repository: repository}} 70 | end 71 | 72 | test "returns basic data", %{conn: conn, repository: repository} do 73 | conn = 74 | conn 75 | |> put_graphql_headers() 76 | |> post("/graphql", %{query: @basic_query, variables: %{slug: "repository-1"}}) 77 | 78 | assert json_response(conn, 200) === %{ 79 | "data" => %{ 80 | "repository" => %{ 81 | "id" => to_string(repository.id), 82 | "name" => repository.name, 83 | "slug" => repository.slug, 84 | "description" => repository.description, 85 | "github_id" => repository.github_id, 86 | "github_url" => repository.github_url, 87 | "stars" => repository.stars, 88 | "forks" => repository.forks, 89 | "github_created_at" => DateTime.to_iso8601(repository.github_created_at), 90 | "stats" => %{ 91 | "rank" => 1, 92 | "languageRank" => 1 93 | } 94 | } 95 | } 96 | } 97 | end 98 | 99 | test "returns developer of repository", %{conn: conn, repository: repository} do 100 | conn = 101 | conn 102 | |> put_graphql_headers() 103 | |> post("/graphql", %{query: @with_developer_query, variables: %{slug: "repository-1"}}) 104 | 105 | assert json_response(conn, 200) === %{ 106 | "data" => %{ 107 | "repository" => %{ 108 | "id" => to_string(repository.id), 109 | "developer" => %{"name" => "Developer 1"} 110 | } 111 | } 112 | } 113 | end 114 | 115 | test "returns language of repository", %{conn: conn, repository: repository} do 116 | conn = 117 | conn 118 | |> put_graphql_headers() 119 | |> post("/graphql", %{query: @with_language_query, variables: %{slug: "repository-1"}}) 120 | 121 | assert json_response(conn, 200) === %{ 122 | "data" => %{ 123 | "repository" => %{ 124 | "id" => to_string(repository.id), 125 | "language" => %{"name" => "Language 1"} 126 | } 127 | } 128 | } 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /test/githubist_web/graphql/queries/turkey_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.GraphQL.TurkeyTests do 2 | use GithubistWeb.ConnCase 3 | 4 | import GithubistWeb.TestSupport.GraphQLHelper 5 | 6 | alias Githubist.TestSupport.DevelopersHelper 7 | alias Githubist.TestSupport.LanguagesHelper 8 | alias Githubist.TestSupport.LocationsHelper 9 | alias Githubist.TestSupport.RepositoriesHelper 10 | 11 | @query """ 12 | query { 13 | turkey { 14 | totalDevelopers 15 | totalLanguages 16 | totalLocations 17 | totalRepositories 18 | } 19 | } 20 | """ 21 | 22 | setup do 23 | location = LocationsHelper.create_location() 24 | language = LanguagesHelper.create_language() 25 | developer = DevelopersHelper.create_developer(%{location_id: location.id}) 26 | RepositoriesHelper.create_repository(%{developer_id: developer.id, language_id: language.id}) 27 | 28 | :ok 29 | end 30 | 31 | test "runs correctly", %{conn: conn} do 32 | conn = 33 | conn 34 | |> put_graphql_headers() 35 | |> post("/graphql", @query) 36 | 37 | assert json_response(conn, 200) === %{ 38 | "data" => %{ 39 | "turkey" => %{ 40 | "totalDevelopers" => 1, 41 | "totalLanguages" => 1, 42 | "totalLocations" => 1, 43 | "totalRepositories" => 1 44 | } 45 | } 46 | } 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/githubist_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.ErrorViewTest do 2 | use GithubistWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.json" do 8 | assert render(GithubistWeb.ErrorView, "404.json", []) == 9 | %{errors: %{detail: "Not Found"}} 10 | end 11 | 12 | test "renders 500.json" do 13 | assert render(GithubistWeb.ErrorView, "500.json", []) == 14 | %{errors: %{detail: "Internal Server Error"}} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | alias Ecto.Adapters.SQL.Sandbox 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | use Phoenix.ChannelTest 24 | 25 | # The default endpoint for testing 26 | @endpoint GithubistWeb.Endpoint 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Sandbox.checkout(Githubist.Repo) 32 | 33 | unless tags[:async] do 34 | Sandbox.mode(Githubist.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | alias Ecto.Adapters.SQL.Sandbox 19 | alias Phoenix.ConnTest 20 | 21 | using do 22 | quote do 23 | # Import conveniences for testing with connections 24 | use Phoenix.ConnTest 25 | import GithubistWeb.Router.Helpers 26 | 27 | # The default endpoint for testing 28 | @endpoint GithubistWeb.Endpoint 29 | end 30 | end 31 | 32 | setup tags do 33 | :ok = Sandbox.checkout(Githubist.Repo) 34 | 35 | unless tags[:async] do 36 | Sandbox.mode(Githubist.Repo, {:shared, self()}) 37 | end 38 | 39 | {:ok, conn: ConnTest.build_conn()} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | alias Ecto.Adapters.SQL.Sandbox 18 | alias Ecto.Changeset 19 | 20 | using do 21 | quote do 22 | alias Githubist.Repo 23 | 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | import Githubist.DataCase 28 | end 29 | end 30 | 31 | setup tags do 32 | :ok = Sandbox.checkout(Githubist.Repo) 33 | 34 | unless tags[:async] do 35 | Sandbox.mode(Githubist.Repo, {:shared, self()}) 36 | end 37 | 38 | :ok 39 | end 40 | 41 | @doc """ 42 | A helper that transform changeset errors to a map of messages. 43 | 44 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 45 | assert "password is too short" in errors_on(changeset).password 46 | assert %{password: ["password is too short"]} = errors_on(changeset) 47 | 48 | """ 49 | def errors_on(changeset) do 50 | Changeset.traverse_errors(changeset, fn {message, opts} -> 51 | Enum.reduce(opts, message, fn {key, value}, acc -> 52 | String.replace(acc, "%{#{key}}", to_string(value)) 53 | end) 54 | end) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/support/developers_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.TestSupport.DevelopersHelper do 2 | @moduledoc """ 3 | Test helper for developers related logic 4 | """ 5 | 6 | alias Githubist.Developers 7 | alias Githubist.Developers.Developer 8 | 9 | @developer_attrs %{ 10 | username: "alpcanaydin", 11 | github_id: 123, 12 | name: "Alpcan Aydin", 13 | avatar_url: "https://example.com/avatar.jpg", 14 | bio: "Developer at Atolye15", 15 | company: "Atolye15", 16 | github_location: "Izmir, Turkey", 17 | github_url: "https://github.com/alpcanaydin", 18 | followers: 100, 19 | following: 100, 20 | public_repos: 50, 21 | total_starred: 500, 22 | score: 600.0, 23 | github_created_at: DateTime.utc_now() 24 | } 25 | 26 | @spec create_developer(map()) :: Developer.t() 27 | def create_developer(attrs \\ %{}) do 28 | merged_attrs = 29 | @developer_attrs 30 | |> Map.merge(attrs) 31 | 32 | {:ok, developer} = Developers.create_developer(merged_attrs) 33 | 34 | developer 35 | end 36 | 37 | @spec get_developer_attrs() :: map() 38 | def get_developer_attrs do 39 | @developer_attrs 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/graphql_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule GithubistWeb.TestSupport.GraphQLHelper do 2 | @moduledoc """ 3 | Test helpers for GraphQL test. 4 | """ 5 | 6 | import Plug.Conn 7 | 8 | def put_graphql_headers(conn) do 9 | conn 10 | |> put_req_header("content-type", "application/graphql") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/support/languages_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.TestSupport.LanguagesHelper do 2 | @moduledoc """ 3 | Test helper for language related logic 4 | """ 5 | 6 | alias Githubist.Languages 7 | alias Githubist.Languages.Language 8 | 9 | @language_attrs %{ 10 | name: "Elixir", 11 | slug: "elixir", 12 | score: 100.0, 13 | total_stars: 100, 14 | total_repositories: 100, 15 | total_developers: 100 16 | } 17 | 18 | @spec create_language(map()) :: Language.t() 19 | def create_language(attrs \\ %{}) do 20 | {:ok, language} = Languages.create_language(Map.merge(@language_attrs, attrs)) 21 | 22 | language 23 | end 24 | 25 | @spec get_language_attrs() :: map() 26 | def get_language_attrs do 27 | @language_attrs 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/locations_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.TestSupport.LocationsHelper do 2 | @moduledoc """ 3 | Test helper for location related logic 4 | """ 5 | 6 | alias Githubist.Locations 7 | alias Githubist.Locations.Location 8 | 9 | @location_attrs %{ 10 | name: "İzmir", 11 | slug: "izmir", 12 | score: 100.12, 13 | total_repositories: 100, 14 | total_developers: 100 15 | } 16 | 17 | @spec create_location(map()) :: Location.t() 18 | def create_location(attrs \\ %{}) do 19 | {:ok, location} = Locations.create_location(Map.merge(@location_attrs, attrs)) 20 | 21 | location 22 | end 23 | 24 | @spec get_location_attrs() :: map() 25 | def get_location_attrs do 26 | @location_attrs 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/support/repositories_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Githubist.TestSupport.RepositoriesHelper do 2 | @moduledoc """ 3 | Test helper for location related logic 4 | """ 5 | 6 | alias Githubist.Repositories 7 | alias Githubist.Repositories.Repository 8 | 9 | @repository_attrs %{ 10 | name: "repo", 11 | slug: "username/repo", 12 | description: "Lorem ipsum dolar sit amet.", 13 | github_id: 123, 14 | github_url: "https://github.com/username/repo", 15 | stars: 100, 16 | forks: 100, 17 | github_created_at: DateTime.utc_now() 18 | } 19 | 20 | @spec create_repository(map()) :: Repository.t() 21 | def create_repository(attrs \\ %{}) do 22 | merged_attrs = 23 | @repository_attrs 24 | |> Map.merge(attrs) 25 | 26 | {:ok, repository} = Repositories.create_repository(merged_attrs) 27 | 28 | repository 29 | end 30 | 31 | @spec get_repository_attrs() :: map() 32 | def get_repository_attrs do 33 | @repository_attrs 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(Githubist.Repo, :manual) 4 | --------------------------------------------------------------------------------