├── .cargo └── config.toml ├── .ci └── docker-compose.yml ├── .clang-format ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── rfc.md └── workflows │ ├── coverage.yml │ ├── docs.yml │ ├── pre-commit.yaml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bin └── installcheck ├── docker-compose.yaml ├── dockerfiles ├── db │ ├── Dockerfile │ └── setup.sql └── graphiql │ └── index.html ├── docs ├── api.md ├── assets │ ├── demo_schema.graphql │ ├── demo_schema.sql │ ├── favicon.ico │ ├── quickstart_graphiql.png │ ├── supabase_add_schema.png │ ├── supabase_api_key.png │ ├── supabase_graphiql.png │ ├── supabase_graphiql_explore.png │ ├── supabase_graphiql_query_table.png │ ├── supabase_project_ref.png │ └── supabase_sql_editor.png ├── changelog.md ├── computed_fields.md ├── configuration.md ├── contributing.md ├── example_schema.md ├── functions.md ├── index.md ├── installation.md ├── quickstart.md ├── requirements_docs.txt ├── security.md ├── sql_interface.md ├── supabase.md ├── usage_with_apollo.md ├── usage_with_relay.md └── views.md ├── mkdocs.yaml ├── pg_graphql.control ├── sql ├── directives.sql ├── load_sql_config.sql ├── load_sql_context.sql ├── raise_exception.sql ├── resolve.sql └── schema_version.sql ├── src ├── bin │ └── pgrx_embed.rs ├── builder.rs ├── graphql.rs ├── gson.rs ├── lib.rs ├── merge.rs ├── omit.rs ├── parser_util.rs ├── resolve.rs ├── sql_types.rs └── transpile.rs └── test ├── expected ├── aggregate.out ├── aggregate_directive.out ├── aliases.out ├── allow_camel_names.out ├── array_args_and_return_types.out ├── bigint_is_string.out ├── builtin_directives.out ├── comment_directive.out ├── comment_directive_description.out ├── compound_filters.out ├── empty_mutations.out ├── enum_mappings.out ├── extend_type_with_function.out ├── extend_type_with_function_relation.out ├── extend_type_with_generated_column.out ├── filter_by_node_id.out ├── fragment_on_mutation.out ├── fragment_on_query.out ├── function_calls.out ├── function_calls_default_args.out ├── function_calls_unsupported.out ├── function_return_row_is_selectable.out ├── function_return_view_has_pkey.out ├── inflection_fields.out ├── inflection_function.out ├── inflection_relationships.out ├── inflection_types.out ├── issue_163.out ├── issue_170.out ├── issue_225.out ├── issue_237_field_merging.out ├── issue_237_field_merging_mismatched.out ├── issue_300.out ├── issue_306.out ├── issue_312.out ├── issue_334_ambiguous_function.out ├── issue_337.out ├── issue_339_function_return_json.out ├── issue_339_function_return_table.out ├── issue_353_too_many_fields.out ├── issue_370_citext_as_string.out ├── issue_373.out ├── issue_377_enum_search_path.out ├── issue_409_fkey_rls_nullability.out ├── issue_444.out ├── issue_463.out ├── issue_511.out ├── issue_533.out ├── issue_542_partial_unique.out ├── issue_551_too_many_fields.out ├── issue_557_1_to_1_nullability.out ├── issue_581_missing_desc_on_schema.out ├── issue_fragment_spread_cycles.out ├── json_is_stringified.out ├── max_page_size.out ├── max_rows_directive.out ├── multi_column_primary_key.out ├── multiple_mutations.out ├── multiple_queries.out ├── mutation_delete.out ├── mutation_delete_variable.out ├── mutation_insert.out ├── mutation_update.out ├── null_argument.out ├── omit_exotic_types.out ├── omit_weird_names.out ├── operation_name.out ├── override_enum_name.out ├── override_field_name.out ├── override_func_field_name.out ├── override_relationship_field_name.out ├── override_type_name.out ├── page_info.out ├── permissions_connection_column.out ├── permissions_functions.out ├── permissions_node_column.out ├── permissions_table_level.out ├── permissions_types.out ├── primary_key_is_required.out ├── relationship_one_to_one.out ├── resolve___schema.out ├── resolve___type.out ├── resolve___typename.out ├── resolve_array_type.out ├── resolve_connection_filter.out ├── resolve_connection_named.out ├── resolve_connection_order_by.out ├── resolve_connection_pagination_args.out ├── resolve_connection_to_conn.out ├── resolve_connection_to_node.out ├── resolve_error_connection_edge_no_field.out ├── resolve_error_connection_edge_node_no_field.out ├── resolve_error_connection_no_field.out ├── resolve_error_from_parser.out ├── resolve_error_mutation_no_field.out ├── resolve_error_node_no_field.out ├── resolve_error_query_no_field.out ├── resolve_fragment.out ├── resolve_graphiql_schema.out ├── resolve_one.out ├── roundtrip_types.out ├── row_level_security.out ├── sqli_connection.out ├── string_filters.out ├── test_error_anon_and_named_operations.out ├── test_error_invalid_offset.out ├── test_error_multiple_anon_operations.out ├── test_error_mutation_transpilation.out ├── test_error_operation_name_not_found.out ├── test_error_operation_names_not_unique.out ├── test_error_query_transpilation.out ├── test_error_subscription.out ├── test_query__type.out ├── total_count.out ├── type_bigfloat.out ├── type_modifier_max_length.out ├── type_opaque.out ├── variable_default.out └── views_integration.out ├── fixtures.sql └── sql ├── aggregate.sql ├── aggregate_directive.sql ├── aliases.sql ├── allow_camel_names.sql ├── array_args_and_return_types.sql ├── bigint_is_string.sql ├── builtin_directives.sql ├── comment_directive.sql ├── comment_directive_description.sql ├── compound_filters.sql ├── empty_mutations.sql ├── enum_mappings.sql ├── extend_type_with_function.sql ├── extend_type_with_function_relation.sql ├── extend_type_with_generated_column.sql ├── filter_by_node_id.sql ├── fragment_on_mutation.sql ├── fragment_on_query.sql ├── function_calls.sql ├── function_calls_default_args.sql ├── function_calls_unsupported.sql ├── function_return_row_is_selectable.sql ├── function_return_view_has_pkey.sql ├── inflection_fields.sql ├── inflection_function.sql ├── inflection_relationships.sql ├── inflection_types.sql ├── issue_163.sql ├── issue_170.sql ├── issue_225.sql ├── issue_237_field_merging.sql ├── issue_237_field_merging_mismatched.sql ├── issue_300.sql ├── issue_306.sql ├── issue_312.sql ├── issue_334_ambiguous_function.sql ├── issue_337.sql ├── issue_339_function_return_json.sql ├── issue_339_function_return_table.sql ├── issue_353_too_many_fields.sql ├── issue_370_citext_as_string.sql ├── issue_373.sql ├── issue_377_enum_search_path.sql ├── issue_409_fkey_rls_nullability.sql ├── issue_444.sql ├── issue_463.sql ├── issue_511.sql ├── issue_533.sql ├── issue_542_partial_unique.sql ├── issue_551_too_many_fields.sql ├── issue_557_1_to_1_nullability.sql ├── issue_581_missing_desc_on_schema.sql ├── issue_fragment_spread_cycles.sql ├── json_is_stringified.sql ├── max_page_size.sql ├── max_rows_directive.sql ├── multi_column_primary_key.sql ├── multiple_mutations.sql ├── multiple_queries.sql ├── mutation_delete.sql ├── mutation_delete_variable.sql ├── mutation_insert.sql ├── mutation_update.sql ├── null_argument.sql ├── omit_exotic_types.sql ├── omit_weird_names.sql ├── operation_name.sql ├── override_enum_name.sql ├── override_field_name.sql ├── override_func_field_name.sql ├── override_relationship_field_name.sql ├── override_type_name.sql ├── page_info.sql ├── permissions_connection_column.sql ├── permissions_functions.sql ├── permissions_node_column.sql ├── permissions_table_level.sql ├── permissions_types.sql ├── primary_key_is_required.sql ├── relationship_one_to_one.sql ├── resolve___schema.sql ├── resolve___type.sql ├── resolve___typename.sql ├── resolve_array_type.sql ├── resolve_connection_filter.sql ├── resolve_connection_named.sql ├── resolve_connection_order_by.sql ├── resolve_connection_pagination_args.sql ├── resolve_connection_to_conn.sql ├── resolve_connection_to_node.sql ├── resolve_error_connection_edge_no_field.sql ├── resolve_error_connection_edge_node_no_field.sql ├── resolve_error_connection_no_field.sql ├── resolve_error_from_parser.sql ├── resolve_error_mutation_no_field.sql ├── resolve_error_node_no_field.sql ├── resolve_error_query_no_field.sql ├── resolve_fragment.sql ├── resolve_graphiql_schema.sql ├── resolve_one.sql ├── roundtrip_types.sql ├── row_level_security.sql ├── sqli_connection.sql ├── string_filters.sql ├── test_error_anon_and_named_operations.sql ├── test_error_invalid_offset.sql ├── test_error_multiple_anon_operations.sql ├── test_error_mutation_transpilation.sql ├── test_error_operation_name_not_found.sql ├── test_error_operation_names_not_unique.sql ├── test_error_query_transpilation.sql ├── test_error_subscription.sql ├── test_query__type.sql ├── total_count.sql ├── type_bigfloat.sql ├── type_modifier_max_length.sql ├── type_opaque.sql ├── variable_default.sql └── views_integration.sql /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Postgres symbols won't be available until runtime 3 | rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"] 4 | -------------------------------------------------------------------------------- /.ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | test: 5 | container_name: pg_graphql_test 6 | build: 7 | context: .. 8 | dockerfile: ./dockerfiles/db/Dockerfile 9 | args: 10 | PG_VERSION: ${PG_VERSION:-15} 11 | command: 12 | - ./bin/installcheck 13 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 4 3 | 4 | BinPackArguments: false 5 | BinPackParameters: false 6 | 7 | ExperimentalAutoDetectBinPacking: false 8 | AllowAllParametersOfDeclarationOnNextLine: false 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .benchmarks 2 | .git/ 3 | .github/ 4 | .pytest_cache/ 5 | dockerfiles/ 6 | docs/ 7 | pg_graphql.egg-info/ 8 | target/ 9 | nix/ 10 | node_modules/ 11 | results/ 12 | venv/ 13 | *.md 14 | *.md 15 | *.py 16 | *.yaml 17 | *.yml 18 | *.graphql 19 | *.nix 20 | .python-version 21 | .gitignore 22 | .clang-format 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: triage-required 6 | assignees: olirice 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Versions:** 27 | - PostgreSQL: [e.g. 14.1] 28 | - pg_graphql commit ref: ea2ab60 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | 33 | **Security** 34 | If you beleive you have identified a security vulnerability in pg_graphql, please follow the instructions at [security.txt](https://supabase.com/.well-known/security.txt) and wait for a response before opening a GitHub issue. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rfc.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: RFC 3 | about: Request for Comment 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | [summary]: #summary 12 | 13 | Short explanation of the feature. 14 | 15 | # Rationale 16 | [rationale]: #rationale 17 | 18 | Why should we do this? 19 | 20 | # Design 21 | [design]: #design 22 | 23 | An dense explanation in sufficient detail that someone familiar with the 24 | project could implement the feature. Specifics and corner cases should be covered. 25 | 26 | # Examples 27 | [examples]: #examples 28 | 29 | Illustrations and examples to clarify descriptions from previous sections. 30 | 31 | # Drawbacks 32 | [drawbacks]: #drawbacks 33 | 34 | What are the negative trade-offs? 35 | 36 | # Alternatives 37 | [alternatives]: #alternatives 38 | 39 | What other solutions have been considered? 40 | 41 | # Unresolved Questions 42 | [unresolved]: #unresolved-questions 43 | 44 | What parts of problem space or proposed designs are unknown or TBD? 45 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | code-coverage: 14 | name: Code Coverage 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - run: | 21 | # Add postgres package repo 22 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' 23 | wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &>/dev/null 24 | 25 | sudo apt-get update 26 | sudo apt-get install -y --no-install-recommends git build-essential libpq-dev curl libreadline6-dev zlib1g-dev pkg-config cmake 27 | sudo apt-get install -y --no-install-recommends libreadline-dev zlib1g-dev flex bison libxml2-dev libxslt-dev libssl-dev libxml2-utils xsltproc ccache 28 | sudo apt-get install -y --no-install-recommends clang libclang-dev gcc tree 29 | 30 | # Install requested postgres version 31 | sudo apt install -y postgresql-16 postgresql-server-dev-16 -y 32 | 33 | # Ensure installed pg_config is first on path 34 | export PATH=$PATH:/usr/lib/postgresql/16/bin 35 | 36 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable && \ 37 | rustup --version && \ 38 | rustc --version && \ 39 | cargo --version 40 | 41 | # Ensure cargo/rust on path 42 | source "$HOME/.cargo/env" 43 | 44 | rustup component add llvm-tools-preview 45 | cargo install cargo-llvm-cov 46 | cargo install cargo-pgrx --version 0.12.9 --locked 47 | cargo pgrx init --pg16=/usr/lib/postgresql/16/bin/pg_config 48 | 49 | sudo chmod a+rw /usr/share/postgresql/16/extension 50 | sudo chmod a+rw /usr/lib/postgresql/16/lib/ 51 | 52 | cargo pgrx install 53 | source <(cargo llvm-cov show-env --export-prefix) 54 | cargo llvm-cov clean --workspace 55 | cargo build 56 | cp ./target/debug/libpg_graphql.so /usr/lib/postgresql/16/lib/pg_graphql.so 57 | ./bin/installcheck 58 | cargo llvm-cov report --lcov --output-path lcov.info 59 | 60 | - name: Coveralls upload 61 | uses: coverallsapp/github-action@v2 62 | with: 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | path-to-lcov: lcov.info 65 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "docs/**" 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | deploy-docs: 14 | runs-on: ubuntu-latest 15 | env: 16 | DOCS_REVALIDATION_KEY: ${{ secrets.DOCS_REVALIDATION_KEY }} 17 | steps: 18 | - name: Request docs revalidation 19 | run: | 20 | curl -X POST https://supabase.com/docs/api/revalidate \ 21 | -H 'Content-Type: application/json' \ 22 | -H 'Authorization: Bearer ${{ secrets.DOCS_REVALIDATION_KEY }}' \ 23 | -d '{"tags": ["graphql"]}' 24 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | on: 3 | pull_request: 4 | push: { branches: [master] } 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: set up python 3.12 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3.12 21 | 22 | - name: install pre-commit 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install pre-commit 26 | 27 | 28 | - name: run pre-commit hooks 29 | run: | 30 | pre-commit run --all-files 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | pull_request: 4 | push: { branches: [master] } 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test: 11 | name: Run tests 12 | strategy: 13 | matrix: 14 | postgres: [14, 15, 16, 17] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Build docker images 21 | run: PG_VERSION=${{ matrix.postgres }} docker compose -f .ci/docker-compose.yml build 22 | 23 | - name: Run tests 24 | run: PG_VERSION=${{ matrix.postgres }} docker compose -f .ci/docker-compose.yml run test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | results/ 2 | debug/ 3 | __pycache__/ 4 | ex/ 5 | .python-version 6 | venv/ 7 | site/ 8 | regression.* 9 | .DS_Store 10 | *.egg-info/ 11 | *.json 12 | *.ipynb 13 | *.swp 14 | *.diff 15 | .ipynb_checkpoints/* 16 | node_modules/ 17 | docs/graphql_lexer/ 18 | perf.txt 19 | query.txt 20 | schema.graphql 21 | target/ 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/Lucas-C/pre-commit-hooks 4 | rev: v1.1.10 5 | hooks: 6 | - id: remove-tabs 7 | name: Tabs-to-Spaces 8 | exclude: ^test/expected 9 | 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.0.1 12 | hooks: 13 | - id: trailing-whitespace 14 | exclude: ^test/expected 15 | - id: end-of-file-fixer 16 | exclude: ^test/expected 17 | - id: check-yaml 18 | - id: check-merge-conflict 19 | - id: check-added-large-files 20 | args: ['--maxkb=500'] 21 | - id: mixed-line-ending 22 | args: ['--fix=lf'] 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pg_graphql" 3 | version = "1.5.11" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "lib"] 8 | 9 | [[bin]] 10 | name = "pgrx_embed_pg_graphql" 11 | path = "./src/bin/pgrx_embed.rs" 12 | 13 | [features] 14 | default = ["pg16"] 15 | pg14 = ["pgrx/pg14", "pgrx-tests/pg14"] 16 | pg15 = ["pgrx/pg15", "pgrx-tests/pg15"] 17 | pg16 = ["pgrx/pg16", "pgrx-tests/pg16"] 18 | pg17 = ["pgrx/pg17", "pgrx-tests/pg17"] 19 | pg_test = [] 20 | 21 | [dependencies] 22 | pgrx = "=0.12.9" 23 | graphql-parser = "0.4" 24 | serde = { version = "1.0", features = ["rc"] } 25 | serde_json = "1.0" 26 | itertools = "0.10.3" 27 | cached = { version = "0.46.0", default-features = false, features = [ 28 | "proc_macro", 29 | ] } 30 | rand = "0.8" 31 | uuid = "1" 32 | base64 = "0.13" 33 | lazy_static = "1" 34 | bimap = { version = "0.6.3", features = ["serde"] } 35 | indexmap = "2.2" 36 | 37 | [dev-dependencies] 38 | pgrx-tests = "=0.12.9" 39 | 40 | [profile.dev] 41 | panic = "unwind" 42 | lto = "thin" 43 | 44 | [profile.release] 45 | panic = "unwind" 46 | opt-level = 3 47 | lto = "fat" 48 | codegen-units = 1 49 | -------------------------------------------------------------------------------- /bin/installcheck: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | ######## 4 | # Vars # 5 | ######## 6 | TMPDIR="$(mktemp -d)" 7 | export PGDATA="$TMPDIR" 8 | export PGHOST="$TMPDIR" 9 | export PGUSER=postgres 10 | export PGDATABASE=postgres 11 | export PGTZ=UTC 12 | export PG_COLOR=auto 13 | 14 | # PATH=~/.pgrx/15.1/pgrx-install/bin/:$PATH 15 | 16 | #################### 17 | # Ensure Clean Env # 18 | #################### 19 | # Stop the server (if running) 20 | trap 'pg_ctl stop -m i' sigint sigterm exit 21 | # Remove temporary data dir 22 | rm -rf "$TMPDIR" 23 | 24 | ############## 25 | # Initialize # 26 | ############## 27 | # Initialize: setting PGUSER as the owner 28 | initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" 29 | # Start the server 30 | pg_ctl start -o "-F -c listen_addresses=\"\" -c log_min_messages=WARNING -k $PGDATA" 31 | # Create the test db 32 | createdb contrib_regression 33 | 34 | ######### 35 | # Tests # 36 | ######### 37 | TESTDIR="test" 38 | PGXS=$(dirname $(pg_config --pgxs)) 39 | REGRESS="${PGXS}/../test/regress/pg_regress" 40 | 41 | # Test names can be passed as parameters to this script. 42 | # If any test names are passed run only those tests. 43 | # Otherwise run all tests. 44 | if [ "$#" -ne 0 ]; then 45 | TESTS=$@ 46 | else 47 | TESTS=$(ls ${TESTDIR}/sql | sed -e 's/\..*$//' | sort) 48 | fi 49 | 50 | # Execute the test fixtures 51 | psql -v ON_ERROR_STOP=1 -f test/fixtures.sql -d contrib_regression 52 | 53 | # Run tests 54 | ${REGRESS} --use-existing --dbname=contrib_regression --inputdir=${TESTDIR} ${TESTS} 55 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | db: 5 | container_name: pg_db 6 | build: 7 | context: . 8 | dockerfile: ./dockerfiles/db/Dockerfile 9 | volumes: 10 | - ./dockerfiles/db/setup.sql:/docker-entrypoint-initdb.d/setup.sql 11 | ports: 12 | - 5406:5432 13 | command: 14 | - postgres 15 | - -c 16 | - wal_level=logical 17 | - -c 18 | - shared_preload_libraries=pg_stat_statements 19 | healthcheck: 20 | test: ["CMD-SHELL", "PGUSER=postgres", "pg_isready"] 21 | interval: 1s 22 | timeout: 10s 23 | retries: 5 24 | environment: 25 | POSTGRES_USER: postgres 26 | POSTGRES_PASSWORD: password 27 | POSTGRES_DB: graphqldb 28 | 29 | rest: 30 | container_name: pg_postgrest 31 | image: postgrest/postgrest:v10.0.0 32 | restart: unless-stopped 33 | ports: 34 | - 3001:3000 35 | environment: 36 | PGRST_DB_URI: postgres://postgres:password@db:5432/graphqldb 37 | PGRST_DB_SCHEMA: public 38 | PGRST_DB_ANON_ROLE: anon 39 | depends_on: 40 | - db 41 | 42 | graphiql: 43 | container_name: pg_graphiql 44 | image: nginx 45 | volumes: 46 | - ./dockerfiles/graphiql:/usr/share/nginx/html 47 | ports: 48 | - 4000:80 49 | depends_on: 50 | - rest 51 | -------------------------------------------------------------------------------- /dockerfiles/db/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PG_VERSION=15 2 | FROM postgres:${PG_VERSION} 3 | RUN apt-get update 4 | 5 | ENV build_deps ca-certificates \ 6 | git \ 7 | build-essential \ 8 | libpq-dev \ 9 | postgresql-server-dev-${PG_MAJOR} \ 10 | curl \ 11 | libreadline6-dev \ 12 | zlib1g-dev 13 | 14 | 15 | RUN apt-get install -y --no-install-recommends $build_deps pkg-config cmake 16 | 17 | WORKDIR /home/pg_graphql 18 | 19 | ENV HOME=/home/pg_graphql \ 20 | PATH=/home/pg_graphql/.cargo/bin:$PATH 21 | RUN chown postgres:postgres /home/pg_graphql 22 | USER postgres 23 | 24 | RUN \ 25 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable && \ 26 | rustup --version && \ 27 | rustc --version && \ 28 | cargo --version 29 | 30 | # PGRX 31 | RUN cargo install cargo-pgrx --version 0.12.9 --locked 32 | 33 | RUN cargo pgrx init --pg${PG_MAJOR} $(which pg_config) 34 | 35 | USER root 36 | 37 | COPY . . 38 | RUN cargo pgrx install 39 | 40 | USER postgres 41 | -------------------------------------------------------------------------------- /dockerfiles/graphiql/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GraphiQL - pg_graphql 5 | 6 | 7 | 8 | 9 | 10 |
11 | 15 | 19 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/assets/demo_schema.sql: -------------------------------------------------------------------------------- 1 | -- Turn on automatic inflection of type names 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | 4 | create table account( 5 | id serial primary key, 6 | email varchar(255) not null, 7 | created_at timestamp not null, 8 | updated_at timestamp not null 9 | ); 10 | 11 | -- enable a `totalCount` field on the `account` query type 12 | comment on table account is e'@graphql({"totalCount": {"enabled": true}})'; 13 | 14 | create table blog( 15 | id serial primary key, 16 | owner_id integer not null references account(id), 17 | name varchar(255) not null, 18 | description varchar(255), 19 | tags text[], 20 | created_at timestamp not null, 21 | updated_at timestamp not null 22 | ); 23 | 24 | create type blog_post_status as enum ('PENDING', 'RELEASED'); 25 | 26 | create table blog_post( 27 | id uuid not null default gen_random_uuid() primary key, 28 | blog_id integer not null references blog(id), 29 | title varchar(255) not null, 30 | body varchar(10000), 31 | status blog_post_status not null, 32 | created_at timestamp not null, 33 | updated_at timestamp not null 34 | ); 35 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/quickstart_graphiql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/quickstart_graphiql.png -------------------------------------------------------------------------------- /docs/assets/supabase_add_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_add_schema.png -------------------------------------------------------------------------------- /docs/assets/supabase_api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_api_key.png -------------------------------------------------------------------------------- /docs/assets/supabase_graphiql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_graphiql.png -------------------------------------------------------------------------------- /docs/assets/supabase_graphiql_explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_graphiql_explore.png -------------------------------------------------------------------------------- /docs/assets/supabase_graphiql_query_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_graphiql_query_table.png -------------------------------------------------------------------------------- /docs/assets/supabase_project_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_project_ref.png -------------------------------------------------------------------------------- /docs/assets/supabase_sql_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/pg_graphql/0aa8dfd0afde70d3de6c7f33b0e6732d95632998/docs/assets/supabase_sql_editor.png -------------------------------------------------------------------------------- /docs/example_schema.md: -------------------------------------------------------------------------------- 1 | The following is a complete example showing how a sample SQL schema translates into a GraphQL schema. 2 | 3 | ```sql 4 | --8<-- "docs/assets/demo_schema.sql" 5 | ``` 6 | 7 | ```graphql 8 | --8<-- "docs/assets/demo_schema.graphql" 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # `pg_graphql` 2 | 3 |

4 | PostgreSQL version 5 | License 6 | tests 7 | 8 |

9 | 10 | --- 11 | 12 | **Documentation**: https://supabase.github.io/pg_graphql 13 | 14 | **Source Code**: https://github.com/supabase/pg_graphql 15 | 16 | --- 17 | 18 | `pg_graphql` adds GraphQL support to your PostgreSQL database. 19 | 20 | - [x] __Performant__ 21 | - [x] __Consistent__ 22 | - [x] __Open Source__ 23 | 24 | ### Overview 25 | `pg_graphql` is a PostgreSQL extension that enables querying the database with GraphQL using a single SQL function. 26 | 27 | The extension reflects a GraphQL schema from the existing SQL schema and exposes it through a SQL function, `graphql.resolve(...)`. This enables any programming language that can connect to PostgreSQL to query the database via GraphQL with no additional servers, processes, or libraries. 28 | 29 | 30 | ### TL;DR 31 | 32 | The SQL schema 33 | 34 | ```sql 35 | create table account( 36 | id serial primary key, 37 | email varchar(255) not null, 38 | created_at timestamp not null, 39 | updated_at timestamp not null 40 | ); 41 | 42 | create table blog( 43 | id serial primary key, 44 | owner_id integer not null references account(id), 45 | name varchar(255) not null, 46 | description varchar(255), 47 | created_at timestamp not null, 48 | updated_at timestamp not null 49 | ); 50 | 51 | create type blog_post_status as enum ('PENDING', 'RELEASED'); 52 | 53 | create table blog_post( 54 | id uuid not null default uuid_generate_v4() primary key, 55 | blog_id integer not null references blog(id), 56 | title varchar(255) not null, 57 | body varchar(10000), 58 | status blog_post_status not null, 59 | created_at timestamp not null, 60 | updated_at timestamp not null 61 | ); 62 | ``` 63 | Translates into a GraphQL schema displayed below. 64 | 65 | Each table receives an entrypoint in the top level `Query` type that is a pageable collection with relationships defined by its foreign keys. Tables similarly receive entrypoints in the `Mutation` type that enable bulk operations for insert, update, and delete. 66 | 67 | ![GraphiQL](./assets/quickstart_graphiql.png) 68 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | First, install [pgrx](https://github.com/tcdi/pgrx) by running `cargo install --locked cargo-pgrx@version`, where version should be compatible with the [pgrx version used by pg_graphql](https://github.com/supabase/pg_graphql/blob/master/Cargo.toml#L16). 2 | 3 | Then clone the repo and install using: 4 | 5 | ```bash 6 | git clone https://github.com/supabase/pg_graphql.git 7 | cd pg_graphql 8 | cargo pgrx install --release 9 | ``` 10 | 11 | Before enabling the extension you need to initialize `pgrx`. The easiest way to get started is to allow `pgrx` to manage its own version/s of Postgres: 12 | 13 | ```bash 14 | cargo pgrx init --pg17=download 15 | ``` 16 | 17 | For more advanced configuration options, like building against an existing Postgres installation from e.g. Homebrew, see the [pgrx docs](https://github.com/pgcentralfoundation/pgrx) 18 | 19 | To start the database: 20 | 21 | ```bash 22 | cargo pgrx start pg17 23 | ``` 24 | 25 | To connect: 26 | 27 | ```bash 28 | cargo pgrx connect pg17 29 | ``` 30 | 31 | Finally, to enable the `pg_graphql` extension in Postgres, execute the `create extension` statement. This extension creates its own schema/namespace named `graphql` to avoid naming conflicts. 32 | 33 | ```psql 34 | create extension pg_graphql; 35 | ``` 36 | 37 | These steps ensure that `pgrx` is properly initialized, and the database is started and connected before attempting to install and use the `pg_graphql` extension. 38 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | If you are new to the project, start here. 2 | 3 | The easiest way to try `pg_graphql` is to run the interactive [GraphiQL IDE](https://github.com/graphql/graphiql) demo. The demo environment launches a database, webserver and the GraphiQL IDE/API explorer with a small pre-populated schema. 4 | 5 | Requires: 6 | 7 | - git 8 | - docker-compose 9 | 10 | First, clone the repo 11 | 12 | ```shell 13 | git clone https://github.com/supabase/pg_graphql.git 14 | cd pg_graphql 15 | ``` 16 | 17 | Next, launch the demo with docker-compose. 18 | 19 | ```shell 20 | docker-compose up 21 | ``` 22 | 23 | Finally, access GraphiQL at `http://localhost:4000/`. 24 | 25 | ![GraphiQL](./assets/quickstart_graphiql.png) 26 | -------------------------------------------------------------------------------- /docs/requirements_docs.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | git+https://github.com/ivome/pygments-graphql-lexer.git 4 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | `pg_graphql` fully respects builtin PostgreSQL role and row security. 2 | 3 | ## Table/Column Visibility 4 | 5 | Table and column visibility in the GraphQL schema are controlled by standard PostgreSQL role permissions. Revoking `SELECT` access from the user/role executing queries removes that entity from the visible schema. 6 | 7 | For example: 8 | ```sql 9 | revoke all privileges on public."Account" from api_user; 10 | ``` 11 | 12 | removes the `Account` GraphQL type. 13 | 14 | Similarly, revoking `SELECT` access on a table's column will remove that field from the associated GraphQL type/s. 15 | 16 | The permissions `SELECT`, `INSERT`, `UPDATE`, and `DELETE` all impact the relevant sections of the GraphQL schema. 17 | 18 | ## Row Visibility 19 | 20 | Visibility of rows in a given table can be configured using PostgreSQL's built-in [row level security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) policies. 21 | -------------------------------------------------------------------------------- /docs/sql_interface.md: -------------------------------------------------------------------------------- 1 | pg_graphql's public facing SQL interface consists of a single SQL function to resolve GraphQL requests. All other entities in the `graphql` schema are private. 2 | 3 | 4 | ### graphql.resolve 5 | 6 | ##### description 7 | Resolves a GraphQL query, returning JSONB. 8 | 9 | ##### signature 10 | ```sql 11 | graphql.resolve( 12 | -- graphql query/mutation 13 | query text, 14 | -- json key/values pairs for variables 15 | variables jsonb default '{}'::jsonb, 16 | -- the name of the graphql operation in *query* to execute 17 | "operationName" text default null, 18 | -- extensions to include in the request 19 | extensions jsonb default null, 20 | ) 21 | returns jsonb 22 | 23 | strict 24 | volatile 25 | parallel safe 26 | language plpgsql 27 | ``` 28 | 29 | ##### usage 30 | 31 | ```sql 32 | -- Create the extension 33 | graphqldb= create extension pg_graphql; 34 | CREATE EXTENSION 35 | 36 | -- Create an example table 37 | graphqldb= create table book(id int primary key, title text); 38 | CREATE TABLE 39 | 40 | -- Insert a record 41 | graphqldb= insert into book(id, title) values (1, 'book 1'); 42 | INSERT 0 1 43 | 44 | -- Query the table via GraphQL 45 | graphqldb= select graphql.resolve($$ 46 | query { 47 | bookCollection { 48 | edges { 49 | node { 50 | id 51 | } 52 | } 53 | } 54 | } 55 | $$); 56 | 57 | resolve 58 | ---------------------------------------------------------------------- 59 | {"data": {"bookCollection": {"edges": [{"node": {"id": 1}}]}}, "errors": []} 60 | ``` 61 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: pg_graphql 2 | site_url: https://supabase.github.io/pg_graphql 3 | site_description: A PostgreSQL extension adding GraphQL support 4 | 5 | repo_name: supabase/pg_graphql 6 | repo_url: https://github.com/supabase/pg_graphql 7 | 8 | nav: 9 | - Welcome: 'index.md' 10 | - Quickstart: 'quickstart.md' 11 | - SQL Interface: 'sql_interface.md' 12 | - API: 'api.md' 13 | - Views: 'views.md' 14 | - Functions: 'functions.md' 15 | - Computed Fields: 'computed_fields.md' 16 | - Security: 'security.md' 17 | - Configuration: 'configuration.md' 18 | - Guides: 19 | - Supabase: 'supabase.md' 20 | - Usage with Apollo: 'usage_with_apollo.md' 21 | - Usage with Relay: 'usage_with_relay.md' 22 | - Example Schema: 'example_schema.md' 23 | - Installation: 'installation.md' 24 | - Contributing: 'contributing.md' 25 | - Changelog: 'changelog.md' 26 | 27 | theme: 28 | name: 'material' 29 | features: 30 | - navigation.expand 31 | favicon: 'assets/favicon.ico' 32 | logo: 'assets/favicon.ico' 33 | homepage: https://supabase.github.io/pg_graphql 34 | palette: 35 | primary: black 36 | accent: light green 37 | 38 | markdown_extensions: 39 | - pymdownx.highlight: 40 | linenums: true 41 | guess_lang: false 42 | use_pygments: true 43 | pygments_style: default 44 | - pymdownx.superfences 45 | - pymdownx.tabbed: 46 | alternate_style: true 47 | - pymdownx.snippets 48 | - pymdownx.tasklist 49 | - admonition 50 | -------------------------------------------------------------------------------- /pg_graphql.control: -------------------------------------------------------------------------------- 1 | comment = 'pg_graphql: GraphQL support' 2 | default_version = '@CARGO_VERSION@' 3 | module_pathname = '$libdir/pg_graphql' 4 | relocatable = false 5 | superuser = true 6 | schema = 'graphql' 7 | -------------------------------------------------------------------------------- /sql/directives.sql: -------------------------------------------------------------------------------- 1 | create function graphql.comment_directive(comment_ text) 2 | returns jsonb 3 | language sql 4 | immutable 5 | as $$ 6 | /* 7 | comment on column public.account.name is '@graphql.name: myField' 8 | */ 9 | select 10 | coalesce( 11 | ( 12 | regexp_match( 13 | comment_, 14 | '@graphql\((.+)\)' 15 | ) 16 | )[1]::jsonb, 17 | jsonb_build_object() 18 | ) 19 | $$; 20 | -------------------------------------------------------------------------------- /sql/load_sql_config.sql: -------------------------------------------------------------------------------- 1 | select 2 | jsonb_build_object( 3 | 'search_path', current_schemas(false), 4 | 'role', current_role, 5 | 'schema_version', graphql.get_schema_version() 6 | ) 7 | -------------------------------------------------------------------------------- /sql/raise_exception.sql: -------------------------------------------------------------------------------- 1 | create or replace function graphql.exception(message text) 2 | returns text 3 | language plpgsql 4 | as $$ 5 | begin 6 | raise exception using errcode='22000', message=message; 7 | end; 8 | $$; 9 | -------------------------------------------------------------------------------- /sql/resolve.sql: -------------------------------------------------------------------------------- 1 | create or replace function graphql.resolve( 2 | "query" text, 3 | "variables" jsonb default '{}', 4 | "operationName" text default null, 5 | "extensions" jsonb default null 6 | ) 7 | returns jsonb 8 | language plpgsql 9 | as $$ 10 | declare 11 | res jsonb; 12 | message_text text; 13 | begin 14 | begin 15 | select graphql._internal_resolve("query" := "query", 16 | "variables" := "variables", 17 | "operationName" := "operationName", 18 | "extensions" := "extensions") into res; 19 | return res; 20 | exception 21 | when others then 22 | get stacked diagnostics message_text = message_text; 23 | return 24 | jsonb_build_object('data', null, 25 | 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); 26 | end; 27 | end; 28 | $$; 29 | -------------------------------------------------------------------------------- /sql/schema_version.sql: -------------------------------------------------------------------------------- 1 | -- Is updated every time the schema changes 2 | create sequence if not exists graphql.seq_schema_version as int cycle; 3 | 4 | create or replace function graphql.increment_schema_version() 5 | returns event_trigger 6 | security definer 7 | language plpgsql 8 | as $$ 9 | begin 10 | perform pg_catalog.nextval('graphql.seq_schema_version'); 11 | end; 12 | $$; 13 | 14 | create or replace function graphql.get_schema_version() 15 | returns int 16 | security definer 17 | language sql 18 | as $$ 19 | select last_value from graphql.seq_schema_version; 20 | $$; 21 | 22 | -- On DDL event, increment the schema version number 23 | create event trigger graphql_watch_ddl 24 | on ddl_command_end 25 | execute procedure graphql.increment_schema_version(); 26 | 27 | create event trigger graphql_watch_drop 28 | on sql_drop 29 | execute procedure graphql.increment_schema_version(); 30 | -------------------------------------------------------------------------------- /src/bin/pgrx_embed.rs: -------------------------------------------------------------------------------- 1 | ::pgrx::pgrx_embed!(); 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::graphql::*; 2 | use crate::omit::Omit; 3 | use graphql_parser::query::parse_query; 4 | use pgrx::*; 5 | use resolve::resolve_inner; 6 | use serde_json::json; 7 | 8 | mod builder; 9 | mod graphql; 10 | mod gson; 11 | mod merge; 12 | mod omit; 13 | mod parser_util; 14 | mod resolve; 15 | mod sql_types; 16 | mod transpile; 17 | 18 | pg_module_magic!(); 19 | 20 | extension_sql_file!("../sql/schema_version.sql"); 21 | extension_sql_file!("../sql/directives.sql"); 22 | extension_sql_file!("../sql/raise_exception.sql"); 23 | extension_sql_file!("../sql/resolve.sql", requires = [resolve]); 24 | 25 | #[allow(non_snake_case, unused_variables)] 26 | #[pg_extern(name = "_internal_resolve")] 27 | fn resolve( 28 | query: &str, 29 | variables: default!(Option, "'{}'"), 30 | operationName: default!(Option, "null"), 31 | extensions: default!(Option, "null"), 32 | ) -> pgrx::JsonB { 33 | // Parse the GraphQL Query 34 | let query_ast_option = parse_query::<&str>(query); 35 | 36 | let response: GraphQLResponse = match query_ast_option { 37 | // Parser errors 38 | Err(err) => { 39 | let errors = vec![ErrorMessage { 40 | message: err.to_string(), 41 | }]; 42 | 43 | GraphQLResponse { 44 | data: Omit::Omitted, 45 | errors: Omit::Present(errors), 46 | } 47 | } 48 | Ok(query_ast) => { 49 | let sql_config = sql_types::load_sql_config(); 50 | let context = sql_types::load_sql_context(&sql_config); 51 | 52 | match context { 53 | Ok(context) => { 54 | let graphql_schema = __Schema { context }; 55 | let variables = variables.map_or(json!({}), |v| v.0); 56 | resolve_inner(query_ast, &variables, &operationName, &graphql_schema) 57 | } 58 | Err(err) => GraphQLResponse { 59 | data: Omit::Omitted, 60 | errors: Omit::Present(vec![ErrorMessage { message: err }]), 61 | }, 62 | } 63 | } 64 | }; 65 | 66 | let value: serde_json::Value = 67 | serde_json::to_value(response).expect("failed to convert response into json"); 68 | 69 | pgrx::JsonB(value) 70 | } 71 | 72 | #[cfg(any(test, feature = "pg_test"))] 73 | #[pg_schema] 74 | mod tests {} 75 | 76 | #[cfg(test)] 77 | pub mod pg_test { 78 | pub fn setup(_options: Vec<&str>) { 79 | // perform one-off initialization when the pg_test framework starts 80 | } 81 | 82 | pub fn postgresql_conf_options() -> Vec<&'static str> { 83 | // return any postgresql.conf settings that are required for your tests 84 | vec![] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/omit.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize, Clone)] 4 | #[serde(untagged)] 5 | pub enum Omit { 6 | Omitted, 7 | Present(T), 8 | } 9 | 10 | impl Omit { 11 | pub fn is_omit(&self) -> bool { 12 | matches!(self, Self::Omitted) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/expected/aggregate_directive.out: -------------------------------------------------------------------------------- 1 | begin; 2 | -- Create a simple table without any directives 3 | create table product( 4 | id serial primary key, 5 | name text not null, 6 | price numeric not null, 7 | stock int not null 8 | ); 9 | insert into product(name, price, stock) 10 | values 11 | ('Widget', 9.99, 100), 12 | ('Gadget', 19.99, 50), 13 | ('Gizmo', 29.99, 25); 14 | -- Try to query aggregate without enabling the directive - should fail 15 | select graphql.resolve($$ 16 | { 17 | productCollection { 18 | aggregate { 19 | count 20 | } 21 | } 22 | } 23 | $$); 24 | resolve 25 | --------------------------------------------------------------------------------------------- 26 | {"data": null, "errors": [{"message": "enable the aggregate directive to use aggregates"}]} 27 | (1 row) 28 | 29 | -- Enable aggregates 30 | comment on table product is e'@graphql({"aggregate": {"enabled": true}})'; 31 | -- Now aggregates should be available - should succeed 32 | select graphql.resolve($$ 33 | { 34 | productCollection { 35 | aggregate { 36 | count 37 | sum { 38 | price 39 | stock 40 | } 41 | avg { 42 | price 43 | } 44 | max { 45 | price 46 | name 47 | } 48 | min { 49 | stock 50 | } 51 | } 52 | } 53 | } 54 | $$); 55 | resolve 56 | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 57 | {"data": {"productCollection": {"aggregate": {"avg": {"price": 19.99}, "max": {"name": "Widget", "price": 29.99}, "min": {"stock": 25}, "sum": {"price": 59.97, "stock": 175}, "count": 3}}}} 58 | (1 row) 59 | 60 | rollback; 61 | -------------------------------------------------------------------------------- /test/expected/aliases.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | comment on table account is e'@graphql({"totalCount": {"enabled": true}})'; 7 | insert into public.account(email) 8 | values 9 | ('aardvark@x.com'); 10 | create table blog( 11 | id serial primary key, 12 | owner_id integer not null references account(id), 13 | name varchar(255) not null 14 | ); 15 | comment on table blog is e'@graphql({"totalCount": {"enabled": true}})'; 16 | insert into blog(owner_id, name) 17 | values 18 | (1, 'A: Blog 1'); 19 | -- Connection: alias all field types and operation 20 | select jsonb_pretty( 21 | graphql.resolve($$ 22 | { 23 | aA: accountCollection(first: 1) { 24 | tc: totalCount 25 | pi: pageInfo { 26 | hnp: hasNextPage 27 | } 28 | e: edges { 29 | c: cursor 30 | n: node{ 31 | id_: id 32 | b: blogCollection { 33 | tc2: totalCount 34 | } 35 | } 36 | } 37 | } 38 | } 39 | $$) 40 | ); 41 | jsonb_pretty 42 | -------------------------------------- 43 | { + 44 | "data": { + 45 | "aA": { + 46 | "e": [ + 47 | { + 48 | "c": "WzFd", + 49 | "n": { + 50 | "b": { + 51 | "tc2": 1+ 52 | }, + 53 | "id_": 1 + 54 | } + 55 | } + 56 | ], + 57 | "pi": { + 58 | "hnp": false + 59 | }, + 60 | "tc": 1 + 61 | } + 62 | } + 63 | } 64 | (1 row) 65 | 66 | select graphql.resolve($$ 67 | query Introspec { 68 | s: __schema { 69 | q: queryType { 70 | n: name 71 | } 72 | } 73 | } 74 | $$); 75 | resolve 76 | ---------------------------------------- 77 | {"data": {"s": {"q": {"n": "Query"}}}} 78 | (1 row) 79 | 80 | rollback; 81 | -------------------------------------------------------------------------------- /test/expected/comment_directive.out: -------------------------------------------------------------------------------- 1 | select 2 | graphql.comment_directive( 3 | comment_ := '@graphql({"name": "myField"})' 4 | ); 5 | comment_directive 6 | --------------------- 7 | {"name": "myField"} 8 | (1 row) 9 | 10 | select 11 | graphql.comment_directive( 12 | comment_ := '@graphql({"name": "myField with (parentheses)"})' 13 | ); 14 | comment_directive 15 | ---------------------------------------- 16 | {"name": "myField with (parentheses)"} 17 | (1 row) 18 | 19 | select 20 | graphql.comment_directive( 21 | comment_ := '@graphql({"name": "myField with a (starting parenthesis"})' 22 | ); 23 | comment_directive 24 | -------------------------------------------------- 25 | {"name": "myField with a (starting parenthesis"} 26 | (1 row) 27 | 28 | select 29 | graphql.comment_directive( 30 | comment_ := '@graphql({"name": "myField with an ending parenthesis)"})' 31 | ); 32 | comment_directive 33 | ------------------------------------------------- 34 | {"name": "myField with an ending parenthesis)"} 35 | (1 row) 36 | 37 | -------------------------------------------------------------------------------- /test/expected/comment_directive_description.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.account( 3 | id int primary key 4 | ); 5 | create function public._one(rec public.account) 6 | returns int 7 | immutable 8 | strict 9 | language sql 10 | as $$ 11 | select 1 12 | $$; 13 | comment on table public.account 14 | is e'@graphql({"description": "Some Description"})'; 15 | comment on column public.account.id 16 | is e'@graphql({"description": "Some Other Description"})'; 17 | comment on function public._one 18 | is e'@graphql({"description": "Func Description"})'; 19 | select jsonb_pretty( 20 | graphql.resolve($$ 21 | { 22 | __type(name: "Account") { 23 | kind 24 | description 25 | fields { 26 | name 27 | description 28 | } 29 | } 30 | } 31 | $$) 32 | ); 33 | jsonb_pretty 34 | ------------------------------------------------------------------------ 35 | { + 36 | "data": { + 37 | "__type": { + 38 | "kind": "OBJECT", + 39 | "fields": [ + 40 | { + 41 | "name": "nodeId", + 42 | "description": "Globally Unique Record Identifier"+ 43 | }, + 44 | { + 45 | "name": "id", + 46 | "description": "Some Other Description" + 47 | }, + 48 | { + 49 | "name": "one", + 50 | "description": "Func Description" + 51 | } + 52 | ], + 53 | "description": "Some Description" + 54 | } + 55 | } + 56 | } 57 | (1 row) 58 | 59 | rollback; 60 | -------------------------------------------------------------------------------- /test/expected/empty_mutations.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create role api; 3 | grant usage on schema graphql to api; 4 | grant execute on function graphql.resolve to api; 5 | create table xyz( id int primary key); 6 | -- Remove mutations so mutationType is null 7 | revoke update on xyz from api; 8 | revoke delete on xyz from api; 9 | set role api; 10 | -- mutationType should be null 11 | select jsonb_pretty( 12 | graphql.resolve($$ 13 | query IntrospectionQuery { 14 | __schema { 15 | queryType { 16 | name 17 | } 18 | mutationType { 19 | name 20 | } 21 | } 22 | } 23 | $$) 24 | ); 25 | jsonb_pretty 26 | ---------------------------------- 27 | { + 28 | "data": { + 29 | "__schema": { + 30 | "queryType": { + 31 | "name": "Query" + 32 | }, + 33 | "mutationType": null+ 34 | } + 35 | } + 36 | } 37 | (1 row) 38 | 39 | rollback; 40 | -------------------------------------------------------------------------------- /test/expected/extend_type_with_function.out: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | create table public.account( 4 | id serial primary key, 5 | first_name varchar(255) not null, 6 | last_name varchar(255) not null, 7 | parent_id int references account(id) 8 | ); 9 | -- Extend with function 10 | create function public._full_name(rec public.account) 11 | returns text 12 | immutable 13 | strict 14 | language sql 15 | as $$ 16 | select format('%s %s', rec.first_name, rec.last_name) 17 | $$; 18 | insert into public.account(first_name, last_name, parent_id) 19 | values 20 | ('Foo', 'Fooington', 1); 21 | select jsonb_pretty( 22 | graphql.resolve($$ 23 | { 24 | accountCollection { 25 | edges { 26 | node { 27 | id 28 | firstName 29 | lastName 30 | fullName 31 | parent { 32 | fullName 33 | } 34 | } 35 | } 36 | } 37 | } 38 | $$) 39 | ); 40 | jsonb_pretty 41 | --------------------------------------------------------- 42 | { + 43 | "data": { + 44 | "accountCollection": { + 45 | "edges": [ + 46 | { + 47 | "node": { + 48 | "id": 1, + 49 | "parent": { + 50 | "fullName": "Foo Fooington"+ 51 | }, + 52 | "fullName": "Foo Fooington", + 53 | "lastName": "Fooington", + 54 | "firstName": "Foo" + 55 | } + 56 | } + 57 | ] + 58 | } + 59 | } + 60 | } 61 | (1 row) 62 | 63 | rollback; 64 | -------------------------------------------------------------------------------- /test/expected/extend_type_with_generated_column.out: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | create table public.account( 4 | id serial primary key, 5 | first_name varchar(255) not null, 6 | last_name varchar(255) not null, 7 | -- Computed Column 8 | full_name text generated always as (first_name || ' ' || last_name) stored 9 | ); 10 | insert into public.account(first_name, last_name) 11 | values 12 | ('Foo', 'Fooington'); 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | accountCollection { 17 | edges { 18 | node { 19 | id 20 | firstName 21 | lastName 22 | fullName 23 | } 24 | } 25 | } 26 | } 27 | $$) 28 | ); 29 | jsonb_pretty 30 | ------------------------------------------------------ 31 | { + 32 | "data": { + 33 | "accountCollection": { + 34 | "edges": [ + 35 | { + 36 | "node": { + 37 | "id": 1, + 38 | "fullName": "Foo Fooington",+ 39 | "lastName": "Fooington", + 40 | "firstName": "Foo" + 41 | } + 42 | } + 43 | ] + 44 | } + 45 | } + 46 | } 47 | (1 row) 48 | 49 | rollback; 50 | -------------------------------------------------------------------------------- /test/expected/fragment_on_mutation.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table blog_post( 3 | id int primary key, 4 | title text not null 5 | ); 6 | select graphql.resolve($$ 7 | mutation { 8 | ...blogPosts_insert 9 | } 10 | 11 | fragment blogPosts_insert on Mutation { 12 | insertIntoBlogPostCollection(objects: [ 13 | { id: 1, title: "foo" } 14 | ]) { 15 | affectedCount 16 | records { 17 | id 18 | title 19 | } 20 | } 21 | } 22 | $$); 23 | resolve 24 | ---------------------------------------------------------------------------------------------------------- 25 | {"data": {"insertIntoBlogPostCollection": {"records": [{"id": 1, "title": "foo"}], "affectedCount": 1}}} 26 | (1 row) 27 | 28 | select * from blog_post; 29 | id | title 30 | ----+------- 31 | 1 | foo 32 | (1 row) 33 | 34 | rollback; 35 | -------------------------------------------------------------------------------- /test/expected/fragment_on_query.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table blog_post( 3 | id int primary key, 4 | title text not null 5 | ); 6 | select graphql.resolve($$ 7 | query { 8 | ...blogPosts_query 9 | } 10 | 11 | fragment blogPosts_query on Query { 12 | blogPostCollection(first:2) { 13 | edges 14 | { 15 | node { 16 | id 17 | title 18 | } 19 | } 20 | } 21 | } 22 | $$); 23 | resolve 24 | ------------------------------------------------- 25 | {"data": {"blogPostCollection": {"edges": []}}} 26 | (1 row) 27 | 28 | rollback; 29 | -------------------------------------------------------------------------------- /test/expected/issue_163.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table profiles( 3 | id int primary key, 4 | username text 5 | ); 6 | insert into public.profiles(id, username) 7 | values 8 | (1, 'foo'); 9 | select jsonb_pretty( 10 | graphql.resolve($$ 11 | query MyQuery { 12 | __typename 13 | profilesCollection { 14 | edges { 15 | node { 16 | id 17 | username 18 | } 19 | } 20 | } 21 | } 22 | $$) 23 | ) 24 | rollback; 25 | rollback 26 | ------------------------------------------- 27 | { + 28 | "data": { + 29 | "__typename": "Query", + 30 | "profilesCollection": { + 31 | "edges": [ + 32 | { + 33 | "node": { + 34 | "id": 1, + 35 | "username": "foo"+ 36 | } + 37 | } + 38 | ] + 39 | } + 40 | } + 41 | } 42 | (1 row) 43 | 44 | -------------------------------------------------------------------------------- /test/expected/issue_170.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | insert into public.account(id) 6 | select * from generate_series(1,5); 7 | -- hasPreviousPage is true when `after` is first element of collection 8 | -- "WzFd" is id=1 9 | -- because result set does not include the record id = 1 10 | select jsonb_pretty( 11 | graphql.resolve($$ 12 | { 13 | accountCollection(first: 2, after: "WzFd") { 14 | pageInfo{ 15 | hasPreviousPage 16 | } 17 | } 18 | } 19 | $$) 20 | ); 21 | jsonb_pretty 22 | ----------------------------------------- 23 | { + 24 | "data": { + 25 | "accountCollection": { + 26 | "pageInfo": { + 27 | "hasPreviousPage": true+ 28 | } + 29 | } + 30 | } + 31 | } 32 | (1 row) 33 | 34 | -- hasPreviousPage is false when `after` is before the first element of collection 35 | -- "WzFd" is id=0 36 | select jsonb_pretty( 37 | graphql.resolve($$ 38 | { 39 | accountCollection(first: 2, after: "WzBd") { 40 | pageInfo{ 41 | hasPreviousPage 42 | } 43 | } 44 | } 45 | $$) 46 | ); 47 | jsonb_pretty 48 | ------------------------------------------ 49 | { + 50 | "data": { + 51 | "accountCollection": { + 52 | "pageInfo": { + 53 | "hasPreviousPage": false+ 54 | } + 55 | } + 56 | } + 57 | } 58 | (1 row) 59 | 60 | rollback; 61 | -------------------------------------------------------------------------------- /test/expected/issue_300.out: -------------------------------------------------------------------------------- 1 | begin; 2 | -- https://github.com/supabase/pg_graphql/issues/300 3 | create role api; 4 | create table project ( 5 | id serial primary key, 6 | title text not null, 7 | created_at int not null default '1', 8 | updated_at int not null default '2' 9 | ); 10 | grant usage on schema graphql to api; 11 | grant usage on all sequences in schema public to api; 12 | revoke all on table project from api; 13 | grant select on table project to api; 14 | grant insert (id, title) on table project to api; 15 | grant update (title) on table project to api; 16 | grant delete on table project to api; 17 | set role to 'api'; 18 | select jsonb_pretty( 19 | graphql.resolve($$ 20 | 21 | mutation CreateProject { 22 | insertIntoProjectCollection(objects: [ 23 | {title: "foo"} 24 | ]) { 25 | affectedCount 26 | records { 27 | id 28 | title 29 | createdAt 30 | updatedAt 31 | } 32 | } 33 | } 34 | $$ 35 | ) 36 | ); 37 | jsonb_pretty 38 | ------------------------------------------ 39 | { + 40 | "data": { + 41 | "insertIntoProjectCollection": {+ 42 | "records": [ + 43 | { + 44 | "id": 1, + 45 | "title": "foo", + 46 | "createdAt": 1, + 47 | "updatedAt": 2 + 48 | } + 49 | ], + 50 | "affectedCount": 1 + 51 | } + 52 | } + 53 | } 54 | (1 row) 55 | 56 | rollback; 57 | -------------------------------------------------------------------------------- /test/expected/issue_312.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create type sub_status as enum ('invited', 'not_invited'); 3 | alter type sub_status add value if not exists 'opened' after 'invited'; 4 | create table account( 5 | id int primary key, 6 | ss sub_status 7 | ); 8 | insert into public.account(id) 9 | select * from generate_series(1,5); 10 | select jsonb_pretty( 11 | graphql.resolve($$ 12 | { 13 | accountCollection(first: 1) { 14 | edges { 15 | node { 16 | id 17 | ss 18 | } 19 | } 20 | } 21 | } 22 | $$) 23 | ); 24 | jsonb_pretty 25 | ------------------------------------ 26 | { + 27 | "data": { + 28 | "accountCollection": { + 29 | "edges": [ + 30 | { + 31 | "node": { + 32 | "id": 1, + 33 | "ss": null+ 34 | } + 35 | } + 36 | ] + 37 | } + 38 | } + 39 | } 40 | (1 row) 41 | 42 | rollback; 43 | -------------------------------------------------------------------------------- /test/expected/issue_334_ambiguous_function.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.recipe_ingredient( 3 | id int primary key 4 | ); 5 | create table public.recipe( 6 | id int primary key 7 | ); 8 | insert into public.recipe(id) values (1); 9 | create or replace function _calories(rec public.recipe_ingredient) 10 | returns smallint 11 | stable 12 | language sql 13 | as $$ 14 | select 1; 15 | $$; 16 | create or replace function _calories(rec public.recipe) 17 | returns smallint 18 | stable 19 | language sql 20 | as $$ 21 | select 1; 22 | $$; 23 | select jsonb_pretty( 24 | graphql.resolve($$ 25 | { 26 | recipeCollection { 27 | edges { 28 | node { 29 | id 30 | calories 31 | } 32 | } 33 | } 34 | } 35 | $$) 36 | ); 37 | jsonb_pretty 38 | --------------------------------------- 39 | { + 40 | "data": { + 41 | "recipeCollection": { + 42 | "edges": [ + 43 | { + 44 | "node": { + 45 | "id": 1, + 46 | "calories": 1+ 47 | } + 48 | } + 49 | ] + 50 | } + 51 | } + 52 | } 53 | (1 row) 54 | 55 | rollback; 56 | -------------------------------------------------------------------------------- /test/expected/issue_339_function_return_json.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.account( 3 | id int primary key 4 | ); 5 | create function public._computed(rec public.account) 6 | returns json 7 | immutable 8 | strict 9 | language sql 10 | as $$ 11 | select jsonb_build_object('hello', 'world'); 12 | $$; 13 | insert into account(id) values (1); 14 | select jsonb_pretty( 15 | graphql.resolve($$ 16 | { 17 | accountCollection { 18 | edges { 19 | node { 20 | id 21 | computed 22 | } 23 | } 24 | } 25 | } 26 | $$) 27 | ); 28 | jsonb_pretty 29 | -------------------------------------------------------------- 30 | { + 31 | "data": { + 32 | "accountCollection": { + 33 | "edges": [ + 34 | { + 35 | "node": { + 36 | "id": 1, + 37 | "computed": "{\"hello\": \"world\"}"+ 38 | } + 39 | } + 40 | ] + 41 | } + 42 | } + 43 | } 44 | (1 row) 45 | 46 | rollback; 47 | -------------------------------------------------------------------------------- /test/expected/issue_339_function_return_table.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.account( 3 | id int primary key 4 | ); 5 | -- appears in pg_catalog as returning a set of int 6 | create function public._computed(rec public.account) 7 | returns table ( id int ) 8 | immutable 9 | strict 10 | language sql 11 | as $$ 12 | select 2 as id; 13 | $$; 14 | -- appears in pg_catalog as returning a set of pseudotype "record" 15 | create function public._computed2(rec public.account) 16 | returns table ( id int, name text ) 17 | immutable 18 | strict 19 | language sql 20 | as $$ 21 | select 2 as id, 'abc' as name; 22 | $$; 23 | insert into account(id) values (1); 24 | -- neither computed nor computed2 should be present 25 | select jsonb_pretty( 26 | graphql.resolve($$ 27 | { 28 | __type(name: "Account") { 29 | kind 30 | fields { 31 | name 32 | } 33 | } 34 | } 35 | $$) 36 | ); 37 | jsonb_pretty 38 | -------------------------------------- 39 | { + 40 | "data": { + 41 | "__type": { + 42 | "kind": "OBJECT", + 43 | "fields": [ + 44 | { + 45 | "name": "nodeId"+ 46 | }, + 47 | { + 48 | "name": "id" + 49 | } + 50 | ] + 51 | } + 52 | } + 53 | } 54 | (1 row) 55 | 56 | rollback; 57 | -------------------------------------------------------------------------------- /test/expected/multiple_queries.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null, 5 | priority int 6 | ); 7 | insert into account(email) 8 | values ('email_1'), ('email_2'); 9 | -- Scenario: Two queries 10 | select jsonb_pretty( 11 | graphql.resolve($$ 12 | { 13 | forward: accountCollection(orderBy: [{id: AscNullsFirst}]) { 14 | edges { 15 | node { 16 | id 17 | } 18 | } 19 | } 20 | backward: accountCollection(orderBy: [{id: DescNullsFirst}]) { 21 | edges { 22 | node { 23 | id 24 | } 25 | } 26 | } 27 | } 28 | $$) 29 | ); 30 | jsonb_pretty 31 | --------------------------------- 32 | { + 33 | "data": { + 34 | "forward": { + 35 | "edges": [ + 36 | { + 37 | "node": { + 38 | "id": 1+ 39 | } + 40 | }, + 41 | { + 42 | "node": { + 43 | "id": 2+ 44 | } + 45 | } + 46 | ] + 47 | }, + 48 | "backward": { + 49 | "edges": [ + 50 | { + 51 | "node": { + 52 | "id": 2+ 53 | } + 54 | }, + 55 | { + 56 | "node": { + 57 | "id": 1+ 58 | } + 59 | } + 60 | ] + 61 | } + 62 | } + 63 | } 64 | (1 row) 65 | 66 | rollback 67 | -------------------------------------------------------------------------------- /test/expected/mutation_delete_variable.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | insert into public.account(email) 7 | values 8 | ('aardvark@x.com'), 9 | ('bat@x.com'); 10 | savepoint a; 11 | -- variable filter value 12 | select graphql.resolve($$ 13 | mutation DeleteAccountByEmail($email: String!) { 14 | deleteFromAccountCollection( 15 | filter: { 16 | email: {eq: $email} 17 | } 18 | atMost: 1 19 | ) { 20 | records { id } 21 | } 22 | } 23 | $$, '{"email": "bat@x.com"}'); 24 | resolve 25 | --------------------------------------------------------------------- 26 | {"data": {"deleteFromAccountCollection": {"records": [{"id": 2}]}}} 27 | (1 row) 28 | 29 | rollback to savepoint a; 30 | -- variable entire filter 31 | select graphql.resolve($$ 32 | mutation DeleteAccountByFilter($afilt: AccountFilter!) { 33 | deleteFromAccountCollection( 34 | filter: $afilt 35 | atMost: 1 36 | ) { 37 | records { id } 38 | } 39 | } 40 | $$, 41 | variables:= '{"afilt": {"id": {"eq": 1}} }' 42 | ); 43 | resolve 44 | --------------------------------------------------------------------- 45 | {"data": {"deleteFromAccountCollection": {"records": [{"id": 1}]}}} 46 | (1 row) 47 | 48 | rollback to savepoint a; 49 | -- variable atMost. should impact too many 50 | select graphql.resolve($$ 51 | mutation SafeDeleteAccount($atMost: Int!) { 52 | deleteFromAccountCollection( 53 | filter: {id: {eq: 1}} 54 | atMost: $atMost 55 | ) { 56 | records { id } 57 | } 58 | } 59 | $$, 60 | variables:= '{"atMost": 0 }' 61 | ); 62 | resolve 63 | ---------------------------------------------------------------------------- 64 | {"data": null, "errors": [{"message": "delete impacts too many records"}]} 65 | (1 row) 66 | 67 | rollback to savepoint a; 68 | rollback; 69 | -------------------------------------------------------------------------------- /test/expected/null_argument.out: -------------------------------------------------------------------------------- 1 | begin; 2 | -- Test that the argument parser can handle null values 3 | create table account( 4 | id serial primary key, 5 | email text 6 | ); 7 | select graphql.resolve($$ 8 | mutation { 9 | insertIntoAccountCollection(objects: [ 10 | { email: null } 11 | ]) { 12 | affectedCount 13 | records { 14 | id 15 | email 16 | } 17 | } 18 | } 19 | $$); 20 | resolve 21 | -------------------------------------------------------------------------------------------------------- 22 | {"data": {"insertIntoAccountCollection": {"records": [{"id": 1, "email": null}], "affectedCount": 1}}} 23 | (1 row) 24 | 25 | rollback; 26 | -------------------------------------------------------------------------------- /test/expected/override_enum_name.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create type account_priority as enum ('high', 'standard'); 3 | comment on type public.account_priority is E'@graphql({"name": "CustomerValue"})'; 4 | select jsonb_pretty( 5 | graphql.resolve($$ 6 | { 7 | __type(name: "CustomerValue") { 8 | enumValues { 9 | name 10 | } 11 | } 12 | } 13 | $$) 14 | ); 15 | jsonb_pretty 16 | ---------------------------------------- 17 | { + 18 | "data": { + 19 | "__type": { + 20 | "enumValues": [ + 21 | { + 22 | "name": "high" + 23 | }, + 24 | { + 25 | "name": "standard"+ 26 | } + 27 | ] + 28 | } + 29 | } + 30 | } 31 | (1 row) 32 | 33 | rollback; 34 | -------------------------------------------------------------------------------- /test/expected/override_field_name.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | comment on column public.account.email is E'@graphql({"name": "emailAddress"})'; 7 | -- expect: 'emailAddresses' 8 | select jsonb_pretty( 9 | graphql.resolve($$ 10 | { 11 | __type(name: "Account") { 12 | fields { 13 | name 14 | } 15 | } 16 | } 17 | $$) 18 | ); 19 | jsonb_pretty 20 | -------------------------------------------- 21 | { + 22 | "data": { + 23 | "__type": { + 24 | "fields": [ + 25 | { + 26 | "name": "nodeId" + 27 | }, + 28 | { + 29 | "name": "id" + 30 | }, + 31 | { + 32 | "name": "emailAddress"+ 33 | } + 34 | ] + 35 | } + 36 | } + 37 | } 38 | (1 row) 39 | 40 | rollback; 41 | -------------------------------------------------------------------------------- /test/expected/override_func_field_name.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | first_name varchar(255) not null, 5 | last_name varchar(255) not null 6 | ); 7 | -- Extend with function 8 | create function _full_name(rec public.account) 9 | returns text 10 | immutable 11 | strict 12 | language sql 13 | as $$ 14 | select format('%s %s', rec.first_name, rec.last_name) 15 | $$; 16 | comment on function public._full_name(public.account) is E'@graphql({"name": "wholeName"})'; 17 | -- expect: 'wholeName' 18 | select jsonb_pretty( 19 | graphql.resolve($$ 20 | { 21 | __type(name: "Account") { 22 | fields { 23 | name 24 | } 25 | } 26 | } 27 | $$) 28 | ); 29 | jsonb_pretty 30 | ----------------------------------------- 31 | { + 32 | "data": { + 33 | "__type": { + 34 | "fields": [ + 35 | { + 36 | "name": "nodeId" + 37 | }, + 38 | { + 39 | "name": "id" + 40 | }, + 41 | { + 42 | "name": "firstName"+ 43 | }, + 44 | { + 45 | "name": "lastName" + 46 | }, + 47 | { + 48 | "name": "wholeName"+ 49 | } + 50 | ] + 51 | } + 52 | } + 53 | } 54 | (1 row) 55 | 56 | rollback; 57 | -------------------------------------------------------------------------------- /test/expected/override_relationship_field_name.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key 4 | ); 5 | create table blog( 6 | id serial primary key, 7 | owner_id integer not null references account(id) 8 | ); 9 | comment on constraint blog_owner_id_fkey 10 | on blog 11 | is E'@graphql({"foreign_name": "author", "local_name": "blogz"})'; 12 | -- expect: 'author' 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | __type(name: "Blog") { 17 | fields { 18 | name 19 | } 20 | } 21 | } 22 | $$) 23 | ); 24 | jsonb_pretty 25 | --------------------------------------- 26 | { + 27 | "data": { + 28 | "__type": { + 29 | "fields": [ + 30 | { + 31 | "name": "nodeId" + 32 | }, + 33 | { + 34 | "name": "id" + 35 | }, + 36 | { + 37 | "name": "ownerId"+ 38 | }, + 39 | { + 40 | "name": "author" + 41 | } + 42 | ] + 43 | } + 44 | } + 45 | } 46 | (1 row) 47 | 48 | -- expect: 'blogz' 49 | select jsonb_pretty( 50 | graphql.resolve($$ 51 | { 52 | __type(name: "Account") { 53 | fields { 54 | name 55 | } 56 | } 57 | } 58 | $$) 59 | ); 60 | jsonb_pretty 61 | -------------------------------------- 62 | { + 63 | "data": { + 64 | "__type": { + 65 | "fields": [ + 66 | { + 67 | "name": "nodeId"+ 68 | }, + 69 | { + 70 | "name": "id" + 71 | }, + 72 | { + 73 | "name": "blogz" + 74 | } + 75 | ] + 76 | } + 77 | } + 78 | } 79 | (1 row) 80 | 81 | rollback; 82 | -------------------------------------------------------------------------------- /test/expected/override_type_name.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | comment on table public.account is E'@graphql({"name": "UserAccount"})'; 7 | select jsonb_pretty( 8 | jsonb_path_query( 9 | graphql.resolve($$ 10 | query IntrospectionQuery { 11 | __schema { 12 | types { 13 | name 14 | } 15 | } 16 | } 17 | $$), 18 | '$.data.__schema.types[*].name ? (@ starts with "UserAccount")' 19 | ) 20 | ); 21 | jsonb_pretty 22 | ----------------------------- 23 | "UserAccount" 24 | "UserAccountConnection" 25 | "UserAccountDeleteResponse" 26 | "UserAccountEdge" 27 | "UserAccountFilter" 28 | "UserAccountInsertInput" 29 | "UserAccountInsertResponse" 30 | "UserAccountOrderBy" 31 | "UserAccountUpdateInput" 32 | "UserAccountUpdateResponse" 33 | (10 rows) 34 | 35 | rollback; 36 | -------------------------------------------------------------------------------- /test/expected/permissions_connection_column.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | encrypted_password varchar(255) not null 5 | ); 6 | insert into public.account(encrypted_password) 7 | values 8 | ('hidden_hash'); 9 | -- Superuser 10 | select graphql.resolve( 11 | $$ 12 | { 13 | accountCollection(first: 1) { 14 | edges { 15 | node { 16 | id 17 | encryptedPassword 18 | } 19 | } 20 | } 21 | } 22 | $$ 23 | ); 24 | resolve 25 | ------------------------------------------------------------------------------------------------------- 26 | {"data": {"accountCollection": {"edges": [{"node": {"id": 1, "encryptedPassword": "hidden_hash"}}]}}} 27 | (1 row) 28 | 29 | create role api; 30 | -- Grant access to GQL 31 | grant usage on schema graphql to api; 32 | grant all on all tables in schema graphql to api; 33 | -- Allow access to public.account.id but nothing else 34 | grant usage on schema public to api; 35 | grant all on all tables in schema public to api; 36 | revoke select on public.account from api; 37 | grant select (id) on public.account to api; 38 | set role api; 39 | -- Select permitted columns 40 | select graphql.resolve( 41 | $$ 42 | { 43 | accountCollection(first: 1) { 44 | edges { 45 | node { 46 | id 47 | } 48 | } 49 | } 50 | } 51 | $$ 52 | ); 53 | resolve 54 | ------------------------------------------------------------------- 55 | {"data": {"accountCollection": {"edges": [{"node": {"id": 1}}]}}} 56 | (1 row) 57 | 58 | -- Attempt select on revoked column 59 | select graphql.resolve( 60 | $$ 61 | { 62 | accountCollection(first: 1) { 63 | edges { 64 | node { 65 | id 66 | encryptedPassword 67 | } 68 | } 69 | } 70 | } 71 | $$ 72 | ); 73 | resolve 74 | ------------------------------------------------------------------------------------------------ 75 | {"data": null, "errors": [{"message": "Unknown field 'encryptedPassword' on type 'Account'"}]} 76 | (1 row) 77 | 78 | rollback; 79 | -------------------------------------------------------------------------------- /test/expected/permissions_types.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create schema xyz; 3 | comment on schema xyz is '@graphql({"inflect_names": true})'; 4 | create type xyz.light as enum ('red'); 5 | -- expect nothing b/c not in search_path 6 | select jsonb_pretty( 7 | graphql.resolve($$ 8 | { 9 | __type(name: "Light") { 10 | kind 11 | name 12 | } 13 | } 14 | $$) 15 | ); 16 | jsonb_pretty 17 | ------------------------ 18 | { + 19 | "data": { + 20 | "__type": null+ 21 | } + 22 | } 23 | (1 row) 24 | 25 | set search_path = 'xyz'; 26 | -- expect 1 record 27 | select jsonb_pretty( 28 | graphql.resolve($$ 29 | { 30 | __type(name: "Light") { 31 | kind 32 | name 33 | } 34 | } 35 | $$) 36 | ); 37 | jsonb_pretty 38 | ----------------------------- 39 | { + 40 | "data": { + 41 | "__type": { + 42 | "kind": "ENUM",+ 43 | "name": "Light"+ 44 | } + 45 | } + 46 | } 47 | (1 row) 48 | 49 | revoke all on type xyz.light from public; 50 | -- Create low priv user without access to xyz.light 51 | create role low_priv; 52 | -- expected false 53 | select pg_catalog.has_type_privilege( 54 | 'low_priv', 55 | 'xyz.light', 56 | 'USAGE' 57 | ); 58 | has_type_privilege 59 | -------------------- 60 | f 61 | (1 row) 62 | 63 | grant usage on schema xyz to low_priv; 64 | grant usage on schema graphql to low_priv; 65 | set role low_priv; 66 | -- expect no results b/c low_priv does not have usage permission 67 | select jsonb_pretty( 68 | graphql.resolve($$ 69 | { 70 | __type(name: "Light") { 71 | kind 72 | name 73 | } 74 | } 75 | $$) 76 | ); 77 | jsonb_pretty 78 | ------------------------ 79 | { + 80 | "data": { + 81 | "__type": null+ 82 | } + 83 | } 84 | (1 row) 85 | 86 | rollback; 87 | -------------------------------------------------------------------------------- /test/expected/primary_key_is_required.out: -------------------------------------------------------------------------------- 1 | begin; 2 | savepoint a; 3 | create table account( 4 | id serial primary key 5 | ); 6 | -- Should be visible because it has a primary ky 7 | select jsonb_pretty( 8 | graphql.resolve($$ 9 | { 10 | __type(name: "Account") { 11 | name 12 | } 13 | } 14 | $$) 15 | ); 16 | jsonb_pretty 17 | ------------------------------- 18 | { + 19 | "data": { + 20 | "__type": { + 21 | "name": "Account"+ 22 | } + 23 | } + 24 | } 25 | (1 row) 26 | 27 | rollback to savepoint a; 28 | create table account( 29 | id serial 30 | ); 31 | -- Should NOT be visible because it does not have a primary ky 32 | select jsonb_pretty( 33 | graphql.resolve($$ 34 | { 35 | __type(name: "Account") { 36 | name 37 | } 38 | } 39 | $$) 40 | ); 41 | jsonb_pretty 42 | ------------------------ 43 | { + 44 | "data": { + 45 | "__type": null+ 46 | } + 47 | } 48 | (1 row) 49 | 50 | rollback; 51 | -------------------------------------------------------------------------------- /test/expected/resolve___type.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null, 5 | encrypted_password varchar(255) not null, 6 | created_at timestamp not null, 7 | updated_at timestamp not null 8 | ); 9 | select jsonb_pretty( 10 | graphql.resolve($$ 11 | { 12 | __type(name: "Account") { 13 | kind 14 | fields { 15 | name 16 | } 17 | } 18 | } 19 | $$) 20 | ); 21 | jsonb_pretty 22 | ------------------------------------------------- 23 | { + 24 | "data": { + 25 | "__type": { + 26 | "kind": "OBJECT", + 27 | "fields": [ + 28 | { + 29 | "name": "nodeId" + 30 | }, + 31 | { + 32 | "name": "id" + 33 | }, + 34 | { + 35 | "name": "email" + 36 | }, + 37 | { + 38 | "name": "encryptedPassword"+ 39 | }, + 40 | { + 41 | "name": "createdAt" + 42 | }, + 43 | { + 44 | "name": "updatedAt" + 45 | } + 46 | ] + 47 | } + 48 | } + 49 | } 50 | (1 row) 51 | 52 | select jsonb_pretty( 53 | graphql.resolve($$ 54 | { 55 | __type(name: "DoesNotExist") { 56 | kind 57 | fields { 58 | name 59 | } 60 | } 61 | } 62 | $$) 63 | ); 64 | jsonb_pretty 65 | ------------------------ 66 | { + 67 | "data": { + 68 | "__type": null+ 69 | } + 70 | } 71 | (1 row) 72 | 73 | rollback; 74 | -------------------------------------------------------------------------------- /test/expected/resolve_connection_named.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | insert into public.account(id) 6 | select * from generate_series(1,5); 7 | select graphql.resolve( 8 | $$ 9 | query FirstNAccounts($first_: Int!) { 10 | accountCollection(first: $first_) { 11 | edges { 12 | node { 13 | id 14 | } 15 | } 16 | } 17 | } 18 | $$, 19 | '{"first_": 2}'::jsonb 20 | ); 21 | resolve 22 | ---------------------------------------------------------------------------------------- 23 | {"data": {"accountCollection": {"edges": [{"node": {"id": 1}}, {"node": {"id": 2}}]}}} 24 | (1 row) 25 | 26 | rollback; 27 | -------------------------------------------------------------------------------- /test/expected/resolve_error_connection_edge_no_field.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | select graphql.resolve($$ 6 | { 7 | accountCollection { 8 | totalCount 9 | edges { 10 | dneField 11 | } 12 | } 13 | } 14 | $$); 15 | resolve 16 | ------------------------------------------------------------------------ 17 | {"data": null, "errors": [{"message": "unknown field in connection"}]} 18 | (1 row) 19 | 20 | rollback; 21 | -------------------------------------------------------------------------------- /test/expected/resolve_error_connection_edge_node_no_field.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | select graphql.resolve($$ 6 | { 7 | accountCollection { 8 | edges { 9 | cursor 10 | node { 11 | dneField 12 | } 13 | } 14 | } 15 | } 16 | $$); 17 | resolve 18 | --------------------------------------------------------------------------------------- 19 | {"data": null, "errors": [{"message": "Unknown field 'dneField' on type 'Account'"}]} 20 | (1 row) 21 | 22 | rollback; 23 | -------------------------------------------------------------------------------- /test/expected/resolve_error_connection_no_field.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | select graphql.resolve($$ 6 | { 7 | accountCollection { 8 | dneField 9 | totalCount 10 | } 11 | } 12 | $$); 13 | resolve 14 | ------------------------------------------------------------------------ 15 | {"data": null, "errors": [{"message": "unknown field in connection"}]} 16 | (1 row) 17 | 18 | rollback; 19 | -------------------------------------------------------------------------------- /test/expected/resolve_error_from_parser.out: -------------------------------------------------------------------------------- 1 | -- Platform specific diffs so we have to test the properties here rather than exact response 2 | with d(val) as ( 3 | select graphql.resolve($$ 4 | { { { 5 | shouldFail 6 | } 7 | } 8 | $$)::json 9 | ) 10 | select 11 | ( 12 | json_typeof(val -> 'errors') = 'array' 13 | and json_array_length(val -> 'errors') = 1 14 | ) as is_valid 15 | from d; 16 | is_valid 17 | ---------- 18 | t 19 | (1 row) 20 | 21 | -------------------------------------------------------------------------------- /test/expected/resolve_error_mutation_no_field.out: -------------------------------------------------------------------------------- 1 | begin; 2 | select graphql.resolve($$ 3 | mutation { 4 | insertDNE(object: { 5 | email: "o@r.com" 6 | }) { 7 | id 8 | } 9 | } 10 | $$); 11 | resolve 12 | ------------------------------------------------------------------ 13 | {"data": null, "errors": [{"message": "Unknown type Mutation"}]} 14 | (1 row) 15 | 16 | rollback; 17 | -------------------------------------------------------------------------------- /test/expected/resolve_error_node_no_field.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key, 4 | parent_id int references account(id) 5 | ); 6 | select graphql.resolve($$ 7 | { 8 | accountCollection { 9 | edges { 10 | cursor 11 | node { 12 | parent { 13 | dneField 14 | } 15 | } 16 | } 17 | } 18 | } 19 | $$); 20 | resolve 21 | --------------------------------------------------------------------------------------- 22 | {"data": null, "errors": [{"message": "Unknown field 'dneField' on type 'Account'"}]} 23 | (1 row) 24 | 25 | rollback; 26 | -------------------------------------------------------------------------------- /test/expected/resolve_error_query_no_field.out: -------------------------------------------------------------------------------- 1 | begin; 2 | select graphql.resolve($$ 3 | { 4 | account { 5 | id 6 | } 7 | } 8 | $$); 9 | resolve 10 | ------------------------------------------------------------------------------------ 11 | {"data": null, "errors": [{"message": "Unknown field \"account\" on type Query"}]} 12 | (1 row) 13 | 14 | rollback; 15 | -------------------------------------------------------------------------------- /test/expected/resolve_fragment.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table blog( 3 | id serial primary key, 4 | owner_id integer not null, 5 | name varchar(255) not null, 6 | description text 7 | ); 8 | insert into blog(owner_id, name, description) 9 | values 10 | (1, 'A: Blog 1', 'first'), 11 | (2, 'A: Blog 2', 'second'); 12 | select graphql.resolve($$ 13 | { 14 | blogCollection(first: 1) { 15 | edges { 16 | cursor 17 | node { 18 | ...BaseBlog 19 | ownerId 20 | } 21 | } 22 | } 23 | } 24 | 25 | fragment BaseBlog on Blog { 26 | name 27 | description 28 | } 29 | $$); 30 | resolve 31 | ------------------------------------------------------------------------------------------------------------------------------------ 32 | {"data": {"blogCollection": {"edges": [{"node": {"name": "A: Blog 1", "ownerId": 1, "description": "first"}, "cursor": "WzFd"}]}}} 33 | (1 row) 34 | 35 | rollback; 36 | -------------------------------------------------------------------------------- /test/expected/row_level_security.out: -------------------------------------------------------------------------------- 1 | begin; 2 | -- Test that row level security policies are applied for non-superusers 3 | create role anon; 4 | alter default privileges in schema public grant all on tables to anon; 5 | grant usage on schema public to anon; 6 | grant usage on schema graphql to anon; 7 | grant all on function graphql.resolve to anon; 8 | create table account( 9 | id int primary key 10 | ); 11 | -- create policy such that only id=2 is visible to anon role 12 | create policy acct_select 13 | on public.account 14 | as permissive 15 | for select 16 | to anon 17 | using (id = 2); 18 | alter table public.account enable row level security; 19 | -- Create records fo id 1..10 20 | insert into public.account(id) 21 | select * from generate_series(1, 10); 22 | set role anon; 23 | -- Only id=2 should be returned 24 | select jsonb_pretty( 25 | graphql.resolve($$ 26 | { 27 | accountCollection { 28 | edges { 29 | node { 30 | id 31 | } 32 | } 33 | } 34 | } 35 | $$) 36 | ); 37 | jsonb_pretty 38 | --------------------------------- 39 | { + 40 | "data": { + 41 | "accountCollection": { + 42 | "edges": [ + 43 | { + 44 | "node": { + 45 | "id": 2+ 46 | } + 47 | } + 48 | ] + 49 | } + 50 | } + 51 | } 52 | (1 row) 53 | 54 | rollback; 55 | -------------------------------------------------------------------------------- /test/expected/test_error_anon_and_named_operations.out: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query QueryA { named } 3 | { anon }' 4 | ) 5 | resolve 6 | -------------------------------------------------------------------------------------- 7 | {"errors": [{"message": "Anonymous operations must be the only defined operation"}]} 8 | (1 row) 9 | 10 | -------------------------------------------------------------------------------- /test/expected/test_error_invalid_offset.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table blog( 3 | id serial primary key, 4 | owner_id integer not null, 5 | name varchar(255) not null, 6 | description text 7 | ); 8 | insert into blog(owner_id, name, description) 9 | values 10 | (1, 'A: Blog 1', 'first'), 11 | (2, 'A: Blog 2', 'second'); 12 | select graphql.resolve($$ 13 | { 14 | blogCollection(first: -1) { 15 | edges { 16 | cursor 17 | node { 18 | ownerId 19 | } 20 | } 21 | } 22 | } 23 | 24 | $$); 25 | resolve 26 | -------------------------------------------------------------------------------- 27 | {"data": null, "errors": [{"message": "`first` must be an unsigned integer"}]} 28 | (1 row) 29 | 30 | select graphql.resolve($$ 31 | { 32 | blogCollection(last: -1) { 33 | edges { 34 | cursor 35 | node { 36 | ownerId 37 | } 38 | } 39 | } 40 | } 41 | 42 | $$); 43 | resolve 44 | ------------------------------------------------------------------------------- 45 | {"data": null, "errors": [{"message": "`last` must be an unsigned integer"}]} 46 | (1 row) 47 | 48 | comment on schema public is E'@graphql({"max_rows": -1, "inflect_names": true})'; 49 | select graphql.resolve($$ 50 | { 51 | blogCollection { 52 | edges { 53 | cursor 54 | node { 55 | ownerId 56 | } 57 | } 58 | } 59 | } 60 | 61 | $$); 62 | resolve 63 | ------------------------------------------------------------------------------------------------------------------------------ 64 | {"errors": [{"message": "Error while loading schema, check comment directives. invalid value: integer `-1`, expected u64"}]} 65 | (1 row) 66 | 67 | rollback; 68 | -------------------------------------------------------------------------------- /test/expected/test_error_multiple_anon_operations.out: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='{ anon1 } { anon2 }' 3 | ) 4 | resolve 5 | -------------------------------------------------------------------------------------- 6 | {"errors": [{"message": "Anonymous operations must be the only defined operation"}]} 7 | (1 row) 8 | 9 | -------------------------------------------------------------------------------- /test/expected/test_error_mutation_transpilation.out: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | create table public.account( 4 | id serial primary key, 5 | first_name varchar(255) not null check (first_name not like '%_%') 6 | ); 7 | -- Second mutation is supposed to generate an exception 8 | select 9 | jsonb_pretty( 10 | graphql.resolve($$ 11 | mutation { 12 | firstInsert: insertIntoAccountCollection(objects: [ 13 | { firstName: "name" } 14 | ]) { 15 | records { 16 | id 17 | firstName 18 | } 19 | } 20 | 21 | secondInsert: insertIntoAccountCollection(objects: [ 22 | { firstName: "another_name" } 23 | ]) { 24 | records { 25 | id 26 | firstName 27 | } 28 | } 29 | } 30 | $$) 31 | ); 32 | jsonb_pretty 33 | ------------------------------------------------------------------------------------------------------------------ 34 | { + 35 | "data": null, + 36 | "errors": [ + 37 | { + 38 | "message": "new row for relation \"account\" violates check constraint \"account_first_name_check\""+ 39 | } + 40 | ] + 41 | } 42 | (1 row) 43 | 44 | select * from public.account; 45 | id | first_name 46 | ----+------------ 47 | (0 rows) 48 | 49 | rollback; 50 | -------------------------------------------------------------------------------- /test/expected/test_error_operation_name_not_found.out: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query ABC { anon }', 3 | "operationName":='DEF' 4 | ) 5 | resolve 6 | -------------------------------------------------- 7 | {"errors": [{"message": "Operation not found"}]} 8 | (1 row) 9 | 10 | -------------------------------------------------------------------------------- /test/expected/test_error_operation_names_not_unique.out: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query ABC { anon } 3 | query ABC { other }' 4 | ) 5 | resolve 6 | ------------------------------------------------------------- 7 | {"errors": [{"message": "Operation names must be unique"}]} 8 | (1 row) 9 | 10 | -------------------------------------------------------------------------------- /test/expected/test_error_query_transpilation.out: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | create table public.account( 4 | id serial primary key, 5 | first_name varchar(255) not null 6 | ); 7 | insert into public.account(first_name) values ('foo'); 8 | -- Extend with function 9 | create function public._raise_err(rec public.account) 10 | returns text 11 | immutable 12 | strict 13 | language sql 14 | as $$ 15 | select 1/0 -- divide by 0 error 16 | $$; 17 | select 18 | jsonb_pretty( 19 | graphql.resolve($$ 20 | { 21 | accountCollection { 22 | edges { 23 | node { 24 | id 25 | firstName 26 | raiseErr 27 | } 28 | } 29 | } 30 | } 31 | $$) 32 | ); 33 | jsonb_pretty 34 | ------------------------------------------- 35 | { + 36 | "data": null, + 37 | "errors": [ + 38 | { + 39 | "message": "division by zero"+ 40 | } + 41 | ] + 42 | } 43 | (1 row) 44 | 45 | select * from public.account; 46 | id | first_name 47 | ----+------------ 48 | 1 | foo 49 | (1 row) 50 | 51 | rollback; 52 | -------------------------------------------------------------------------------- /test/expected/test_error_subscription.out: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='subscription Abc { anon }' 3 | ) 4 | resolve 5 | -------------------------------------------------------------- 6 | {"errors": [{"message": "Subscriptions are not supported"}]} 7 | (1 row) 8 | 9 | -------------------------------------------------------------------------------- /test/expected/test_query__type.out: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query Abc { __type(name: "Int") { name kind description } }' 3 | ); 4 | resolve 5 | ---------------------------------------------------------------------------------------------------------- 6 | {"data": {"__type": {"kind": "SCALAR", "name": "Int", "description": "A scalar integer up to 32 bits"}}} 7 | (1 row) 8 | 9 | -------------------------------------------------------------------------------- /test/expected/total_count.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | insert into public.account(email) 7 | values 8 | ('a@x.com'), 9 | ('b@x.com'); 10 | -- Should fail. totalCount not enabled 11 | select graphql.resolve($$ 12 | { 13 | accountCollection { 14 | totalCount 15 | edges { 16 | cursor 17 | } 18 | } 19 | } 20 | $$); 21 | resolve 22 | ------------------------------------------------------------------------ 23 | {"data": null, "errors": [{"message": "unknown field in connection"}]} 24 | (1 row) 25 | 26 | -- Enable totalCount 27 | comment on table account is e'@graphql({"totalCount": {"enabled": true}})'; 28 | -- Should work. totalCount is enabled 29 | select graphql.resolve($$ 30 | { 31 | accountCollection { 32 | totalCount 33 | edges { 34 | cursor 35 | } 36 | } 37 | } 38 | $$); 39 | resolve 40 | ------------------------------------------------------------------------------------------------------- 41 | {"data": {"accountCollection": {"edges": [{"cursor": "WzFd"}, {"cursor": "WzJd"}], "totalCount": 2}}} 42 | (1 row) 43 | 44 | rollback; 45 | -------------------------------------------------------------------------------- /test/expected/variable_default.out: -------------------------------------------------------------------------------- 1 | begin; 2 | create table blog( 3 | id int primary key 4 | ); 5 | insert into blog(id) 6 | select generate_series(1, 5); 7 | -- User defined default for variable $first. 8 | -- Returns 2 rows 9 | -- No value provided for variable $first so user defined default applies 10 | select graphql.resolve($$ 11 | query Blogs($first: Int = 2) { 12 | blogCollection(first: $first) { 13 | edges { 14 | node { 15 | id 16 | } 17 | } 18 | } 19 | } 20 | $$); 21 | resolve 22 | ------------------------------------------------------------------------------------- 23 | {"data": {"blogCollection": {"edges": [{"node": {"id": 1}}, {"node": {"id": 2}}]}}} 24 | (1 row) 25 | 26 | -- Returns 1 row 27 | -- Provided value for variable $first applies 28 | select graphql.resolve($$ 29 | query Blogs($first: Int = 2) { 30 | blogCollection(first: $first) { 31 | edges { 32 | node { 33 | id 34 | } 35 | } 36 | } 37 | } 38 | $$, 39 | variables := jsonb_build_object( 40 | 'first', 1 41 | ) 42 | ); 43 | resolve 44 | ---------------------------------------------------------------- 45 | {"data": {"blogCollection": {"edges": [{"node": {"id": 1}}]}}} 46 | (1 row) 47 | 48 | -- Returns all rows 49 | -- No default, no variable value. Falls back to sever side behavior 50 | select graphql.resolve($$ 51 | query Blogs($first: Int) { 52 | blogCollection(first: $first) { 53 | edges { 54 | node { 55 | id 56 | } 57 | } 58 | } 59 | } 60 | $$ 61 | ); 62 | resolve 63 | ---------------------------------------------------------------------------------------------------------------------------------------------------- 64 | {"data": {"blogCollection": {"edges": [{"node": {"id": 1}}, {"node": {"id": 2}}, {"node": {"id": 3}}, {"node": {"id": 4}}, {"node": {"id": 5}}]}}} 65 | (1 row) 66 | 67 | rollback; 68 | -------------------------------------------------------------------------------- /test/fixtures.sql: -------------------------------------------------------------------------------- 1 | drop extension if exists pg_graphql; 2 | create extension pg_graphql cascade; 3 | comment on schema public is '@graphql({"inflect_names": true})'; 4 | 5 | 6 | -- To remove after test suite port 7 | create or replace function graphql.encode(jsonb) 8 | returns text 9 | language sql 10 | immutable 11 | as $$ 12 | /* 13 | select graphql.encode('("{""(email,asc,t)"",""(id,asc,f)""}","[""aardvark@x.com"", 1]")'::graphql.cursor) 14 | */ 15 | select encode(convert_to($1::text, 'utf-8'), 'base64') 16 | $$; 17 | -------------------------------------------------------------------------------- /test/sql/aggregate_directive.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | -- Create a simple table without any directives 4 | create table product( 5 | id serial primary key, 6 | name text not null, 7 | price numeric not null, 8 | stock int not null 9 | ); 10 | 11 | insert into product(name, price, stock) 12 | values 13 | ('Widget', 9.99, 100), 14 | ('Gadget', 19.99, 50), 15 | ('Gizmo', 29.99, 25); 16 | 17 | -- Try to query aggregate without enabling the directive - should fail 18 | select graphql.resolve($$ 19 | { 20 | productCollection { 21 | aggregate { 22 | count 23 | } 24 | } 25 | } 26 | $$); 27 | 28 | -- Enable aggregates 29 | comment on table product is e'@graphql({"aggregate": {"enabled": true}})'; 30 | 31 | -- Now aggregates should be available - should succeed 32 | select graphql.resolve($$ 33 | { 34 | productCollection { 35 | aggregate { 36 | count 37 | sum { 38 | price 39 | stock 40 | } 41 | avg { 42 | price 43 | } 44 | max { 45 | price 46 | name 47 | } 48 | min { 49 | stock 50 | } 51 | } 52 | } 53 | } 54 | $$); 55 | 56 | rollback; 57 | -------------------------------------------------------------------------------- /test/sql/aliases.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null 6 | ); 7 | comment on table account is e'@graphql({"totalCount": {"enabled": true}})'; 8 | 9 | 10 | insert into public.account(email) 11 | values 12 | ('aardvark@x.com'); 13 | 14 | 15 | create table blog( 16 | id serial primary key, 17 | owner_id integer not null references account(id), 18 | name varchar(255) not null 19 | ); 20 | comment on table blog is e'@graphql({"totalCount": {"enabled": true}})'; 21 | 22 | 23 | insert into blog(owner_id, name) 24 | values 25 | (1, 'A: Blog 1'); 26 | 27 | -- Connection: alias all field types and operation 28 | select jsonb_pretty( 29 | graphql.resolve($$ 30 | { 31 | aA: accountCollection(first: 1) { 32 | tc: totalCount 33 | pi: pageInfo { 34 | hnp: hasNextPage 35 | } 36 | e: edges { 37 | c: cursor 38 | n: node{ 39 | id_: id 40 | b: blogCollection { 41 | tc2: totalCount 42 | } 43 | } 44 | } 45 | } 46 | } 47 | $$) 48 | ); 49 | 50 | select graphql.resolve($$ 51 | query Introspec { 52 | s: __schema { 53 | q: queryType { 54 | n: name 55 | } 56 | } 57 | } 58 | $$); 59 | 60 | 61 | rollback; 62 | -------------------------------------------------------------------------------- /test/sql/bigint_is_string.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog_post( 4 | id bigserial primary key, 5 | title text not null, 6 | parent_id bigint references blog_post(id) 7 | ); 8 | comment on table blog_post is e'@graphql({"totalCount": {"enabled": true}})'; 9 | 10 | select graphql.resolve($$ 11 | mutation { 12 | insertIntoBlogPostCollection(objects: [{ 13 | title: "hello" 14 | parentId: "1" 15 | }]) { 16 | affectedCount 17 | records { 18 | id 19 | parentId 20 | } 21 | } 22 | } 23 | $$); 24 | 25 | select graphql.resolve($$ 26 | mutation { 27 | updateBlogPostCollection(set: { 28 | title: "xx" 29 | }) { 30 | affectedCount 31 | records { 32 | id 33 | parentId 34 | } 35 | } 36 | } 37 | $$); 38 | 39 | select graphql.resolve($$ 40 | { 41 | blogPostCollection { 42 | totalCount 43 | edges { 44 | node { 45 | id 46 | parentId 47 | parent { 48 | id 49 | parentId 50 | } 51 | } 52 | } 53 | } 54 | } 55 | $$); 56 | 57 | select graphql.resolve($$ 58 | mutation { 59 | deleteFromBlogPostCollection { 60 | affectedCount 61 | records { 62 | id 63 | parentId 64 | } 65 | } 66 | } 67 | $$); 68 | 69 | rollback; 70 | -------------------------------------------------------------------------------- /test/sql/comment_directive.sql: -------------------------------------------------------------------------------- 1 | select 2 | graphql.comment_directive( 3 | comment_ := '@graphql({"name": "myField"})' 4 | ); 5 | 6 | select 7 | graphql.comment_directive( 8 | comment_ := '@graphql({"name": "myField with (parentheses)"})' 9 | ); 10 | 11 | select 12 | graphql.comment_directive( 13 | comment_ := '@graphql({"name": "myField with a (starting parenthesis"})' 14 | ); 15 | 16 | select 17 | graphql.comment_directive( 18 | comment_ := '@graphql({"name": "myField with an ending parenthesis)"})' 19 | ); 20 | -------------------------------------------------------------------------------- /test/sql/comment_directive_description.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.account( 3 | id int primary key 4 | ); 5 | 6 | create function public._one(rec public.account) 7 | returns int 8 | immutable 9 | strict 10 | language sql 11 | as $$ 12 | select 1 13 | $$; 14 | 15 | comment on table public.account 16 | is e'@graphql({"description": "Some Description"})'; 17 | 18 | comment on column public.account.id 19 | is e'@graphql({"description": "Some Other Description"})'; 20 | 21 | comment on function public._one 22 | is e'@graphql({"description": "Func Description"})'; 23 | 24 | select jsonb_pretty( 25 | graphql.resolve($$ 26 | { 27 | __type(name: "Account") { 28 | kind 29 | description 30 | fields { 31 | name 32 | description 33 | } 34 | } 35 | } 36 | $$) 37 | ); 38 | 39 | rollback; 40 | -------------------------------------------------------------------------------- /test/sql/empty_mutations.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create role api; 4 | grant usage on schema graphql to api; 5 | grant execute on function graphql.resolve to api; 6 | 7 | create table xyz( id int primary key); 8 | 9 | -- Remove mutations so mutationType is null 10 | revoke update on xyz from api; 11 | revoke delete on xyz from api; 12 | 13 | set role api; 14 | 15 | -- mutationType should be null 16 | select jsonb_pretty( 17 | graphql.resolve($$ 18 | query IntrospectionQuery { 19 | __schema { 20 | queryType { 21 | name 22 | } 23 | mutationType { 24 | name 25 | } 26 | } 27 | } 28 | $$) 29 | ); 30 | 31 | rollback; 32 | -------------------------------------------------------------------------------- /test/sql/enum_mappings.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create type my_enum as enum ('test', 'valid value', 'another value'); 4 | 5 | comment on type my_enum is E'@graphql({"mappings": {"valid value": "valid_value", "another value": "another_value"}})'; 6 | 7 | create table enums ( 8 | id serial primary key, 9 | value my_enum 10 | ); 11 | 12 | -- Seed with value that's valid in both Postgres and GraphQL 13 | insert into enums (value) values ('test'); 14 | 15 | -- Mutation to insert 16 | select graphql.resolve($$ 17 | mutation { 18 | insertIntoEnumsCollection(objects: [ { value: "valid_value" } ]) { 19 | affectedCount 20 | } 21 | } 22 | $$); 23 | 24 | -- Mutation to update 25 | select graphql.resolve($$ 26 | mutation { 27 | updateEnumsCollection(set: { value: "another_value" }, filter: { value: {eq: "test"} } ) { 28 | records { value } 29 | } 30 | } 31 | $$); 32 | 33 | --- Query 34 | select graphql.resolve($$ 35 | { 36 | enumsCollection { 37 | edges { 38 | node { 39 | value 40 | } 41 | } 42 | } 43 | } 44 | $$); 45 | 46 | --- Query with filter 47 | select graphql.resolve($$ 48 | { 49 | enumsCollection(filter: {value: {eq: "another_value"}}) { 50 | edges { 51 | node { 52 | value 53 | } 54 | } 55 | } 56 | } 57 | $$); 58 | 59 | --- Query with `in` filter 60 | select graphql.resolve($$ 61 | { 62 | enumsCollection(filter: {value: {in: ["another_value"]}}) { 63 | edges { 64 | node { 65 | value 66 | } 67 | } 68 | } 69 | } 70 | $$); 71 | 72 | -- Display type via introspection 73 | select jsonb_pretty( 74 | graphql.resolve($$ 75 | { 76 | __type(name: "MyEnum") { 77 | kind 78 | name 79 | enumValues { 80 | name 81 | } 82 | } 83 | } 84 | $$) 85 | ); 86 | 87 | rollback; 88 | -------------------------------------------------------------------------------- /test/sql/extend_type_with_function.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | 4 | create table public.account( 5 | id serial primary key, 6 | first_name varchar(255) not null, 7 | last_name varchar(255) not null, 8 | parent_id int references account(id) 9 | ); 10 | 11 | -- Extend with function 12 | create function public._full_name(rec public.account) 13 | returns text 14 | immutable 15 | strict 16 | language sql 17 | as $$ 18 | select format('%s %s', rec.first_name, rec.last_name) 19 | $$; 20 | 21 | insert into public.account(first_name, last_name, parent_id) 22 | values 23 | ('Foo', 'Fooington', 1); 24 | 25 | 26 | select jsonb_pretty( 27 | graphql.resolve($$ 28 | { 29 | accountCollection { 30 | edges { 31 | node { 32 | id 33 | firstName 34 | lastName 35 | fullName 36 | parent { 37 | fullName 38 | } 39 | } 40 | } 41 | } 42 | } 43 | $$) 44 | ); 45 | 46 | 47 | rollback; 48 | -------------------------------------------------------------------------------- /test/sql/extend_type_with_generated_column.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | 4 | create table public.account( 5 | id serial primary key, 6 | first_name varchar(255) not null, 7 | last_name varchar(255) not null, 8 | -- Computed Column 9 | full_name text generated always as (first_name || ' ' || last_name) stored 10 | ); 11 | 12 | insert into public.account(first_name, last_name) 13 | values 14 | ('Foo', 'Fooington'); 15 | 16 | 17 | select jsonb_pretty( 18 | graphql.resolve($$ 19 | { 20 | accountCollection { 21 | edges { 22 | node { 23 | id 24 | firstName 25 | lastName 26 | fullName 27 | } 28 | } 29 | } 30 | } 31 | $$) 32 | ); 33 | 34 | rollback; 35 | -------------------------------------------------------------------------------- /test/sql/filter_by_node_id.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key, 4 | email text 5 | ); 6 | 7 | insert into public.account(id, email) 8 | values 9 | (1, 'foo@foo.com'), 10 | (2, 'bar@bar.com'), 11 | (3, 'baz@baz.com'); 12 | 13 | savepoint a; 14 | 15 | -- Display the node_ids 16 | select jsonb_pretty( 17 | graphql.resolve($${accountCollection { edges { node { id nodeId } } }}$$) 18 | ); 19 | 20 | select jsonb_pretty( 21 | graphql.resolve($$ 22 | { 23 | accountCollection(filter: { nodeId: { eq: "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDJd"} } ) { 24 | edges { 25 | node { 26 | id 27 | } 28 | } 29 | } 30 | } 31 | $$) 32 | ); 33 | 34 | -- Select by nodeId 35 | select jsonb_pretty( 36 | graphql.resolve($$ 37 | { 38 | accountCollection( 39 | filter: { 40 | nodeId: {eq: "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDJd"} 41 | } 42 | ) { 43 | edges { 44 | node { 45 | id 46 | nodeId 47 | } 48 | } 49 | } 50 | }$$ 51 | ) 52 | ); 53 | 54 | -- Update by nodeId 55 | select graphql.resolve($$ 56 | mutation { 57 | updateAccountCollection( 58 | set: { 59 | email: "new@email.com" 60 | } 61 | filter: { 62 | nodeId: {eq: "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDJd"} 63 | } 64 | ) { 65 | records { id } 66 | } 67 | } 68 | $$); 69 | rollback to savepoint a; 70 | 71 | -- Delete by nodeId 72 | select graphql.resolve($$ 73 | mutation { 74 | deleteFromAccountCollection( 75 | filter: { 76 | nodeId: {eq: "WyJwdWJsaWMiLCAiYWNjb3VudCIsIDJd"} 77 | } 78 | ) { 79 | records { id } 80 | } 81 | } 82 | $$); 83 | select * from public.account; 84 | rollback to savepoint a; 85 | 86 | -- ERRORS: use incorrect table 87 | select graphql.encode('["public", "blog", 1]'::jsonb); 88 | 89 | -- Wrong table 90 | select jsonb_pretty( 91 | graphql.resolve($$ 92 | { 93 | accountCollection( 94 | filter: { 95 | nodeId: {eq: "WyJwdWJsaWMiLCAiYmxvZyIsIDFd"} 96 | } 97 | ) { 98 | edges { 99 | node { 100 | id 101 | nodeId 102 | } 103 | } 104 | } 105 | }$$ 106 | ) 107 | ); 108 | 109 | rollback; 110 | -------------------------------------------------------------------------------- /test/sql/fragment_on_mutation.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog_post( 4 | id int primary key, 5 | title text not null 6 | ); 7 | 8 | select graphql.resolve($$ 9 | mutation { 10 | ...blogPosts_insert 11 | } 12 | 13 | fragment blogPosts_insert on Mutation { 14 | insertIntoBlogPostCollection(objects: [ 15 | { id: 1, title: "foo" } 16 | ]) { 17 | affectedCount 18 | records { 19 | id 20 | title 21 | } 22 | } 23 | } 24 | $$); 25 | 26 | select * from blog_post; 27 | 28 | rollback; 29 | -------------------------------------------------------------------------------- /test/sql/fragment_on_query.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog_post( 4 | id int primary key, 5 | title text not null 6 | ); 7 | 8 | select graphql.resolve($$ 9 | query { 10 | ...blogPosts_query 11 | } 12 | 13 | fragment blogPosts_query on Query { 14 | blogPostCollection(first:2) { 15 | edges 16 | { 17 | node { 18 | id 19 | title 20 | } 21 | } 22 | } 23 | } 24 | $$); 25 | 26 | rollback; 27 | -------------------------------------------------------------------------------- /test/sql/function_return_row_is_selectable.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null 6 | ); 7 | 8 | create function returns_account() 9 | returns account language sql stable 10 | as $$ select id, email from account; $$; 11 | 12 | insert into account(email) 13 | values 14 | ('aardvark@x.com'); 15 | 16 | 17 | create role anon; 18 | grant usage on schema graphql to anon; 19 | grant select on account to anon; 20 | 21 | savepoint a; 22 | 23 | set local role anon; 24 | 25 | -- Should be visible 26 | select jsonb_pretty( 27 | graphql.resolve($$ 28 | { 29 | __type(name: "Account") { 30 | __typename 31 | } 32 | } 33 | $$) 34 | ); 35 | 36 | -- Should show an entrypoint on Query for returnAccount 37 | select jsonb_pretty( 38 | graphql.resolve($$ 39 | query IntrospectionQuery { 40 | __schema { 41 | queryType { 42 | fields { 43 | name 44 | } 45 | } 46 | } 47 | } 48 | $$) 49 | ); 50 | 51 | rollback to a; 52 | 53 | revoke select on account from anon; 54 | set local role anon; 55 | 56 | -- We should no longer see "Account" types after revoking access 57 | select jsonb_pretty( 58 | graphql.resolve($$ 59 | { 60 | __type(name: "Account") { 61 | __typename 62 | } 63 | } 64 | $$) 65 | ); 66 | 67 | -- We should no longer see returnAccount since it references an unknown return type "Account" 68 | select jsonb_pretty( 69 | graphql.resolve($$ 70 | query IntrospectionQuery { 71 | __schema { 72 | queryType { 73 | fields { 74 | name 75 | } 76 | } 77 | } 78 | } 79 | $$) 80 | ); 81 | 82 | rollback; 83 | -------------------------------------------------------------------------------- /test/sql/function_return_view_has_pkey.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create view account as 4 | select 5 | 1 as foo, 6 | 2 as bar; 7 | 8 | create function returns_account() 9 | returns account language sql stable 10 | as $$ select foo, bar from account; $$; 11 | 12 | -- Account should not be visible because the view has no primary key 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | __type(name: "Account") { 17 | __typename 18 | } 19 | } 20 | $$) 21 | ); 22 | 23 | -- returnsAccount should also not be visible because account has no primary key 24 | select jsonb_pretty( 25 | graphql.resolve($$ 26 | query IntrospectionQuery { 27 | __schema { 28 | queryType { 29 | fields { 30 | name 31 | } 32 | } 33 | } 34 | } 35 | $$) 36 | ); 37 | 38 | comment on view account is e' 39 | @graphql({ 40 | "primary_key_columns": ["foo"] 41 | })'; 42 | 43 | -- Account should be visible because the view is selectable and has a primary key 44 | select jsonb_pretty( 45 | graphql.resolve($$ 46 | { 47 | __type(name: "Account") { 48 | __typename 49 | } 50 | } 51 | $$) 52 | ); 53 | 54 | -- returnsAccount should also be visible because account has a primary key and is selectable 55 | select jsonb_pretty( 56 | graphql.resolve($$ 57 | query IntrospectionQuery { 58 | __schema { 59 | queryType { 60 | fields { 61 | name 62 | } 63 | } 64 | } 65 | } 66 | $$) 67 | ); 68 | 69 | 70 | 71 | rollback; 72 | -------------------------------------------------------------------------------- /test/sql/inflection_fields.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account ( 3 | id int primary key, 4 | name_with_underscore text 5 | ); 6 | 7 | -- Inflection off, Overrides: off 8 | comment on schema public is e'@graphql({"inflect_names": false})'; 9 | savepoint a; 10 | 11 | select jsonb_pretty( 12 | graphql.resolve($$ 13 | { 14 | __type(name: "account") { 15 | fields { 16 | name 17 | } 18 | } 19 | } 20 | $$) 21 | ); 22 | 23 | -- Inflection off, Overrides: on 24 | comment on column account.id is e'@graphql({"name": "IddD"})'; 25 | comment on column account.name_with_underscore is e'@graphql({"name": "nAMe"})'; 26 | select jsonb_pretty( 27 | graphql.resolve($$ 28 | { 29 | __type(name: "account") { 30 | fields { 31 | name 32 | } 33 | } 34 | } 35 | $$) 36 | ); 37 | 38 | rollback to savepoint a; 39 | 40 | -- Inflection on, Overrides: off 41 | comment on schema public is e'@graphql({"inflect_names": true})'; 42 | select jsonb_pretty( 43 | graphql.resolve($$ 44 | { 45 | __type(name: "Account") { 46 | fields { 47 | name 48 | } 49 | } 50 | } 51 | $$) 52 | ); 53 | 54 | -- Inflection on, Overrides: on 55 | comment on column account.id is e'@graphql({"name": "IddD"})'; 56 | comment on column account.name_with_underscore is e'@graphql({"name": "nAMe"})'; 57 | select jsonb_pretty( 58 | graphql.resolve($$ 59 | { 60 | __type(name: "Account") { 61 | fields { 62 | name 63 | } 64 | } 65 | } 66 | $$) 67 | ); 68 | 69 | rollback; 70 | -------------------------------------------------------------------------------- /test/sql/inflection_function.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account ( 3 | id int primary key 4 | ); 5 | 6 | create function _full_name(rec public.account) 7 | returns text 8 | immutable 9 | strict 10 | language sql 11 | as $$ 12 | select 'Foo'; 13 | $$; 14 | 15 | -- Inflection off, Overrides: off 16 | comment on schema public is e'@graphql({"inflect_names": false})'; 17 | select jsonb_pretty( 18 | graphql.resolve($$ 19 | { 20 | __type(name: "account") { 21 | fields { 22 | name 23 | } 24 | } 25 | } 26 | $$) 27 | ); 28 | 29 | savepoint a; 30 | 31 | -- Inflection off, Overrides: on 32 | comment on function public._full_name(public.account) is E'@graphql({"name": "wholeName"})'; 33 | select jsonb_pretty( 34 | graphql.resolve($$ 35 | { 36 | __type(name: "account") { 37 | fields { 38 | name 39 | } 40 | } 41 | } 42 | $$) 43 | ); 44 | 45 | rollback to savepoint a; 46 | 47 | -- Inflection on, Overrides: off 48 | comment on schema public is e'@graphql({"inflect_names": true})'; 49 | select jsonb_pretty( 50 | graphql.resolve($$ 51 | { 52 | __type(name: "Account") { 53 | fields { 54 | name 55 | } 56 | } 57 | } 58 | $$) 59 | ); 60 | 61 | -- Inflection on, Overrides: on 62 | comment on function public._full_name(public.account) is E'@graphql({"name": "WholeName"})'; 63 | select jsonb_pretty( 64 | graphql.resolve($$ 65 | { 66 | __type(name: "Account") { 67 | fields { 68 | name 69 | } 70 | } 71 | } 72 | $$) 73 | ); 74 | 75 | rollback; 76 | -------------------------------------------------------------------------------- /test/sql/inflection_types.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table blog_post( 3 | id int primary key, 4 | author_id int 5 | ); 6 | 7 | savepoint a; 8 | 9 | -- Inflection off, Overrides: off 10 | comment on schema public is e'@graphql({"inflect_names": false})'; 11 | 12 | select jsonb_pretty( 13 | jsonb_path_query( 14 | graphql.resolve($$ 15 | query IntrospectionQuery { 16 | __schema { 17 | types { 18 | name 19 | } 20 | } 21 | } 22 | $$), 23 | '$.data.__schema.types[*].name ? (@ starts with "blog")' 24 | ) 25 | ); 26 | 27 | -- Inflection off, Overrides: on 28 | comment on table blog_post is e'@graphql({"name": "BlogZZZ"})'; 29 | select jsonb_pretty( 30 | jsonb_path_query( 31 | graphql.resolve($$ 32 | query IntrospectionQuery { 33 | __schema { 34 | types { 35 | name 36 | } 37 | } 38 | } 39 | $$), 40 | '$.data.__schema.types[*].name ? (@ starts with "Blog")' 41 | ) 42 | ); 43 | 44 | rollback to savepoint a; 45 | 46 | -- Inflection on, Overrides: off 47 | comment on schema public is e'@graphql({"inflect_names": true})'; 48 | select jsonb_pretty( 49 | jsonb_path_query( 50 | graphql.resolve($$ 51 | query IntrospectionQuery { 52 | __schema { 53 | types { 54 | name 55 | } 56 | } 57 | } 58 | $$), 59 | '$.data.__schema.types[*].name ? (@ starts with "Blog")' 60 | ) 61 | ); 62 | 63 | -- Inflection on, Overrides: on 64 | comment on table blog_post is e'@graphql({"name": "BlogZZZ"})'; 65 | select jsonb_pretty( 66 | jsonb_path_query( 67 | graphql.resolve($$ 68 | query IntrospectionQuery { 69 | __schema { 70 | types { 71 | name 72 | } 73 | } 74 | } 75 | $$), 76 | '$.data.__schema.types[*].name ? (@ starts with "Blog")' 77 | ) 78 | ); 79 | 80 | rollback; 81 | -------------------------------------------------------------------------------- /test/sql/issue_163.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table profiles( 3 | id int primary key, 4 | username text 5 | ); 6 | 7 | insert into public.profiles(id, username) 8 | values 9 | (1, 'foo'); 10 | 11 | select jsonb_pretty( 12 | graphql.resolve($$ 13 | query MyQuery { 14 | __typename 15 | profilesCollection { 16 | edges { 17 | node { 18 | id 19 | username 20 | } 21 | } 22 | } 23 | } 24 | $$) 25 | ) 26 | 27 | rollback; 28 | -------------------------------------------------------------------------------- /test/sql/issue_170.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | 6 | insert into public.account(id) 7 | select * from generate_series(1,5); 8 | 9 | -- hasPreviousPage is true when `after` is first element of collection 10 | -- "WzFd" is id=1 11 | -- because result set does not include the record id = 1 12 | 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | accountCollection(first: 2, after: "WzFd") { 17 | pageInfo{ 18 | hasPreviousPage 19 | } 20 | } 21 | } 22 | $$) 23 | ); 24 | 25 | -- hasPreviousPage is false when `after` is before the first element of collection 26 | -- "WzFd" is id=0 27 | select jsonb_pretty( 28 | graphql.resolve($$ 29 | { 30 | accountCollection(first: 2, after: "WzBd") { 31 | pageInfo{ 32 | hasPreviousPage 33 | } 34 | } 35 | } 36 | $$) 37 | ); 38 | rollback; 39 | -------------------------------------------------------------------------------- /test/sql/issue_225.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | -- https://github.com/supabase/pg_graphql/issues/225 3 | 4 | create table post( 5 | id int primary key, 6 | title text 7 | ); 8 | 9 | insert into public.post(id, title) 10 | select x.id, (10-x.id)::text from generate_series(1,3) x(id); 11 | 12 | select jsonb_pretty( 13 | graphql.resolve($$ 14 | { 15 | postCollection( orderBy: [{id: DescNullsFirst, title: null}]) { 16 | edges { 17 | node { 18 | id 19 | title 20 | } 21 | } 22 | } 23 | } 24 | $$) 25 | ); 26 | 27 | select jsonb_pretty( 28 | graphql.resolve($$ 29 | { 30 | postCollection( orderBy: [{id: null, title: DescNullsLast}]) { 31 | edges { 32 | node { 33 | id 34 | title 35 | } 36 | } 37 | } 38 | } 39 | $$) 40 | ); 41 | 42 | select jsonb_pretty( 43 | graphql.resolve($$ 44 | { 45 | postCollection( orderBy: [{id: null}, { title: DescNullsLast}]) { 46 | edges { 47 | node { 48 | id 49 | title 50 | } 51 | } 52 | } 53 | } 54 | $$) 55 | ); 56 | 57 | 58 | rollback; 59 | -------------------------------------------------------------------------------- /test/sql/issue_300.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | -- https://github.com/supabase/pg_graphql/issues/300 3 | create role api; 4 | 5 | create table project ( 6 | id serial primary key, 7 | title text not null, 8 | created_at int not null default '1', 9 | updated_at int not null default '2' 10 | ); 11 | 12 | grant usage on schema graphql to api; 13 | grant usage on all sequences in schema public to api; 14 | 15 | revoke all on table project from api; 16 | grant select on table project to api; 17 | grant insert (id, title) on table project to api; 18 | grant update (title) on table project to api; 19 | grant delete on table project to api; 20 | 21 | set role to 'api'; 22 | 23 | select jsonb_pretty( 24 | graphql.resolve($$ 25 | 26 | mutation CreateProject { 27 | insertIntoProjectCollection(objects: [ 28 | {title: "foo"} 29 | ]) { 30 | affectedCount 31 | records { 32 | id 33 | title 34 | createdAt 35 | updatedAt 36 | } 37 | } 38 | } 39 | $$ 40 | ) 41 | ); 42 | 43 | rollback; 44 | -------------------------------------------------------------------------------- /test/sql/issue_306.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key, 4 | is_verified bool 5 | ); 6 | 7 | insert into account(id) select generate_series(1, 10); 8 | 9 | -- Forward pagination 10 | -- hasPreviousPage is false, hasNextPage is true 11 | select jsonb_pretty( 12 | graphql.resolve($$ 13 | { 14 | accountCollection(first: 3) { 15 | pageInfo { 16 | hasNextPage 17 | hasPreviousPage 18 | startCursor 19 | endCursor 20 | } 21 | edges { 22 | cursor 23 | node { 24 | id 25 | } 26 | } 27 | } 28 | } 29 | $$) 30 | ); 31 | 32 | -- hasPreviousPage is true, hasNextPage is true 33 | select jsonb_pretty( 34 | graphql.resolve($$ 35 | { 36 | accountCollection(first: 3, after: "WzNd" ) { 37 | pageInfo { 38 | hasNextPage 39 | hasPreviousPage 40 | startCursor 41 | endCursor 42 | } 43 | edges { 44 | cursor 45 | node { 46 | id 47 | } 48 | } 49 | } 50 | } 51 | $$) 52 | ); 53 | 54 | -- hasPreviousPage is false, hasNextPage is true 55 | select jsonb_pretty( 56 | graphql.resolve($$ 57 | { 58 | accountCollection(last: 3, before: "WzRd" ) { 59 | pageInfo { 60 | hasNextPage 61 | hasPreviousPage 62 | startCursor 63 | endCursor 64 | } 65 | edges { 66 | cursor 67 | node { 68 | id 69 | } 70 | } 71 | } 72 | } 73 | $$) 74 | ); 75 | 76 | 77 | -- hasPreviousPage is true, hasNextPage is true 78 | select jsonb_pretty( 79 | graphql.resolve($$ 80 | { 81 | accountCollection(last: 2, before: "WzRd" ) { 82 | pageInfo { 83 | hasNextPage 84 | hasPreviousPage 85 | startCursor 86 | endCursor 87 | } 88 | edges { 89 | cursor 90 | node { 91 | id 92 | } 93 | } 94 | } 95 | } 96 | $$) 97 | ); 98 | 99 | rollback; 100 | -------------------------------------------------------------------------------- /test/sql/issue_312.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create type sub_status as enum ('invited', 'not_invited'); 4 | alter type sub_status add value if not exists 'opened' after 'invited'; 5 | 6 | create table account( 7 | id int primary key, 8 | ss sub_status 9 | ); 10 | 11 | insert into public.account(id) 12 | select * from generate_series(1,5); 13 | 14 | select jsonb_pretty( 15 | graphql.resolve($$ 16 | { 17 | accountCollection(first: 1) { 18 | edges { 19 | node { 20 | id 21 | ss 22 | } 23 | } 24 | } 25 | } 26 | $$) 27 | ); 28 | 29 | rollback; 30 | -------------------------------------------------------------------------------- /test/sql/issue_334_ambiguous_function.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table public.recipe_ingredient( 4 | id int primary key 5 | ); 6 | 7 | create table public.recipe( 8 | id int primary key 9 | ); 10 | 11 | insert into public.recipe(id) values (1); 12 | 13 | create or replace function _calories(rec public.recipe_ingredient) 14 | returns smallint 15 | stable 16 | language sql 17 | as $$ 18 | select 1; 19 | $$; 20 | 21 | create or replace function _calories(rec public.recipe) 22 | returns smallint 23 | stable 24 | language sql 25 | as $$ 26 | select 1; 27 | $$; 28 | 29 | select jsonb_pretty( 30 | graphql.resolve($$ 31 | { 32 | recipeCollection { 33 | edges { 34 | node { 35 | id 36 | calories 37 | } 38 | } 39 | } 40 | } 41 | $$) 42 | ); 43 | 44 | 45 | 46 | 47 | 48 | rollback; 49 | -------------------------------------------------------------------------------- /test/sql/issue_339_function_return_json.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.account( 3 | id int primary key 4 | ); 5 | 6 | create function public._computed(rec public.account) 7 | returns json 8 | immutable 9 | strict 10 | language sql 11 | as $$ 12 | select jsonb_build_object('hello', 'world'); 13 | $$; 14 | 15 | insert into account(id) values (1); 16 | 17 | select jsonb_pretty( 18 | graphql.resolve($$ 19 | { 20 | accountCollection { 21 | edges { 22 | node { 23 | id 24 | computed 25 | } 26 | } 27 | } 28 | } 29 | $$) 30 | ); 31 | 32 | rollback; 33 | -------------------------------------------------------------------------------- /test/sql/issue_339_function_return_table.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.account( 3 | id int primary key 4 | ); 5 | 6 | -- appears in pg_catalog as returning a set of int 7 | create function public._computed(rec public.account) 8 | returns table ( id int ) 9 | immutable 10 | strict 11 | language sql 12 | as $$ 13 | select 2 as id; 14 | $$; 15 | 16 | -- appears in pg_catalog as returning a set of pseudotype "record" 17 | create function public._computed2(rec public.account) 18 | returns table ( id int, name text ) 19 | immutable 20 | strict 21 | language sql 22 | as $$ 23 | select 2 as id, 'abc' as name; 24 | $$; 25 | 26 | insert into account(id) values (1); 27 | 28 | -- neither computed nor computed2 should be present 29 | select jsonb_pretty( 30 | graphql.resolve($$ 31 | { 32 | __type(name: "Account") { 33 | kind 34 | fields { 35 | name 36 | } 37 | } 38 | } 39 | $$) 40 | ); 41 | 42 | rollback; 43 | -------------------------------------------------------------------------------- /test/sql/issue_370_citext_as_string.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | -- https://github.com/supabase/pg_graphql/issues/370 3 | -- citext is common enough that we should handle treating it as a string 4 | create extension citext; 5 | 6 | create table account( 7 | id int primary key, 8 | email citext 9 | ); 10 | 11 | insert into public.account(id, email) 12 | values (1, 'aBc'), (2, 'def'); 13 | 14 | select jsonb_pretty( 15 | graphql.resolve($$ 16 | { 17 | __type(name: "Account") { 18 | kind 19 | fields { 20 | name type { kind name ofType { name } } 21 | } 22 | } 23 | } 24 | $$) 25 | ); 26 | 27 | select jsonb_pretty( 28 | graphql.resolve($$ 29 | { 30 | accountCollection( filter: {email: {in: ["abc"]}}) { 31 | edges { 32 | node { 33 | id 34 | email 35 | } 36 | } 37 | } 38 | } 39 | $$) 40 | ); 41 | 42 | 43 | rollback; 44 | -------------------------------------------------------------------------------- /test/sql/issue_373.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table "Account"( 4 | id serial primary key, 5 | name text not null 6 | ); 7 | 8 | create table "EmailAddress"( 9 | id serial primary key, 10 | "accountId" int not null references "Account"(id), 11 | "isPrimary" bool not null, 12 | address text not null 13 | ); 14 | 15 | select jsonb_pretty( 16 | graphql.resolve($$ 17 | { 18 | __type(name: "EmailAddress") { 19 | kind 20 | fields { 21 | name type { kind name ofType { name } } 22 | } 23 | } 24 | } 25 | $$) 26 | ); 27 | 28 | rollback; 29 | -------------------------------------------------------------------------------- /test/sql/issue_377_enum_search_path.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": false})'; 3 | 4 | create schema salt; 5 | create type salt.encr as enum ('variant'); 6 | 7 | 8 | create table public.sample( 9 | id int primary key, 10 | val salt.encr 11 | ); 12 | 13 | -- encr should not be visible 14 | select jsonb_pretty( 15 | graphql.resolve($$ 16 | { 17 | __type(name: "encr") { 18 | name 19 | } 20 | } 21 | $$) 22 | ); 23 | 24 | -- the `val` column should have opaque type since `encr` not on search path 25 | select jsonb_pretty( 26 | graphql.resolve($$ 27 | { 28 | __type(name: "sample") { 29 | kind 30 | name 31 | fields { 32 | name 33 | type { 34 | name 35 | ofType { 36 | kind 37 | name 38 | } 39 | } 40 | } 41 | } 42 | } 43 | $$) 44 | ); 45 | 46 | -- Adding it to the search path adds `encr` to the schema 47 | set local search_path = public,salt; 48 | 49 | -- encr now visible 50 | select jsonb_pretty( 51 | graphql.resolve($$ 52 | { 53 | __type(name: "encr") { 54 | kind 55 | name 56 | enumValues { 57 | name 58 | } 59 | } 60 | } 61 | $$) 62 | ); 63 | 64 | -- A table referencing encr references it vs opaque 65 | select jsonb_pretty( 66 | graphql.resolve($$ 67 | { 68 | __type(name: "sample") { 69 | kind 70 | name 71 | fields { 72 | name 73 | type { 74 | name 75 | kind 76 | } 77 | } 78 | } 79 | } 80 | $$) 81 | ); 82 | 83 | rollback; 84 | -------------------------------------------------------------------------------- /test/sql/issue_444.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create function "someFunc" (arg uuid) 4 | returns int 5 | immutable 6 | language sql 7 | as $$ select 1; $$; 8 | 9 | select jsonb_pretty( 10 | graphql.resolve($$ 11 | { 12 | __type(name: "Query") { 13 | fields(includeDeprecated: true) { 14 | name 15 | args { 16 | name 17 | type { 18 | kind 19 | name 20 | ofType { 21 | kind 22 | name 23 | } 24 | } 25 | } 26 | } 27 | 28 | } 29 | } 30 | $$) 31 | ); 32 | 33 | rollback; 34 | -------------------------------------------------------------------------------- /test/sql/issue_463.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table foo( 4 | id int primary key 5 | ); 6 | 7 | insert into foo (id) values (1); 8 | 9 | create or replace function bar(foo) 10 | returns int[] 11 | language sql 12 | stable 13 | as $$ 14 | select array[1, 2, 3]::int[]; 15 | $$; 16 | 17 | select graphql.resolve($$ 18 | query { 19 | fooCollection { 20 | edges { 21 | node { 22 | id 23 | bar 24 | } 25 | } 26 | } 27 | } 28 | $$ 29 | ); 30 | 31 | select jsonb_pretty( 32 | graphql.resolve($$ 33 | { 34 | __type(name: "Foo") { 35 | kind 36 | fields { 37 | name 38 | type { 39 | kind 40 | name 41 | ofType { 42 | kind 43 | name 44 | } 45 | } 46 | } 47 | } 48 | } 49 | $$) 50 | ); 51 | 52 | 53 | rollback; 54 | -------------------------------------------------------------------------------- /test/sql/issue_511.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table users( 4 | id uuid primary key, 5 | email character varying (255), 6 | phone text 7 | ); 8 | 9 | insert into public.users(id, email, phone) 10 | values 11 | ('dd5add8a-7dd2-4495-bc1a-a1dfe95ef23a', 'a@b.com', '987654321'), 12 | ('12e684cc-b7c2-492e-8554-9ab3e03fa37f', null, null), 13 | ('748a81bb-5f71-4dc8-88d9-efe9f03a14b8', null, '123456789'); 14 | 15 | create or replace view users_with_phone as select 16 | id, 17 | email, 18 | phone 19 | from public.users 20 | where phone is not null; 21 | 22 | create table polls( 23 | id uuid primary key, 24 | user_id uuid references users(id) 25 | ); 26 | 27 | insert into public.polls(id, user_id) 28 | values 29 | ('98813159-4814-42fa-911d-5cc900bd80b8', 'dd5add8a-7dd2-4495-bc1a-a1dfe95ef23a'), 30 | ('07dc0104-3811-4a5d-8887-4018c8116e5c', '12e684cc-b7c2-492e-8554-9ab3e03fa37f'), 31 | ('3bec2818-7bea-40ba-81fa-7d0ba061c3de', '748a81bb-5f71-4dc8-88d9-efe9f03a14b8'); 32 | 33 | create 34 | or replace function author (rec polls) returns users_with_phone stable strict language sql security definer 35 | set 36 | search_path = public as $$ 37 | select 38 | * 39 | from users_with_phone u 40 | where u.id = $1.user_id; 41 | $$; 42 | 43 | select jsonb_pretty( 44 | graphql.resolve($$ 45 | { 46 | pollsCollection { 47 | edges { 48 | node { 49 | author { 50 | id, 51 | email, 52 | phone 53 | } 54 | } 55 | } 56 | } 57 | } 58 | $$) 59 | ); 60 | 61 | rollback; 62 | -------------------------------------------------------------------------------- /test/sql/issue_542_partial_unique.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table public.works( 4 | work_id int primary key 5 | ); 6 | 7 | create table public.readthroughs ( 8 | readthrough_id int primary key, 9 | work_id int not null references public.works(work_id), 10 | status text not null 11 | ); 12 | 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | __type(name: "Works") { 17 | kind 18 | fields { 19 | name 20 | type { 21 | kind 22 | name 23 | } 24 | } 25 | } 26 | } 27 | $$) 28 | ); 29 | 30 | /* Creating partial unique referencing status should NOT change the relationship with 31 | the readthroughs to a 1:1 because its partial and other statuses may 32 | have multiple associated readthroughs */ 33 | create unique index idx_unique_in_progress_readthrough 34 | on public.readthroughs (work_id) 35 | where status in ('in_progress'); 36 | 37 | select jsonb_pretty( 38 | graphql.resolve($$ 39 | { 40 | __type(name: "Works") { 41 | kind 42 | fields { 43 | name 44 | type { 45 | kind 46 | name 47 | } 48 | } 49 | } 50 | } 51 | $$) 52 | ); 53 | 54 | rollback; 55 | -------------------------------------------------------------------------------- /test/sql/issue_557_1_to_1_nullability.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | -- Create the party table 4 | create table party ( 5 | id uuid primary key default gen_random_uuid(), 6 | kind varchar not null -- Indicates whether the party is a 'contact' or an 'organisation' 7 | ); 8 | 9 | -- Create the contact table 10 | create table contact ( 11 | id uuid primary key, -- Also a foreign key to party.id 12 | given_name text, 13 | family_name text, 14 | foreign key (id) references party(id) 15 | ); 16 | 17 | -- Create the organisation table 18 | create table organization ( 19 | id uuid primary key, -- Also a foreign key to party.id 20 | name text not null, 21 | foreign key (id) references party(id) 22 | ); 23 | 24 | -- Party should have nullable relationships to Contact and Organization 25 | select jsonb_pretty( 26 | graphql.resolve($$ 27 | { 28 | __type(name: "Party") { 29 | kind 30 | fields { 31 | name 32 | type { 33 | name 34 | kind 35 | description 36 | ofType { 37 | name 38 | kind 39 | description 40 | } 41 | 42 | } 43 | } 44 | } 45 | } 46 | $$) 47 | ); 48 | 49 | -- Contact and Organization should have non-nullable relationship to Party 50 | select jsonb_pretty( 51 | graphql.resolve($$ 52 | { 53 | __type(name: "Organization") { 54 | kind 55 | fields { 56 | name 57 | description 58 | type { 59 | name 60 | kind 61 | description 62 | ofType { 63 | name 64 | kind 65 | description 66 | } 67 | } 68 | } 69 | } 70 | } 71 | $$) 72 | ); 73 | 74 | select jsonb_pretty( 75 | graphql.resolve($$ 76 | { 77 | __type(name: "Contact") { 78 | kind 79 | fields { 80 | name 81 | description 82 | type { 83 | name 84 | kind 85 | description 86 | ofType { 87 | name 88 | kind 89 | description 90 | } 91 | } 92 | } 93 | } 94 | } 95 | $$) 96 | ); 97 | 98 | rollback; 99 | -------------------------------------------------------------------------------- /test/sql/issue_581_missing_desc_on_schema.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | select graphql.resolve($$ 4 | query IntrospectionQuery { 5 | __schema { 6 | description 7 | queryType { name } 8 | mutationType { name } 9 | subscriptionType { name } 10 | types { 11 | ...FullType 12 | } 13 | directives { 14 | name 15 | description 16 | 17 | locations 18 | args(includeDeprecated: true) { 19 | ...InputValue 20 | } 21 | } 22 | } 23 | } 24 | 25 | fragment FullType on __Type { 26 | kind 27 | name 28 | description 29 | 30 | fields(includeDeprecated: true) { 31 | name 32 | description 33 | args(includeDeprecated: true) { 34 | ...InputValue 35 | } 36 | type { 37 | ...TypeRef 38 | } 39 | isDeprecated 40 | deprecationReason 41 | } 42 | inputFields(includeDeprecated: true) { 43 | ...InputValue 44 | } 45 | interfaces { 46 | ...TypeRef 47 | } 48 | enumValues(includeDeprecated: true) { 49 | name 50 | description 51 | isDeprecated 52 | deprecationReason 53 | } 54 | possibleTypes { 55 | ...TypeRef 56 | } 57 | } 58 | 59 | fragment InputValue on __InputValue { 60 | name 61 | description 62 | type { ...TypeRef } 63 | defaultValue 64 | isDeprecated 65 | deprecationReason 66 | } 67 | 68 | fragment TypeRef on __Type { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | ofType { 87 | kind 88 | name 89 | ofType { 90 | kind 91 | name 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | $$, NULL, 'IntrospectionQuery'); 101 | 102 | rollback; 103 | -------------------------------------------------------------------------------- /test/sql/json_is_stringified.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog_post( 4 | id int primary key, 5 | data jsonb, 6 | parent_id int references blog_post(id) 7 | ); 8 | 9 | select graphql.resolve($$ 10 | mutation { 11 | insertIntoBlogPostCollection(objects: [{ 12 | id: 1 13 | data: "{\"key\": \"value\"}" 14 | parentId: 1 15 | }]) { 16 | records { 17 | id 18 | data 19 | } 20 | } 21 | } 22 | $$); 23 | 24 | select * from blog_post; 25 | 26 | select graphql.resolve($$ 27 | mutation { 28 | updateBlogPostCollection(set: { 29 | data: "{\"key\": \"value2\"}" 30 | }) { 31 | records { 32 | id 33 | data 34 | } 35 | } 36 | } 37 | $$); 38 | 39 | select * from blog_post; 40 | 41 | select graphql.resolve($$ 42 | { 43 | blogPostCollection { 44 | edges { 45 | node { 46 | data 47 | parent { 48 | id 49 | data 50 | } 51 | } 52 | } 53 | } 54 | } 55 | $$); 56 | 57 | select graphql.resolve($$ 58 | mutation { 59 | deleteFromBlogPostCollection { 60 | records { 61 | id 62 | data 63 | } 64 | } 65 | } 66 | $$); 67 | 68 | rollback; 69 | -------------------------------------------------------------------------------- /test/sql/max_page_size.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key 5 | ); 6 | 7 | insert into account(id) 8 | select * from generate_series(1, 40); 9 | 10 | -- Requested 50, expect 30 11 | select jsonb_pretty( 12 | graphql.resolve($$ 13 | { 14 | accountCollection(first: 50) { 15 | edges { 16 | node { 17 | id 18 | } 19 | } 20 | } 21 | } 22 | $$) 23 | ); 24 | 25 | rollback; 26 | -------------------------------------------------------------------------------- /test/sql/max_rows_directive.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | 6 | insert into public.account(id) 7 | select * from generate_series(1, 100); 8 | 9 | -- expect default 30 rows on first page 10 | select graphql.resolve($$ 11 | { 12 | accountCollection 13 | { 14 | edges { 15 | node { 16 | id 17 | } 18 | } 19 | } 20 | } 21 | $$); 22 | 23 | comment on schema public is e'@graphql({"max_rows": 5})'; 24 | 25 | -- expect 5 rows on first page 26 | select graphql.resolve($$ 27 | { 28 | accountCollection 29 | { 30 | edges { 31 | node { 32 | id 33 | } 34 | } 35 | } 36 | } 37 | $$); 38 | 39 | comment on schema public is e'@graphql({"max_rows": 40})'; 40 | 41 | -- expect 40 rows on first page 42 | select graphql.resolve($$ 43 | { 44 | accountCollection 45 | { 46 | edges { 47 | node { 48 | id 49 | } 50 | } 51 | } 52 | } 53 | $$); 54 | 55 | rollback; 56 | -------------------------------------------------------------------------------- /test/sql/multi_column_primary_key.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table person( 3 | id int primary key, 4 | name text 5 | ); 6 | 7 | create table address( 8 | id int primary key, 9 | city text 10 | ); 11 | 12 | create table person_at_address( 13 | person_id int not null references person (id), 14 | address_id int not null references address (id), 15 | primary key (person_id, address_id) 16 | ); 17 | 18 | insert into public.person(id, name) 19 | values 20 | (1, 'foo'), 21 | (2, 'bar'), 22 | (3, 'baz'); 23 | 24 | insert into public.address(id, city) 25 | values 26 | (4, 'Chicago'), 27 | (5, 'Atlanta'), 28 | (6, 'Portland'); 29 | 30 | insert into public.person_at_address(person_id, address_id) 31 | values 32 | (1, 4), 33 | (2, 4), 34 | (3, 6); 35 | 36 | savepoint a; 37 | 38 | select jsonb_pretty( 39 | graphql.resolve($$ 40 | { 41 | personAtAddressCollection { 42 | edges { 43 | cursor 44 | node { 45 | nodeId 46 | personId 47 | addressId 48 | person { 49 | name 50 | } 51 | address { 52 | city 53 | } 54 | } 55 | } 56 | } 57 | } 58 | $$) 59 | ); 60 | rollback to savepoint a; 61 | 62 | select jsonb_pretty( 63 | graphql.resolve($$ 64 | { 65 | personAtAddressCollection( 66 | first: 1, 67 | after: "WzEsIDRd" 68 | ) { 69 | edges { 70 | node { 71 | personId 72 | addressId 73 | nodeId 74 | } 75 | } 76 | } 77 | } 78 | $$) 79 | ); 80 | rollback to savepoint a; 81 | 82 | 83 | select jsonb_pretty( 84 | graphql.resolve($$ 85 | { 86 | node(nodeId: "WyJwdWJsaWMiLCAicGVyc29uX2F0X2FkZHJlc3MiLCAxLCA0XQ==") { 87 | nodeId 88 | ... on PersonAtAddress { 89 | nodeId 90 | personId 91 | person { 92 | name 93 | personAtAddressCollection { 94 | edges { 95 | node { 96 | addressId 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | $$) 105 | ); 106 | 107 | rollback; 108 | -------------------------------------------------------------------------------- /test/sql/multiple_mutations.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null, 6 | priority int 7 | ); 8 | 9 | savepoint a; 10 | 11 | -- Scenario: Two Mutations, both are vaild 12 | select jsonb_pretty(graphql.resolve($$ 13 | mutation { 14 | firstInsert: insertIntoAccountCollection(objects: [ 15 | { email: "foo@barsley.com", priority: 1 } 16 | ]) { 17 | affectedCount 18 | records { 19 | id 20 | email 21 | } 22 | } 23 | 24 | secondInsert: insertIntoAccountCollection(objects: [ 25 | { email: "bar@foosworth.com" } 26 | ]) { 27 | affectedCount 28 | records { 29 | id 30 | email 31 | } 32 | } 33 | } 34 | $$)); 35 | 36 | select * from account; 37 | 38 | rollback to savepoint a; 39 | 40 | -- Scenario: Two Mutations, first one fails. Expect total rollback 41 | select jsonb_pretty(graphql.resolve($$ 42 | mutation { 43 | firstInsert: insertIntoAccountCollection(objects: [ 44 | { email: "foo@barsley.com", invalidKey: 1 } 45 | ]) { 46 | records { 47 | id 48 | email 49 | } 50 | } 51 | 52 | secondInsert: insertIntoAccountCollection(objects: [ 53 | { email: "bar@foosworth.com" } 54 | ]) { 55 | records { 56 | id 57 | email 58 | } 59 | } 60 | } 61 | $$)); 62 | 63 | select * from account; 64 | 65 | rollback to savepoint a; 66 | 67 | -- Scenario: Two Mutations, second one fails. Expect total rollback 68 | select jsonb_pretty(graphql.resolve($$ 69 | mutation { 70 | secondInsert: insertIntoAccountCollection(objects: [ 71 | { email: "bar@foosworth.com" } 72 | ]) { 73 | records { 74 | id 75 | email 76 | } 77 | } 78 | 79 | firstInsert: insertIntoAccountCollection(objects: [ 80 | { email: "foo@barsley.com", invalidKey: 1 } 81 | ]) { 82 | records { 83 | id 84 | email 85 | } 86 | } 87 | } 88 | $$)); 89 | 90 | select * from account; 91 | 92 | rollback to savepoint a; 93 | 94 | rollback 95 | -------------------------------------------------------------------------------- /test/sql/multiple_queries.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null, 6 | priority int 7 | ); 8 | 9 | insert into account(email) 10 | values ('email_1'), ('email_2'); 11 | 12 | -- Scenario: Two queries 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | forward: accountCollection(orderBy: [{id: AscNullsFirst}]) { 17 | edges { 18 | node { 19 | id 20 | } 21 | } 22 | } 23 | backward: accountCollection(orderBy: [{id: DescNullsFirst}]) { 24 | edges { 25 | node { 26 | id 27 | } 28 | } 29 | } 30 | } 31 | $$) 32 | ); 33 | 34 | rollback 35 | -------------------------------------------------------------------------------- /test/sql/mutation_delete_variable.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null 6 | ); 7 | 8 | insert into public.account(email) 9 | values 10 | ('aardvark@x.com'), 11 | ('bat@x.com'); 12 | 13 | savepoint a; 14 | 15 | -- variable filter value 16 | select graphql.resolve($$ 17 | mutation DeleteAccountByEmail($email: String!) { 18 | deleteFromAccountCollection( 19 | filter: { 20 | email: {eq: $email} 21 | } 22 | atMost: 1 23 | ) { 24 | records { id } 25 | } 26 | } 27 | $$, '{"email": "bat@x.com"}'); 28 | 29 | rollback to savepoint a; 30 | 31 | -- variable entire filter 32 | select graphql.resolve($$ 33 | mutation DeleteAccountByFilter($afilt: AccountFilter!) { 34 | deleteFromAccountCollection( 35 | filter: $afilt 36 | atMost: 1 37 | ) { 38 | records { id } 39 | } 40 | } 41 | $$, 42 | variables:= '{"afilt": {"id": {"eq": 1}} }' 43 | ); 44 | rollback to savepoint a; 45 | 46 | -- variable atMost. should impact too many 47 | select graphql.resolve($$ 48 | mutation SafeDeleteAccount($atMost: Int!) { 49 | deleteFromAccountCollection( 50 | filter: {id: {eq: 1}} 51 | atMost: $atMost 52 | ) { 53 | records { id } 54 | } 55 | } 56 | $$, 57 | variables:= '{"atMost": 0 }' 58 | ); 59 | rollback to savepoint a; 60 | 61 | rollback; 62 | -------------------------------------------------------------------------------- /test/sql/null_argument.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | -- Test that the argument parser can handle null values 3 | 4 | create table account( 5 | id serial primary key, 6 | email text 7 | ); 8 | 9 | select graphql.resolve($$ 10 | mutation { 11 | insertIntoAccountCollection(objects: [ 12 | { email: null } 13 | ]) { 14 | affectedCount 15 | records { 16 | id 17 | email 18 | } 19 | } 20 | } 21 | $$); 22 | 23 | rollback; 24 | -------------------------------------------------------------------------------- /test/sql/omit_exotic_types.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | /* 4 | Composite types are not currently supported as inputs 5 | - confirm composites are not allowed anywhere 6 | */ 7 | 8 | create type complex as (r int, i int); 9 | 10 | create table something( 11 | id serial primary key, 12 | name varchar(255) not null, 13 | tags text[], 14 | comps complex, 15 | js json, 16 | jsb jsonb 17 | ); 18 | 19 | -- Inflection on, Overrides: off 20 | comment on schema public is e'@graphql({"inflect_names": true})'; 21 | select jsonb_pretty( 22 | jsonb_path_query( 23 | graphql.resolve($$ 24 | { 25 | __schema { 26 | types { 27 | name 28 | fields { 29 | name 30 | } 31 | inputFields { 32 | name 33 | } 34 | } 35 | } 36 | } 37 | $$), 38 | '$.data.__schema.types[*] ? (@.name starts with "Something")' 39 | ) 40 | ); 41 | 42 | rollback; 43 | -------------------------------------------------------------------------------- /test/sql/omit_weird_names.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | savepoint a; 4 | 5 | create table "@xyz"( id int primary key); 6 | 7 | select jsonb_pretty( 8 | graphql.resolve($$ 9 | { 10 | __schema { 11 | types { 12 | name 13 | } 14 | } 15 | } 16 | $$) 17 | ); 18 | 19 | rollback to savepoint a; 20 | 21 | create table xyz( "! q" int primary key); 22 | select jsonb_pretty( 23 | graphql.resolve($$ 24 | { 25 | __type(name: "Xyz") { 26 | fields { 27 | name 28 | } 29 | } 30 | } 31 | $$) 32 | ); 33 | 34 | rollback to savepoint a; 35 | 36 | rollback; 37 | -------------------------------------------------------------------------------- /test/sql/override_enum_name.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create type account_priority as enum ('high', 'standard'); 3 | comment on type public.account_priority is E'@graphql({"name": "CustomerValue"})'; 4 | 5 | select jsonb_pretty( 6 | graphql.resolve($$ 7 | { 8 | __type(name: "CustomerValue") { 9 | enumValues { 10 | name 11 | } 12 | } 13 | } 14 | $$) 15 | ); 16 | 17 | rollback; 18 | -------------------------------------------------------------------------------- /test/sql/override_field_name.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | 7 | comment on column public.account.email is E'@graphql({"name": "emailAddress"})'; 8 | 9 | 10 | -- expect: 'emailAddresses' 11 | select jsonb_pretty( 12 | graphql.resolve($$ 13 | { 14 | __type(name: "Account") { 15 | fields { 16 | name 17 | } 18 | } 19 | } 20 | $$) 21 | ); 22 | 23 | rollback; 24 | -------------------------------------------------------------------------------- /test/sql/override_func_field_name.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | first_name varchar(255) not null, 5 | last_name varchar(255) not null 6 | ); 7 | 8 | -- Extend with function 9 | create function _full_name(rec public.account) 10 | returns text 11 | immutable 12 | strict 13 | language sql 14 | as $$ 15 | select format('%s %s', rec.first_name, rec.last_name) 16 | $$; 17 | 18 | comment on function public._full_name(public.account) is E'@graphql({"name": "wholeName"})'; 19 | 20 | -- expect: 'wholeName' 21 | select jsonb_pretty( 22 | graphql.resolve($$ 23 | { 24 | __type(name: "Account") { 25 | fields { 26 | name 27 | } 28 | } 29 | } 30 | $$) 31 | ); 32 | 33 | rollback; 34 | -------------------------------------------------------------------------------- /test/sql/override_relationship_field_name.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key 5 | ); 6 | 7 | create table blog( 8 | id serial primary key, 9 | owner_id integer not null references account(id) 10 | ); 11 | 12 | comment on constraint blog_owner_id_fkey 13 | on blog 14 | is E'@graphql({"foreign_name": "author", "local_name": "blogz"})'; 15 | 16 | 17 | -- expect: 'author' 18 | select jsonb_pretty( 19 | graphql.resolve($$ 20 | { 21 | __type(name: "Blog") { 22 | fields { 23 | name 24 | } 25 | } 26 | } 27 | $$) 28 | ); 29 | 30 | -- expect: 'blogz' 31 | select jsonb_pretty( 32 | graphql.resolve($$ 33 | { 34 | __type(name: "Account") { 35 | fields { 36 | name 37 | } 38 | } 39 | } 40 | $$) 41 | ); 42 | 43 | 44 | rollback; 45 | -------------------------------------------------------------------------------- /test/sql/override_type_name.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id serial primary key, 4 | email varchar(255) not null 5 | ); 6 | 7 | comment on table public.account is E'@graphql({"name": "UserAccount"})'; 8 | 9 | select jsonb_pretty( 10 | jsonb_path_query( 11 | graphql.resolve($$ 12 | query IntrospectionQuery { 13 | __schema { 14 | types { 15 | name 16 | } 17 | } 18 | } 19 | $$), 20 | '$.data.__schema.types[*].name ? (@ starts with "UserAccount")' 21 | ) 22 | ); 23 | 24 | rollback; 25 | -------------------------------------------------------------------------------- /test/sql/permissions_connection_column.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | encrypted_password varchar(255) not null 6 | ); 7 | 8 | insert into public.account(encrypted_password) 9 | values 10 | ('hidden_hash'); 11 | 12 | -- Superuser 13 | select graphql.resolve( 14 | $$ 15 | { 16 | accountCollection(first: 1) { 17 | edges { 18 | node { 19 | id 20 | encryptedPassword 21 | } 22 | } 23 | } 24 | } 25 | $$ 26 | ); 27 | 28 | 29 | create role api; 30 | -- Grant access to GQL 31 | grant usage on schema graphql to api; 32 | grant all on all tables in schema graphql to api; 33 | 34 | -- Allow access to public.account.id but nothing else 35 | grant usage on schema public to api; 36 | grant all on all tables in schema public to api; 37 | revoke select on public.account from api; 38 | grant select (id) on public.account to api; 39 | 40 | set role api; 41 | 42 | -- Select permitted columns 43 | select graphql.resolve( 44 | $$ 45 | { 46 | accountCollection(first: 1) { 47 | edges { 48 | node { 49 | id 50 | } 51 | } 52 | } 53 | } 54 | $$ 55 | ); 56 | 57 | -- Attempt select on revoked column 58 | select graphql.resolve( 59 | $$ 60 | { 61 | accountCollection(first: 1) { 62 | edges { 63 | node { 64 | id 65 | encryptedPassword 66 | } 67 | } 68 | } 69 | } 70 | $$ 71 | ); 72 | rollback; 73 | -------------------------------------------------------------------------------- /test/sql/permissions_functions.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | -- Create a new non-superuser role to manipulate permissions 4 | create role api; 5 | grant usage on schema graphql to api; 6 | 7 | -- Create minimal Query function 8 | create function public.get_one() 9 | returns int 10 | language sql 11 | immutable 12 | as 13 | $$ 14 | select 1; 15 | $$; 16 | 17 | savepoint a; 18 | 19 | -- Use api role 20 | set role api; 21 | 22 | -- Confirm that getOne is visible to the api role 23 | select jsonb_pretty( 24 | graphql.resolve($$ 25 | { 26 | __type(name: "Query") { 27 | fields { 28 | name 29 | } 30 | } 31 | } 32 | $$) 33 | ); 34 | 35 | -- Execute 36 | select jsonb_pretty( 37 | graphql.resolve($$ 38 | { getOne } 39 | $$) 40 | ); 41 | 42 | -- revert to superuser 43 | rollback to savepoint a; 44 | select current_user; 45 | 46 | -- revoke default access from the public role for new functions 47 | -- this is not actually necessary for this test, but including here 48 | -- as a best practice in case this test is used as a reference 49 | alter default privileges revoke execute on functions from public; 50 | -- explicitly revoke execute from api role 51 | revoke execute on function public.get_one from api; 52 | -- api inherits from the public role, so we need to revoke from public too 53 | revoke execute on function public.get_one from public; 54 | 55 | -- Use api role w/o execute permission on get_one 56 | set role api; 57 | 58 | -- confirm we're using the api role 59 | select current_user; 60 | 61 | -- confirm that api can not execute get_one() 62 | select pg_catalog.has_function_privilege('api', 'get_one()', 'execute'); 63 | 64 | -- Confirm getOne is not visible in the Query type 65 | select jsonb_pretty( 66 | graphql.resolve($$ 67 | { 68 | __type(name: "Query") { 69 | fields { 70 | name 71 | } 72 | } 73 | } 74 | $$) 75 | ); 76 | 77 | -- Confirm getOne can not be executed / is not found during resolution 78 | select jsonb_pretty( 79 | graphql.resolve($$ 80 | { getOne } 81 | $$) 82 | ); 83 | 84 | rollback; 85 | -------------------------------------------------------------------------------- /test/sql/permissions_node_column.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | encrypted_password varchar(255) not null, 6 | parent_id int references account(id) 7 | ); 8 | 9 | insert into public.account(encrypted_password, parent_id) 10 | values 11 | ('hidden_hash', 1); 12 | 13 | -- Superuser 14 | select jsonb_pretty( 15 | graphql.resolve($$ 16 | { 17 | accountCollection { 18 | edges { 19 | node { 20 | parent { 21 | encryptedPassword 22 | } 23 | } 24 | } 25 | } 26 | } 27 | $$) 28 | ); 29 | 30 | create role api; 31 | 32 | -- Grant access to GQL 33 | grant usage on schema graphql to api; 34 | 35 | -- Allow access to public.account.id but nothing else 36 | grant usage on schema public to api; 37 | grant all on all tables in schema public to api; 38 | revoke select on public.account from api; 39 | 40 | grant select (id, parent_id) on public.account to api; 41 | 42 | set role api; 43 | 44 | select jsonb_pretty( 45 | graphql.resolve($$ 46 | { 47 | __type(name: "Account") { 48 | kind 49 | fields { 50 | name 51 | } 52 | } 53 | } 54 | $$) 55 | ); 56 | 57 | 58 | 59 | -- Select permitted columns 60 | select jsonb_pretty( 61 | graphql.resolve($$ 62 | { 63 | accountCollection { 64 | edges { 65 | node { 66 | parent { 67 | id 68 | } 69 | } 70 | } 71 | } 72 | } 73 | $$) 74 | ); 75 | 76 | 77 | -- Attempt select on revoked column 78 | select jsonb_pretty( 79 | graphql.resolve($$ 80 | { 81 | accountCollection { 82 | edges { 83 | node { 84 | parent { 85 | encryptedPassword 86 | } 87 | } 88 | } 89 | } 90 | } 91 | $$) 92 | ); 93 | rollback; 94 | -------------------------------------------------------------------------------- /test/sql/permissions_table_level.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | encrypted_password varchar(255) not null, 6 | parent_id int references account(id) 7 | ); 8 | 9 | create role api; 10 | 11 | -- Grant access to GQL 12 | grant usage on schema graphql to api; 13 | grant all on all tables in schema graphql to api; 14 | 15 | -- Allow access to public.account.id but nothing else 16 | grant usage on schema public to api; 17 | grant all on all tables in schema public to api; 18 | 19 | savepoint a; 20 | 21 | -- Nothing is excluded 22 | set role api; 23 | select jsonb_pretty(graphql.resolve(' {__type(name: "Query") { fields { name } } } ') ); 24 | select jsonb_pretty(graphql.resolve(' {__type(name: "Mutation") { fields { name } } } ') ); 25 | rollback to savepoint a; 26 | 27 | -- Revoke Select Excludes: All entity types 28 | revoke select on public.account from api; 29 | set role api; 30 | select jsonb_pretty(graphql.resolve(' {__type(name: "Query") { fields { name } } } ') ); 31 | select jsonb_pretty(graphql.resolve(' {__type(name: "Mutation") { fields { name } } } ') ); 32 | rollback to savepoint a; 33 | 34 | -- Revoke Insert Excludes: CreateNode 35 | revoke insert on public.account from api; 36 | set role api; 37 | select jsonb_pretty(graphql.resolve(' {__type(name: "Query") { fields { name } } } ') ); 38 | select jsonb_pretty(graphql.resolve(' {__type(name: "Mutation") { fields { name } } } ') ); 39 | rollback to savepoint a; 40 | 41 | -- Revoke Update Excludes: UpdateNode 42 | revoke update on public.account from api; 43 | set role api; 44 | select jsonb_pretty(graphql.resolve(' {__type(name: "Query") { fields { name } } } ') ); 45 | select jsonb_pretty(graphql.resolve(' {__type(name: "Mutation") { fields { name } } } ') ); 46 | rollback to savepoint a; 47 | 48 | -- Revoke Delete Excludes: from Mutation schema 49 | revoke delete on public.account from api; 50 | set role api; 51 | select jsonb_pretty(graphql.resolve(' {__type(name: "Query") { fields { name } } } ') ); 52 | select jsonb_pretty(graphql.resolve(' {__type(name: "Mutation") { fields { name } } } ') ); 53 | rollback to savepoint a; 54 | 55 | rollback; 56 | -------------------------------------------------------------------------------- /test/sql/permissions_types.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create schema xyz; 3 | comment on schema xyz is '@graphql({"inflect_names": true})'; 4 | 5 | create type xyz.light as enum ('red'); 6 | 7 | -- expect nothing b/c not in search_path 8 | select jsonb_pretty( 9 | graphql.resolve($$ 10 | { 11 | __type(name: "Light") { 12 | kind 13 | name 14 | } 15 | } 16 | $$) 17 | ); 18 | 19 | set search_path = 'xyz'; 20 | 21 | -- expect 1 record 22 | select jsonb_pretty( 23 | graphql.resolve($$ 24 | { 25 | __type(name: "Light") { 26 | kind 27 | name 28 | } 29 | } 30 | $$) 31 | ); 32 | 33 | revoke all on type xyz.light from public; 34 | 35 | -- Create low priv user without access to xyz.light 36 | create role low_priv; 37 | 38 | -- expected false 39 | select pg_catalog.has_type_privilege( 40 | 'low_priv', 41 | 'xyz.light', 42 | 'USAGE' 43 | ); 44 | 45 | grant usage on schema xyz to low_priv; 46 | grant usage on schema graphql to low_priv; 47 | 48 | set role low_priv; 49 | 50 | -- expect no results b/c low_priv does not have usage permission 51 | select jsonb_pretty( 52 | graphql.resolve($$ 53 | { 54 | __type(name: "Light") { 55 | kind 56 | name 57 | } 58 | } 59 | $$) 60 | ); 61 | 62 | 63 | rollback; 64 | -------------------------------------------------------------------------------- /test/sql/primary_key_is_required.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | savepoint a; 4 | 5 | create table account( 6 | id serial primary key 7 | ); 8 | 9 | -- Should be visible because it has a primary ky 10 | select jsonb_pretty( 11 | graphql.resolve($$ 12 | { 13 | __type(name: "Account") { 14 | name 15 | } 16 | } 17 | $$) 18 | ); 19 | 20 | rollback to savepoint a; 21 | 22 | create table account( 23 | id serial 24 | ); 25 | 26 | -- Should NOT be visible because it does not have a primary ky 27 | select jsonb_pretty( 28 | graphql.resolve($$ 29 | { 30 | __type(name: "Account") { 31 | name 32 | } 33 | } 34 | $$) 35 | ); 36 | 37 | rollback; 38 | -------------------------------------------------------------------------------- /test/sql/relationship_one_to_one.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key 5 | ); 6 | 7 | create table address( 8 | id int primary key, 9 | -- unique constraint makes this a 1:1 relationship 10 | account_id int not null unique references account(id) 11 | ); 12 | 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | __type(name: "Account") { 17 | kind 18 | fields { 19 | name 20 | type { 21 | name 22 | } 23 | } 24 | } 25 | } 26 | $$) 27 | ); 28 | 29 | select jsonb_pretty( 30 | graphql.resolve($$ 31 | { 32 | __type(name: "Address") { 33 | kind 34 | fields { 35 | name 36 | type { 37 | name 38 | kind 39 | ofType { name } 40 | } 41 | } 42 | } 43 | } 44 | $$) 45 | ); 46 | 47 | insert into account(id) select * from generate_series(1, 10); 48 | insert into address(id, account_id) select y.x, y.x from generate_series(1, 10) y(x); 49 | 50 | -- Filter by Int 51 | select jsonb_pretty( 52 | graphql.resolve($$ 53 | { 54 | accountCollection(filter: {id: {eq: 3}}) { 55 | edges { 56 | node { 57 | id 58 | address { 59 | id 60 | account { 61 | id 62 | address { 63 | id 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | $$) 72 | ); 73 | 74 | rollback; 75 | -------------------------------------------------------------------------------- /test/sql/resolve___schema.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null, 6 | encrypted_password varchar(255) not null, 7 | created_at timestamp not null, 8 | updated_at timestamp not null 9 | ); 10 | 11 | 12 | create table blog( 13 | id serial primary key, 14 | owner_id integer not null references account(id), 15 | name varchar(255) not null, 16 | description varchar(255), 17 | created_at timestamp not null, 18 | updated_at timestamp not null 19 | ); 20 | 21 | 22 | create type blog_post_status as enum ('PENDING', 'RELEASED'); 23 | 24 | 25 | create table blog_post( 26 | id uuid not null default gen_random_uuid() primary key, 27 | blog_id integer not null references blog(id), 28 | title varchar(255) not null, 29 | body varchar(10000), 30 | status blog_post_status not null, 31 | created_at timestamp not null, 32 | updated_at timestamp not null 33 | ); 34 | 35 | 36 | select jsonb_pretty( 37 | graphql.resolve($$ 38 | query IntrospectionQuery { 39 | __schema { 40 | queryType { 41 | name 42 | } 43 | mutationType { 44 | name 45 | } 46 | types { 47 | kind 48 | name 49 | } 50 | directives { 51 | name 52 | description 53 | locations 54 | } 55 | } 56 | } 57 | $$) 58 | ); 59 | 60 | 61 | rollback; 62 | -------------------------------------------------------------------------------- /test/sql/resolve___type.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null, 6 | encrypted_password varchar(255) not null, 7 | created_at timestamp not null, 8 | updated_at timestamp not null 9 | ); 10 | 11 | 12 | select jsonb_pretty( 13 | graphql.resolve($$ 14 | { 15 | __type(name: "Account") { 16 | kind 17 | fields { 18 | name 19 | } 20 | } 21 | } 22 | $$) 23 | ); 24 | 25 | select jsonb_pretty( 26 | graphql.resolve($$ 27 | { 28 | __type(name: "DoesNotExist") { 29 | kind 30 | fields { 31 | name 32 | } 33 | } 34 | } 35 | $$) 36 | ); 37 | 38 | rollback; 39 | -------------------------------------------------------------------------------- /test/sql/resolve___typename.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key, 5 | parent_id int references account(id) 6 | ); 7 | 8 | insert into public.account(id, parent_id) 9 | values 10 | (1, 1); 11 | 12 | 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | query Abc { 16 | __typename 17 | accountCollection { 18 | __typename 19 | pageInfo { 20 | __typename 21 | } 22 | edges { 23 | __typename 24 | node { 25 | __typename 26 | parent { 27 | __typename 28 | } 29 | } 30 | } 31 | } 32 | } 33 | $$) 34 | ); 35 | 36 | select jsonb_pretty( 37 | graphql.resolve($$ 38 | mutation Abc { 39 | __typename 40 | insertIntoAccountCollection(objects: [ 41 | { id: 2, parentId: 1 } 42 | ]) { 43 | __typename 44 | records { 45 | __typename 46 | } 47 | } 48 | } 49 | $$) 50 | ); 51 | 52 | select jsonb_pretty( 53 | graphql.resolve($$ 54 | mutation { 55 | updateAccountCollection( 56 | set: { parentId: 1 } 57 | atMost: 100 58 | ) { 59 | __typename 60 | records { 61 | id 62 | __typename 63 | } 64 | } 65 | } 66 | $$) 67 | ); 68 | 69 | select jsonb_pretty( 70 | graphql.resolve($$ 71 | mutation { 72 | deleteFromAccountCollection(atMost: 100) { 73 | __typename 74 | records { 75 | __typename 76 | } 77 | } 78 | } 79 | $$) 80 | ); 81 | 82 | rollback; 83 | -------------------------------------------------------------------------------- /test/sql/resolve_array_type.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table xrr( 4 | id bigserial primary key, 5 | tags text[], 6 | nums int[], 7 | uids uuid[] 8 | ); 9 | 10 | insert into xrr(id, tags, nums) 11 | values (9, array['a', 'b'], array[1, 2]); 12 | 13 | select jsonb_pretty( 14 | graphql.resolve($$ 15 | { 16 | xrrCollection { 17 | edges { 18 | node { 19 | id 20 | tags 21 | } 22 | } 23 | } 24 | } 25 | $$) 26 | ); 27 | 28 | -- Insert 29 | select jsonb_pretty( 30 | graphql.resolve($$ 31 | mutation { 32 | insertIntoXrrCollection(objects: [ 33 | { nums: 1 }, 34 | { tags: "b", nums: null }, 35 | { tags: ["c", "d"], nums: [3, null] }, 36 | ]) { 37 | affectedCount 38 | records { 39 | id 40 | tags 41 | nums 42 | } 43 | } 44 | } 45 | $$)); 46 | 47 | -- Update 48 | select jsonb_pretty( 49 | graphql.resolve($$ 50 | mutation { 51 | updateXrrCollection( 52 | filter: { id: { gte: "8" } }, 53 | set: { tags: "g" } 54 | ) { 55 | affectedCount 56 | records { 57 | id 58 | tags 59 | nums 60 | } 61 | } 62 | } 63 | $$)); 64 | 65 | -- Delete 66 | select jsonb_pretty( 67 | graphql.resolve($$ 68 | mutation { 69 | updateXrrCollection( 70 | filter: { id: { eq: 1 } }, 71 | set: { tags: ["h", null, "i"], uids: [null, "9fb1c8e9-da2a-4072-b9fb-4f277446df9c"] } 72 | ) { 73 | affectedCount 74 | records { 75 | id 76 | tags 77 | nums 78 | uids 79 | } 80 | } 81 | } 82 | $$)); 83 | 84 | select * from xrr; 85 | 86 | rollback; 87 | -------------------------------------------------------------------------------- /test/sql/resolve_connection_named.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table account( 3 | id int primary key 4 | ); 5 | 6 | 7 | insert into public.account(id) 8 | select * from generate_series(1,5); 9 | 10 | 11 | select graphql.resolve( 12 | $$ 13 | query FirstNAccounts($first_: Int!) { 14 | accountCollection(first: $first_) { 15 | edges { 16 | node { 17 | id 18 | } 19 | } 20 | } 21 | } 22 | $$, 23 | '{"first_": 2}'::jsonb 24 | ); 25 | 26 | rollback; 27 | -------------------------------------------------------------------------------- /test/sql/resolve_connection_to_conn.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null 6 | ); 7 | 8 | 9 | insert into public.account(email) 10 | values 11 | ('aardvark@x.com'), 12 | ('bat@x.com'), 13 | ('cat@x.com'), 14 | ('dog@x.com'), 15 | ('elephant@x.com'); 16 | 17 | 18 | create table blog( 19 | id serial primary key, 20 | owner_id integer not null references account(id), 21 | name varchar(255) not null 22 | ); 23 | comment on table blog is e'@graphql({"totalCount": {"enabled": true}})'; 24 | 25 | 26 | insert into blog(owner_id, name) 27 | values 28 | ((select id from account where email ilike 'a%'), 'A: Blog 1'), 29 | ((select id from account where email ilike 'a%'), 'A: Blog 2'), 30 | ((select id from account where email ilike 'a%'), 'A: Blog 3'), 31 | ((select id from account where email ilike 'b%'), 'B: Blog 4'); 32 | 33 | 34 | select jsonb_pretty( 35 | graphql.resolve($$ 36 | { 37 | accountCollection { 38 | edges { 39 | node { 40 | id 41 | email 42 | blogCollection { 43 | totalCount 44 | edges { 45 | node { 46 | name 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | $$) 55 | ); 56 | 57 | 58 | rollback; 59 | -------------------------------------------------------------------------------- /test/sql/resolve_connection_to_node.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null 6 | ); 7 | 8 | 9 | insert into public.account(email) 10 | values 11 | ('aardvark@x.com'), 12 | ('bat@x.com'), 13 | ('cat@x.com'), 14 | ('dog@x.com'), 15 | ('elephant@x.com'); 16 | 17 | 18 | create table blog( 19 | id serial primary key, 20 | owner_id integer not null references account(id) 21 | ); 22 | 23 | 24 | insert into blog(owner_id) 25 | values 26 | ((select id from account where email ilike 'a%')), 27 | ((select id from account where email ilike 'a%')), 28 | ((select id from account where email ilike 'a%')), 29 | ((select id from account where email ilike 'b%')); 30 | 31 | 32 | select jsonb_pretty( 33 | graphql.resolve($$ 34 | { 35 | blogCollection { 36 | edges { 37 | node { 38 | ownerId 39 | owner { 40 | id 41 | } 42 | } 43 | } 44 | } 45 | } 46 | $$) 47 | ); 48 | 49 | rollback; 50 | -------------------------------------------------------------------------------- /test/sql/resolve_error_connection_edge_no_field.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key 5 | ); 6 | 7 | 8 | select graphql.resolve($$ 9 | { 10 | accountCollection { 11 | totalCount 12 | edges { 13 | dneField 14 | } 15 | } 16 | } 17 | $$); 18 | 19 | rollback; 20 | -------------------------------------------------------------------------------- /test/sql/resolve_error_connection_edge_node_no_field.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key 5 | ); 6 | 7 | 8 | select graphql.resolve($$ 9 | { 10 | accountCollection { 11 | edges { 12 | cursor 13 | node { 14 | dneField 15 | } 16 | } 17 | } 18 | } 19 | $$); 20 | 21 | rollback; 22 | -------------------------------------------------------------------------------- /test/sql/resolve_error_connection_no_field.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key 5 | ); 6 | 7 | 8 | select graphql.resolve($$ 9 | { 10 | accountCollection { 11 | dneField 12 | totalCount 13 | } 14 | } 15 | $$); 16 | 17 | rollback; 18 | -------------------------------------------------------------------------------- /test/sql/resolve_error_from_parser.sql: -------------------------------------------------------------------------------- 1 | -- Platform specific diffs so we have to test the properties here rather than exact response 2 | with d(val) as ( 3 | select graphql.resolve($$ 4 | { { { 5 | shouldFail 6 | } 7 | } 8 | $$)::json 9 | ) 10 | 11 | select 12 | ( 13 | json_typeof(val -> 'errors') = 'array' 14 | and json_array_length(val -> 'errors') = 1 15 | ) as is_valid 16 | from d; 17 | -------------------------------------------------------------------------------- /test/sql/resolve_error_mutation_no_field.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | select graphql.resolve($$ 4 | mutation { 5 | insertDNE(object: { 6 | email: "o@r.com" 7 | }) { 8 | id 9 | } 10 | } 11 | $$); 12 | 13 | rollback; 14 | -------------------------------------------------------------------------------- /test/sql/resolve_error_node_no_field.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id int primary key, 5 | parent_id int references account(id) 6 | ); 7 | 8 | 9 | select graphql.resolve($$ 10 | { 11 | accountCollection { 12 | edges { 13 | cursor 14 | node { 15 | parent { 16 | dneField 17 | } 18 | } 19 | } 20 | } 21 | } 22 | $$); 23 | 24 | rollback; 25 | -------------------------------------------------------------------------------- /test/sql/resolve_error_query_no_field.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | select graphql.resolve($$ 4 | { 5 | account { 6 | id 7 | } 8 | } 9 | $$); 10 | 11 | rollback; 12 | -------------------------------------------------------------------------------- /test/sql/resolve_fragment.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog( 4 | id serial primary key, 5 | owner_id integer not null, 6 | name varchar(255) not null, 7 | description text 8 | ); 9 | 10 | insert into blog(owner_id, name, description) 11 | values 12 | (1, 'A: Blog 1', 'first'), 13 | (2, 'A: Blog 2', 'second'); 14 | 15 | 16 | select graphql.resolve($$ 17 | { 18 | blogCollection(first: 1) { 19 | edges { 20 | cursor 21 | node { 22 | ...BaseBlog 23 | ownerId 24 | } 25 | } 26 | } 27 | } 28 | 29 | fragment BaseBlog on Blog { 30 | name 31 | description 32 | } 33 | $$); 34 | 35 | rollback; 36 | -------------------------------------------------------------------------------- /test/sql/row_level_security.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | -- Test that row level security policies are applied for non-superusers 3 | create role anon; 4 | alter default privileges in schema public grant all on tables to anon; 5 | grant usage on schema public to anon; 6 | grant usage on schema graphql to anon; 7 | grant all on function graphql.resolve to anon; 8 | 9 | create table account( 10 | id int primary key 11 | ); 12 | 13 | -- create policy such that only id=2 is visible to anon role 14 | create policy acct_select 15 | on public.account 16 | as permissive 17 | for select 18 | to anon 19 | using (id = 2); 20 | 21 | alter table public.account enable row level security; 22 | 23 | -- Create records fo id 1..10 24 | insert into public.account(id) 25 | select * from generate_series(1, 10); 26 | 27 | set role anon; 28 | 29 | -- Only id=2 should be returned 30 | select jsonb_pretty( 31 | graphql.resolve($$ 32 | { 33 | accountCollection { 34 | edges { 35 | node { 36 | id 37 | } 38 | } 39 | } 40 | } 41 | $$) 42 | ); 43 | 44 | rollback; 45 | -------------------------------------------------------------------------------- /test/sql/test_error_anon_and_named_operations.sql: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query QueryA { named } 3 | { anon }' 4 | ) 5 | -------------------------------------------------------------------------------- /test/sql/test_error_invalid_offset.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog( 4 | id serial primary key, 5 | owner_id integer not null, 6 | name varchar(255) not null, 7 | description text 8 | ); 9 | 10 | insert into blog(owner_id, name, description) 11 | values 12 | (1, 'A: Blog 1', 'first'), 13 | (2, 'A: Blog 2', 'second'); 14 | 15 | 16 | select graphql.resolve($$ 17 | { 18 | blogCollection(first: -1) { 19 | edges { 20 | cursor 21 | node { 22 | ownerId 23 | } 24 | } 25 | } 26 | } 27 | 28 | $$); 29 | 30 | select graphql.resolve($$ 31 | { 32 | blogCollection(last: -1) { 33 | edges { 34 | cursor 35 | node { 36 | ownerId 37 | } 38 | } 39 | } 40 | } 41 | 42 | $$); 43 | 44 | comment on schema public is E'@graphql({"max_rows": -1, "inflect_names": true})'; 45 | 46 | select graphql.resolve($$ 47 | { 48 | blogCollection { 49 | edges { 50 | cursor 51 | node { 52 | ownerId 53 | } 54 | } 55 | } 56 | } 57 | 58 | $$); 59 | 60 | 61 | rollback; 62 | -------------------------------------------------------------------------------- /test/sql/test_error_multiple_anon_operations.sql: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='{ anon1 } { anon2 }' 3 | ) 4 | -------------------------------------------------------------------------------- /test/sql/test_error_mutation_transpilation.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | comment on schema public is '@graphql({"inflect_names": true})'; 3 | 4 | create table public.account( 5 | id serial primary key, 6 | first_name varchar(255) not null check (first_name not like '%_%') 7 | ); 8 | 9 | -- Second mutation is supposed to generate an exception 10 | select 11 | jsonb_pretty( 12 | graphql.resolve($$ 13 | mutation { 14 | firstInsert: insertIntoAccountCollection(objects: [ 15 | { firstName: "name" } 16 | ]) { 17 | records { 18 | id 19 | firstName 20 | } 21 | } 22 | 23 | secondInsert: insertIntoAccountCollection(objects: [ 24 | { firstName: "another_name" } 25 | ]) { 26 | records { 27 | id 28 | firstName 29 | } 30 | } 31 | } 32 | $$) 33 | ); 34 | 35 | select * from public.account; 36 | 37 | rollback; 38 | -------------------------------------------------------------------------------- /test/sql/test_error_operation_name_not_found.sql: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query ABC { anon }', 3 | "operationName":='DEF' 4 | ) 5 | -------------------------------------------------------------------------------- /test/sql/test_error_operation_names_not_unique.sql: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query ABC { anon } 3 | query ABC { other }' 4 | ) 5 | -------------------------------------------------------------------------------- /test/sql/test_error_query_transpilation.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | comment on schema public is '@graphql({"inflect_names": true})'; 4 | 5 | create table public.account( 6 | id serial primary key, 7 | first_name varchar(255) not null 8 | ); 9 | 10 | insert into public.account(first_name) values ('foo'); 11 | 12 | -- Extend with function 13 | create function public._raise_err(rec public.account) 14 | returns text 15 | immutable 16 | strict 17 | language sql 18 | as $$ 19 | select 1/0 -- divide by 0 error 20 | $$; 21 | 22 | select 23 | jsonb_pretty( 24 | graphql.resolve($$ 25 | { 26 | accountCollection { 27 | edges { 28 | node { 29 | id 30 | firstName 31 | raiseErr 32 | } 33 | } 34 | } 35 | } 36 | $$) 37 | ); 38 | 39 | select * from public.account; 40 | rollback; 41 | -------------------------------------------------------------------------------- /test/sql/test_error_subscription.sql: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='subscription Abc { anon }' 3 | ) 4 | -------------------------------------------------------------------------------- /test/sql/test_query__type.sql: -------------------------------------------------------------------------------- 1 | select graphql.resolve( 2 | query:='query Abc { __type(name: "Int") { name kind description } }' 3 | ); 4 | -------------------------------------------------------------------------------- /test/sql/total_count.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table account( 4 | id serial primary key, 5 | email varchar(255) not null 6 | ); 7 | 8 | 9 | insert into public.account(email) 10 | values 11 | ('a@x.com'), 12 | ('b@x.com'); 13 | 14 | 15 | -- Should fail. totalCount not enabled 16 | select graphql.resolve($$ 17 | { 18 | accountCollection { 19 | totalCount 20 | edges { 21 | cursor 22 | } 23 | } 24 | } 25 | $$); 26 | 27 | -- Enable totalCount 28 | comment on table account is e'@graphql({"totalCount": {"enabled": true}})'; 29 | 30 | -- Should work. totalCount is enabled 31 | select graphql.resolve($$ 32 | { 33 | accountCollection { 34 | totalCount 35 | edges { 36 | cursor 37 | } 38 | } 39 | } 40 | $$); 41 | 42 | rollback; 43 | -------------------------------------------------------------------------------- /test/sql/type_bigfloat.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.amount( 3 | id serial primary key, 4 | val numeric(10,2) 5 | ); 6 | 7 | insert into public.amount(val) 8 | values 9 | ('123.45'), 10 | ('543.21'); 11 | 12 | -- should work 13 | select graphql.resolve($$ 14 | mutation { 15 | insertIntoAmountCollection(objects: [ 16 | { val: "123.45" } 17 | ]) { 18 | records { 19 | id 20 | val 21 | } 22 | } 23 | } 24 | $$); 25 | 26 | savepoint a; 27 | 28 | -- should fail: must be a string 29 | select graphql.resolve($$ 30 | mutation { 31 | insertIntoAmountCollection(objects: [ 32 | { val: 543.25 } 33 | ]) { 34 | records { 35 | id 36 | val 37 | } 38 | } 39 | } 40 | $$); 41 | 42 | rollback to savepoint a; 43 | 44 | select graphql.resolve($$ 45 | mutation { 46 | updateAmountCollection( 47 | set: { 48 | val: "222.65" 49 | } 50 | filter: {id: {eq: 1}} 51 | atMost: 1 52 | ) { 53 | records { id } 54 | } 55 | } 56 | $$); 57 | 58 | -- Filter: should work 59 | select jsonb_pretty( 60 | graphql.resolve($$ 61 | { 62 | amountCollection(filter: {val: {eq: "222.65"}}) { 63 | edges { 64 | node { 65 | id 66 | } 67 | } 68 | } 69 | } 70 | $$) 71 | ); 72 | 73 | 74 | -- should fail: must be string 75 | select jsonb_pretty( 76 | graphql.resolve($$ 77 | { 78 | amountCollection(filter: {val: {lt: 9999}}) { 79 | edges { 80 | node { 81 | id 82 | } 83 | } 84 | } 85 | } 86 | $$) 87 | ); 88 | 89 | 90 | 91 | rollback; 92 | -------------------------------------------------------------------------------- /test/sql/type_modifier_max_length.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table memo( 3 | id serial primary key, 4 | vc8 varchar(8), 5 | c2 char(2) 6 | ); 7 | 8 | insert into memo(vc8, c2) 9 | values ('foo bar', 'aa'); 10 | 11 | -- Expect success 12 | select graphql.resolve($$ 13 | mutation { 14 | insertIntoMemoCollection(objects: [ 15 | { vc8: "baz", c2: "bb" } 16 | ]) { 17 | records { 18 | id 19 | vc8 20 | c2 21 | } 22 | } 23 | } 24 | $$); 25 | 26 | -- Expect fail, vc8 too long 27 | select graphql.resolve($$ 28 | mutation { 29 | insertIntoMemoCollection(objects: [ 30 | { vc8: "123456789", c2: "bb" } 31 | ]) { 32 | records { 33 | id 34 | vc8 35 | c2 36 | } 37 | } 38 | } 39 | $$); 40 | 41 | -- Expect fail, c2 too long 42 | select graphql.resolve($$ 43 | mutation { 44 | insertIntoMemoCollection(objects: [ 45 | { vc8: "12345", c2: "123" } 46 | ]) { 47 | records { 48 | id 49 | vc8 50 | c2 51 | } 52 | } 53 | } 54 | $$); 55 | 56 | -- Expect fail, filter value too long 57 | select graphql.resolve($$ 58 | { 59 | memoCollection(filter: {c2: {eq: "too long"}}){ 60 | edges { node { id } } 61 | 62 | } 63 | } 64 | $$); 65 | 66 | -- Expect success 67 | select graphql.resolve($$ 68 | { 69 | memoCollection(filter: {c2: {eq: "aa"}}){ 70 | edges { node { id } } 71 | 72 | } 73 | } 74 | $$); 75 | 76 | 77 | rollback; 78 | -------------------------------------------------------------------------------- /test/sql/type_opaque.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | create table public.device( 3 | id serial primary key, 4 | val inet 5 | ); 6 | 7 | -- should work 8 | select graphql.resolve($$ 9 | mutation { 10 | insertIntoDeviceCollection(objects: [ 11 | { val: "102.118.1.1" } 12 | ]) { 13 | records { 14 | id 15 | val 16 | } 17 | } 18 | } 19 | $$); 20 | 21 | select graphql.resolve($$ 22 | mutation { 23 | updateDeviceCollection( 24 | set: { 25 | val: "1.1.1.1" 26 | } 27 | atMost: 1 28 | ) { 29 | records { 30 | id 31 | val 32 | } 33 | } 34 | } 35 | $$); 36 | 37 | -- Filter: should work 38 | select jsonb_pretty( 39 | graphql.resolve($$ 40 | { 41 | deviceCollection(filter: {val: {eq: "1.1.1.1"}}) { 42 | edges { 43 | node { 44 | id 45 | val 46 | } 47 | } 48 | } 49 | } 50 | $$) 51 | ); 52 | 53 | -- Filter: should work 54 | select jsonb_pretty( 55 | graphql.resolve($$ 56 | { 57 | deviceCollection(filter: {val: {is: NOT_NULL}}) { 58 | edges { 59 | node { 60 | id 61 | val 62 | } 63 | } 64 | } 65 | } 66 | $$) 67 | ); 68 | 69 | rollback; 70 | -------------------------------------------------------------------------------- /test/sql/variable_default.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table blog( 4 | id int primary key 5 | ); 6 | 7 | insert into blog(id) 8 | select generate_series(1, 5); 9 | 10 | -- User defined default for variable $first. 11 | 12 | -- Returns 2 rows 13 | -- No value provided for variable $first so user defined default applies 14 | select graphql.resolve($$ 15 | query Blogs($first: Int = 2) { 16 | blogCollection(first: $first) { 17 | edges { 18 | node { 19 | id 20 | } 21 | } 22 | } 23 | } 24 | $$); 25 | 26 | -- Returns 1 row 27 | -- Provided value for variable $first applies 28 | select graphql.resolve($$ 29 | query Blogs($first: Int = 2) { 30 | blogCollection(first: $first) { 31 | edges { 32 | node { 33 | id 34 | } 35 | } 36 | } 37 | } 38 | $$, 39 | variables := jsonb_build_object( 40 | 'first', 1 41 | ) 42 | ); 43 | 44 | -- Returns all rows 45 | -- No default, no variable value. Falls back to sever side behavior 46 | select graphql.resolve($$ 47 | query Blogs($first: Int) { 48 | blogCollection(first: $first) { 49 | edges { 50 | node { 51 | id 52 | } 53 | } 54 | } 55 | } 56 | $$ 57 | ); 58 | 59 | rollback; 60 | --------------------------------------------------------------------------------