Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ on:
- 'docs/**'
- 'website/**'
- '.github/workflows/docs.yml'
release:
types: [published]
workflow_dispatch:
workflow_dispatch: {}

# One static group: deploys from different branches force-push the same
Expand Down Expand Up @@ -39,6 +42,11 @@ jobs:
working-directory: website
run: npm ci

- name: Find the latest release
run: |
git fetch --tags --quiet
echo "GRAPHITI_RELEASE=$(git tag --list 'v2*' --sort=-v:refname | head -1 | sed 's/^v//')" >> "$GITHUB_ENV"

- name: Build
working-directory: website
run: npm run build
Expand Down
24 changes: 24 additions & 0 deletions bin/cut-docs-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'TEXT'
Usage: bin/cut-docs-version <version> e.g. bin/cut-docs-version 2.1

Freezes docs/ as website/versioned_docs/version-<version>, served at /<version>/.
docs/ itself keeps serving at the root as "Latest". Run it before merging docs
whose syntax the current release lacks, naming the release being left behind.
Review the new files and commit them.
TEXT
}

case "${1:-}" in
""|-h|--help) usage; exit 0 ;;
esac

cd "$(dirname "$0")/../website"
[ -d node_modules ] || npm ci
npm run docusaurus docs:version "$1"

echo
echo "Cut $1. Review website/versioned_docs/version-$1 and commit it."
4 changes: 2 additions & 2 deletions docs/concepts/backends-and-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A **scope** is whatever your backend needs to run a query. Graphiti doesn't care

```ruby
class EmployeeResource < ApplicationResource
self.adapter = Graphiti::Adapters::Null
adapter :null

attribute :name, :string

Expand Down Expand Up @@ -49,7 +49,7 @@ Writing that per Resource gets old. Once the pattern stabilizes, move it into an

```ruby
class EmployeeResource < ApplicationResource
self.adapter = BackendAdapter
adapter BackendAdapter
attribute :name, :string
end
```
Expand Down
16 changes: 8 additions & 8 deletions docs/concepts/links.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ this happens automatically:
```ruby
class ApplicationResource < Graphiti::Resource
# ... code ...
self.endpoint_namespace = '/api/v1'
endpoint_namespace '/api/v1'
end

class PostResource < ApplicationResource
Expand Down Expand Up @@ -145,7 +145,7 @@ secondary_endpoint '/top_posts', [:index]

```ruby
class ApplicationResource < Graphiti::Resource
self.relationship_links = false
relationship_links false
end

class PostResource < ApplicationResource
Expand All @@ -166,15 +166,15 @@ Endpoints are validated in two directions, each with its own setting.

```ruby
class ApplicationResource < Graphiti::Resource
self.validate_requests = false
validate_requests false
end
```

`validate_links` guards what goes out. Before rendering a relationship link, Graphiti checks that the target endpoint is actually routable for the action the link needs, which is `:show` for a `belongs_to` and `:index` otherwise. You never serialize a link that 404s. Custom `link do ... end` blocks and remote Resources are skipped:

```ruby
class ApplicationResource < Graphiti::Resource
self.validate_links = false
validate_links false
end
```

Expand All @@ -188,7 +188,7 @@ To only render relationship links when requested in the URL with `?links=true`:

```ruby
class ApplicationResource < Graphiti::Resource
self.relationship_links = :on_demand
relationship_links :on_demand
end
```

Expand All @@ -206,7 +206,7 @@ Every collection response returns pagination links:

```ruby
class ApplicationResource < Graphiti::Resource
self.page_links = true
page_links true
end
```

Expand All @@ -216,7 +216,7 @@ Links are rendered only when the request asks for them with `?page_links=true` (

```ruby
class ApplicationResource < Graphiti::Resource
self.page_links = :on_demand
page_links :on_demand
end
```

Expand All @@ -231,7 +231,7 @@ To change the URL associated with a Resource:
```ruby
class PostResource < ApplicationResource
# Most commonly seen in ApplicationResource
self.endpoint_namespace = '/api/v1'
endpoint_namespace '/api/v1'

primary_endpoint '/posts', [:index, :show]
# OR
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ To change how far a `belongs_to` goes, across a whole API, set it on the resourc

