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