```ruby
class ApplicationResource < Graphiti::Resource
self.belongs_to_resource_ids_by_default = :foreign_key
belongs_to_resource_ids_by_default :foreign_key
end
```

Expand All @@ -191,7 +191,7 @@ A client never has to work out which rule applied. The relationship object says

A relationship with neither is left out of the payload. Relationships are linked by default, so the link shape is the one you normally see.

`self.relationship_placeholders = true` brings back the 1.x shape, `{"meta": {"included": false}}`. It is not part of JSON:API and carries nothing a client can act on.
`relationship_placeholders true` brings back the 1.x shape, `{"meta": {"included": false}}`. It is not part of JSON:API and carries nothing a client can act on.

The setting covers `belongs_to` and `polymorphic_belongs_to`, and no collection, deliberately. An API-wide `:always` on collections would be the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) applied everywhere at once.

Expand Down
102 changes: 76 additions & 26 deletions docs/concepts/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,17 @@ The model is only looked up when a guard declares a parameter for it, so zero-ar

```ruby
# On ApplicationResource, affects every subclass
self.attributes_readable_by_default = false # default true
self.attributes_writable_by_default = false # default true
self.attributes_filterable_by_default = false # default true
self.attributes_sortable_by_default = false # default true
self.attributes_schema_by_default = false # default true
attributes_readable_by_default false # default true
attributes_writable_by_default false # default true
attributes_filterable_by_default false # default true
attributes_sortable_by_default false # default true
attributes_schema_by_default false # default true
```

Each `*_by_default` setting can also be a guard symbol, delegating the check to a method. Useful for wiring every attribute through one authorization system:

```ruby
self.attributes_readable_by_default = :attribute_readable?
attributes_readable_by_default :attribute_readable?

def attribute_readable?(model_instance, attribute_name)
PolicyChecker.new(model_instance).attribute_readable?(attribute_name)
Expand All @@ -101,6 +101,54 @@ attribute :name, :string do
end
```

### Public Ids {#public-ids}

By default the JSON:API `id` is the model's primary key. To hide your real database ids from clients, specify a public_id column:

```ruby
class PostResource < ApplicationResource
public_id :slug
end
```
Now `id` is the slug everywhere a client interacts with it, including filters, sorts, stats, groups, and links. Graphiti internals like foreign keys, sideloads and joins still run on the primary key, which clients never see.

If you don't have a dedicated column and just want to obfuscate your database ids: you provide a block to encode and decode them on the fly, using something like [Sqids](https://sqids.org):

```ruby
class PostResource < ApplicationResource
public_id do
encode { |primary_key| Sqids.new.encode([primary_key]) }
decode { |public_id| Sqids.new.decode(public_id).first }
end
end
```

If someone sends an id that doesn't decode, or that decodes to something which wouldn't encode back to the same string, Graphiti treats it as not found. Your `decode` block doesn't need to validate anything.

Declare `public_id` on an abstract resource, like `ApplicationResource`, and every subclass inherits it.

**Types.** The public id takes its type from the ActiveRecord column, and is a string anywhere that can't be read. Pass a type to override: `public_id :code, :integer`.

**Foreign keys.** If you expose a foreign key as a readable attribute, say `attribute :author_id, :integer`, it would print the author's real database id. So Graphiti raises when `AuthorResource` has a public id. Mark the attribute `readable: false` or `only: [:filterable]` instead. Clients can still write to it, filter on it and group by it, using the author's public id.

If you write your own filter block, you get the value exactly as the client sent it. Ask for `primary_keys:` when you want it decoded for you:

```ruby
filter :author_id, :string do
eq do |scope, value, primary_keys:|
scope.where(author_id: primary_keys).or(scope.where(coauthor_id: primary_keys))
end
end
```

A filter that isn't named after the foreign key can decode by hand with `AuthorResource.decode_public_ids(value)`, or `decode_public_id` for one.

**Links.** Relationships link by public id in both directions. For a `belongs_to` that means one extra query per relationship to translate the foreign keys in the response, or no query at all with the encode and decode blocks. The translation goes through the target resource's `base_scope`, so a record the current user can't see simply renders no linkage.

When Graphiti can't build a link without exposing a real id, it leaves the link out. That happens for a `belongs_to` whose `primary_key` isn't something the target can filter on, and for a `has_many` whose child filter has a custom block that doesn't ask for `primary_keys:`. Run `bin/rake graphiti:audit` to see which relationships are affected, and give any of them a `link` block if you'd rather build the URL yourself. If two parent resources both link into the same child filter, one of them needs an `inverse_filter`.

**Schema.** `schema.json` records the public id column, or `true` when you're encoding, and the schema check flags any change to it. Every id a client has stored would stop working.

### Types {#types}

| Type | Notes |
Expand Down Expand Up @@ -146,7 +194,7 @@ To serialize values exactly as the model returns them, skipping the type's `read

```ruby
class ApplicationResource < Graphiti::Resource
self.typecast_reads = false
typecast_reads false
end
```

Expand Down Expand Up @@ -431,8 +479,8 @@ Two settings you might want to adjust, both usually set on `ApplicationResource`

```ruby
class ApplicationResource < Graphiti::Resource
self.page_default_size = 10 # unset falls back to 20
self.page_max_size = 100 # default 1_000
page_default_size 10 # unset falls back to 20
page_max_size 100 # default 1_000
end
```

Expand Down Expand Up @@ -461,7 +509,7 @@ end

```ruby
class PostResource < ApplicationResource
self.page_cursors = true # default false
page_cursors true # default false
end
```

Expand Down Expand Up @@ -552,14 +600,14 @@ end

```ruby
class PostResource < ApplicationResource
self.model = Post
self.type = 'posts'
model Post
type 'posts'

# Only used if you care about Links
primary_endpoint '/posts', [:index, :show, :create, :update, :destroy]

self.default_sort = [{ title: :asc }] # default nil
self.page_default_size = 10 # default 20
default_sort [{ title: :asc }] # default nil
page_default_size 10 # default 20
end
```

Expand All @@ -568,36 +616,38 @@ Typically inherited from `ApplicationResource`, where cross-cutting settings liv
```ruby
class ApplicationResource < Graphiti::Resource
# Required when there's no corresponding model
self.abstract_class = true
abstract_class

# Subclasses override as needed
self.adapter = Graphiti::Adapters::ActiveRecord
adapter :active_record

# Default attribute flags. See #limiting-behavior
self.attributes_readable_by_default = true
self.attributes_writable_by_default = true
self.attributes_sortable_by_default = true
self.attributes_filterable_by_default = true
attributes_readable_by_default true
attributes_writable_by_default true
attributes_sortable_by_default true
attributes_filterable_by_default true

# Used for link generation
self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000')
base_url ENV.fetch('BASE_URL', 'http://localhost:3000')
# Suggest referencing this in config/routes.rb:
# scope path: '/api/v1' do
# resources :posts
# end
self.endpoint_namespace = '/api/v1'
endpoint_namespace '/api/v1'

# Refuse requests reaching this Resource from a URL it isn't allowlisted for
self.validate_requests = true
validate_requests true

# Refuse to render a link pointing at an endpoint that isn't routable
self.validate_links = true
validate_links true

# Render relationship links: true, false, or :on_demand
self.relationship_links = true
relationship_links true
end
```

The bare form arrived in 2.1. Every setting also accepts the assignment form, `self.page_default_size = 10`, which is what code written for earlier versions uses, and the two are interchangeable.

### Polymorphic Resources {#polymorphic-resources}

Polymorphic Resources are similar to [ActiveRecord STI](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html): a single query returns multiple Resource types. Querying `/tasks` can return `bugs`, `features`, and `epics`.
Expand Down Expand Up @@ -635,7 +685,7 @@ end
```ruby
class TaskResource < ApplicationResource
# Reference child classes
self.polymorphic = [
polymorphic [
'BugResource',
'FeatureResource',
'EpicResource'
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Resource:
```ruby
# Assuming you already have a Post ActiveRecord Model
class PostResource < Graphiti::Resource
self.adapter = Graphiti::Adapters::ActiveRecord
adapter :active_record
attribute :title, :string
end

Expand Down
Loading
Loading