graphiti 2.0.0.beta.4 → 2.0.0.beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +5 -0
  3. data/.github/workflows/docs.yml +4 -7
  4. data/CHANGELOG.md +29 -0
  5. data/docs/concepts/links.md +3 -3
  6. data/docs/concepts/relationships.md +109 -9
  7. data/docs/topics/debugging.md +26 -0
  8. data/docs/upgrading.md +18 -17
  9. data/graphiti.gemspec +1 -19
  10. data/lib/generators/graphiti/resource_generator.rb +25 -1
  11. data/lib/generators/graphiti/templates/controller.rb.erb +1 -1
  12. data/lib/graphiti/audit/report.rb +226 -0
  13. data/lib/graphiti/audit.rb +231 -0
  14. data/lib/graphiti/errors.rb +47 -0
  15. data/lib/graphiti/rails/rake_helpers.rb +42 -0
  16. data/lib/graphiti/request_validators/validator.rb +10 -1
  17. data/lib/graphiti/resource/configuration.rb +16 -1
  18. data/lib/graphiti/resource/polymorphism.rb +10 -0
  19. data/lib/graphiti/schema.rb +1 -1
  20. data/lib/graphiti/sideload/belongs_to.rb +22 -29
  21. data/lib/graphiti/sideload.rb +48 -13
  22. data/lib/graphiti/spec_helpers/matchers.rb +196 -0
  23. data/lib/graphiti/spec_helpers/rspec.rb +1 -0
  24. data/lib/graphiti/spec_helpers.rb +1 -0
  25. data/lib/graphiti/util/serializer_relationships.rb +27 -4
  26. data/lib/graphiti/version.rb +1 -1
  27. data/lib/graphiti.rb +2 -0
  28. data/lib/tasks/graphiti.rake +16 -32
  29. data/website/docusaurus.config.js +67 -11
  30. data/website/src/css/custom.css +29 -2
  31. data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
  32. data/website/static/1.13/assets/img/home-bg.jpg +0 -0
  33. data/website/static/1.13/assets/img/sunrise.png +0 -0
  34. data/website/static/1.13/assets/main.css +4 -4
  35. data/website/static/CNAME +1 -0
  36. metadata +9 -9
@@ -3,40 +3,33 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
3
3
  :belongs_to
4
4
  end
5
5
 
6
- def default_include_resource_ids?
7
- linkage_from_foreign_key?
6
+ def default_render_resource_ids?
7
+ case parent_resource_class&.belongs_to_resource_ids_by_default
8
+ when :always then renderable_at_all?
9
+ when :never then false
10
+ else resource_ids_from_foreign_key?
11
+ end
12
+ end
13
+
14
+ def renderable_at_all?
15
+ readable_guarded? || readable?
8
16
  end
9
17
 
10
- # The parent already carries the foreign key, and for a plain belongs_to
11
- # that key *is* the related id, so linkage costs nothing. Anything that can
12
- # change which record the relationship resolves to, or what type it carries,
13
- # has to load the association instead:
14
- #
15
- # - a scope/params block or a base_scope can filter out the record the
16
- # foreign key points at, so the key would claim a relationship the API
17
- # would not actually return
18
- # - a polymorphic target takes its type from the record, not from the
19
- # relationship, so the key alone cannot say what type the id has
20
- # - a remote resource has no local foreign key to read
21
- # - a custom primary_key points the relationship at some other column, so
22
- # the key holds that column's value rather than the related id
23
- def linkage_from_foreign_key?
24
- # Ask before resolving #resource: an unreadable relationship renders
25
- # nothing, and its resource class may not even be inferrable.
26
- return false unless readable?
27
- return false unless foreign_key_is_related_id?
28
- return false if polymorphic_child?
29
- return false if self.class.scope_proc || self.class.params_proc
30
- return false if @base_scope
31
- return false if remote?
32
- return false if resource.class.polymorphic.present?
18
+ def resource_ids_blocker
19
+ return :unreadable unless renderable_at_all?
20
+ return :custom_primary_key unless foreign_key_is_related_id?
21
+ return :polymorphic_child if polymorphic_child?
22
+ return :scope_block if self.class.scope_proc
23
+ return :params_block if self.class.params_proc
24
+ return :base_scope if @base_scope
25
+ return :remote if remote?
26
+ return :polymorphic_resource if resource.class.polymorphic.present?
33
27
 
34
- true
28
+ nil
35
29
  end
36
30
 
37
- # The foreign key can stand in for the related id only when the two hold the
38
- # same value. base_filter matches the key against primary_key, so pointing
39
- # that at another column means the key holds that column instead.
31
+ # base_filter matches the foreign key against primary_key, so a custom
32
+ # primary_key means the key holds that column's value, not the related id.
40
33
  def foreign_key_is_related_id?
41
34
  primary_key == :id
42
35
  end
@@ -20,6 +20,7 @@ module Graphiti
20
20
  def initialize(name, opts)
21
21
  @name = name
22
22
  validate_options!(opts)
23
+ translate_deprecated_options!(opts)
23
24
  @parent_resource_class = opts[:parent_resource]
24
25
  @resource_class_name = opts[:resource]
25
26
  @primary_key = opts[:primary_key]
@@ -42,7 +43,7 @@ module Graphiti
42
43
  @group_name = opts[:group_name]
43
44
  @polymorphic_child = opts[:polymorphic_child]
44
45
  @parent = opts[:parent]
45
- @always_include_resource_ids = opts[:always_include_resource_ids]
46
+ @render_resource_ids = opts[:resource_ids]
46
47
 
47
48
  if polymorphic_child?
48
49
  parent.resource.polymorphic << resource_class
@@ -110,6 +111,31 @@ module Graphiti
110
111
  dynamic_flag?(@readable) || dynamic_flag?(@writable)
111
112
  end
112
113
 
114
+ def readable_guarded?
115
+ dynamic_flag?(@readable)
116
+ end
117
+
118
+ def readable_guard_name
119
+ @readable.to_sym if @readable.is_a?(Symbol) || @readable.is_a?(String)
120
+ end
121
+
122
+ def non_default_options
123
+ options = {}
124
+ options[:as] = association_name if @as
125
+ options[:primary_key] = primary_key unless primary_key == :id
126
+ options[:single] = true if single?
127
+ options[:remote] = @remote if remote?
128
+ options[:link] = @link unless @link.nil?
129
+ options[:readable] = @readable unless @readable.nil? || @readable == true
130
+ options[:writable] = @writable unless @writable.nil? || @writable == true
131
+ options[:resource_ids] = @render_resource_ids unless @render_resource_ids.nil?
132
+ options
133
+ end
134
+
135
+ def customized_base_scope?
136
+ !!@base_scope
137
+ end
138
+
113
139
  def single?
114
140
  !!@single
115
141
  end
@@ -122,24 +148,21 @@ module Graphiti
122
148
  !!@polymorphic_as
123
149
  end
124
150
 
125
- # False everywhere but a plain belongs_to - see
126
- # Sideload::BelongsTo#linkage_from_foreign_key?.
127
- def linkage_from_foreign_key?
128
- false
151
+ def resource_ids_from_foreign_key?
152
+ resource_ids_blocker.nil?
129
153
  end
130
154
 
131
- # nil at either of the first two levels means "not specified" rather
132
- # than "false".
133
- def always_include_resource_ids?
134
- return !!@always_include_resource_ids unless @always_include_resource_ids.nil?
155
+ def resource_ids_blocker
156
+ :no_foreign_key_on_parent
157
+ end
135
158
 
136
- configured = parent_resource_class&.always_include_resource_ids_by_default
137
- return !!configured unless configured.nil?
159
+ def render_resource_ids?
160
+ return !!@render_resource_ids unless @render_resource_ids.nil?
138
161
 
139
- default_include_resource_ids?
162
+ default_render_resource_ids?
140
163
  end
141
164
 
142
- def default_include_resource_ids?
165
+ def default_render_resource_ids?
143
166
  false
144
167
  end
145
168
 
@@ -434,6 +457,18 @@ module Graphiti
434
457
  false
435
458
  end
436
459
 
460
+ def translate_deprecated_options!(opts)
461
+ return unless opts.key?(:always_include_resource_ids)
462
+
463
+ Graphiti::DEPRECATOR.deprecation_warning(
464
+ :always_include_resource_ids,
465
+ "Use :resource_ids instead (#{opts[:parent_resource]&.name}##{@name})"
466
+ )
467
+
468
+ value = opts.delete(:always_include_resource_ids)
469
+ opts[:resource_ids] = value unless opts.key?(:resource_ids)
470
+ end
471
+
437
472
  def validate_options!(opts)
438
473
  if opts[:remote]
439
474
  if opts[:resource]
@@ -0,0 +1,196 @@
1
+ module Graphiti
2
+ module SpecHelpers
3
+ # Assertions against a resource's DSL. The schema specs cannot cover
4
+ # adapter-dependent options (primary key, foreign key, and so on) because
5
+ # the schema does not carry them; these read the resource config directly.
6
+ module Matchers
7
+ class BaseMatcher
8
+ GRAPHITI_OPTS = [].freeze
9
+ GRAPHITI_CONFIG_KEY = ""
10
+ EXPECTED_ACTION = ""
11
+
12
+ def description
13
+ "#{self.class::EXPECTED_ACTION} #{target}"
14
+ end
15
+
16
+ def failure_message
17
+ "expected that #{resource.class} would #{self.class::EXPECTED_ACTION} #{target}\n#{@opt_failures.join("\n")}"
18
+ end
19
+
20
+ def failure_message_when_negated
21
+ "expected that #{resource.class} would not #{self.class::EXPECTED_ACTION} #{target}"
22
+ end
23
+
24
+ def opt_failure_message(opt, expected, actual)
25
+ "expected that #{opt} would be #{expected}, was #{actual}"
26
+ end
27
+
28
+ def does_not_match?(resource)
29
+ !matches?(resource)
30
+ end
31
+
32
+ def matches?(resource)
33
+ @resource = resource
34
+
35
+ expected? && expected_opts?
36
+ end
37
+
38
+ def with_options(opts)
39
+ @opts = opts
40
+ self
41
+ end
42
+
43
+ private
44
+
45
+ def actual_opts
46
+ self.class::GRAPHITI_OPTS & opts.keys
47
+ end
48
+
49
+ def config
50
+ @config ||= resource.class.config[self.class::GRAPHITI_CONFIG_KEY][target]
51
+ end
52
+
53
+ def expected_opts?
54
+ return false unless config
55
+
56
+ actual_opts.map { |opt| assert_opt(opt) }.all?(true)
57
+ end
58
+ end
59
+
60
+ class RelationMatcher < BaseMatcher
61
+ GRAPHITI_OPTS = %i[primary_key foreign_key resource readable writable link single].freeze
62
+ GRAPHITI_CONFIG_KEY = :sideloads
63
+ EXPECTED_ACTION = ""
64
+
65
+ def initialize(target)
66
+ @target = target
67
+ @opts = {}
68
+ @opt_failures = []
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :target, :opts, :resource
74
+
75
+ def assert_opt(opt)
76
+ asserted_opt = opt == :resource ? :resource_class : opt
77
+ return true if config.send(asserted_opt) == opts[opt]
78
+
79
+ @opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
80
+ false
81
+ end
82
+
83
+ def expected?
84
+ config && config.type == relation_name
85
+ end
86
+
87
+ # BelongsToMatcher -> :belongs_to, matching Sideload#type.
88
+ def relation_name
89
+ self.class.name.demodulize.gsub("Matcher", "").underscore.to_sym
90
+ end
91
+ end
92
+
93
+ class BelongsToMatcher < RelationMatcher
94
+ EXPECTED_ACTION = "belong to"
95
+ end
96
+
97
+ class HasManyMatcher < RelationMatcher
98
+ EXPECTED_ACTION = "have many"
99
+ end
100
+
101
+ class HasOneMatcher < RelationMatcher
102
+ EXPECTED_ACTION = "have one"
103
+ end
104
+
105
+ class ResourceDSLMatcher < BaseMatcher
106
+ def initialize(target, type)
107
+ @target = target
108
+ @type = type
109
+ @opts = {}
110
+ @opt_failures = []
111
+ end
112
+
113
+ private
114
+
115
+ attr_reader :target, :type, :opts, :resource
116
+
117
+ def expected?
118
+ config && assert_type
119
+ end
120
+
121
+ def assert_type
122
+ return true if config[:type] == type
123
+
124
+ @opt_failures << opt_failure_message("type", type, config[:type])
125
+ false
126
+ end
127
+
128
+ def assert_opt(opt)
129
+ return true if config[opt] == opts[opt]
130
+
131
+ @opt_failures << opt_failure_message(opt, opts[opt], config[opt])
132
+ false
133
+ end
134
+ end
135
+
136
+ class ExposeAttributeMatcher < ResourceDSLMatcher
137
+ GRAPHITI_OPTS = %i[writable readable sortable filterable].freeze
138
+ GRAPHITI_CONFIG_KEY = :attributes
139
+ EXPECTED_ACTION = "expose"
140
+ end
141
+
142
+ class FilterAttributeMatcher < ResourceDSLMatcher
143
+ GRAPHITI_OPTS = %i[allow deny single required allow_nil deny_empty].freeze
144
+ GRAPHITI_CONFIG_KEY = :filters
145
+ EXPECTED_ACTION = "filter"
146
+ end
147
+
148
+ # @param [Symbol] relation
149
+ #
150
+ # @example expect(subject).to belong_to_resource(:user)
151
+ # @example expect(subject).to belong_to_resource(:user).with_options(foreign_key: :user_id, resource: UserResource)
152
+ # @example expect(subject).not_to belong_to_resource(:user)
153
+ def belong_to_resource(relation)
154
+ BelongsToMatcher.new(relation)
155
+ end
156
+
157
+ # @param [Symbol] relation
158
+ #
159
+ # @example expect(subject).to have_many_resources(:posts)
160
+ # @example expect(subject).to have_many_resources(:posts).with_options(foreign_key: :post_id, resource: PostResource)
161
+ # @example expect(subject).not_to have_many_resources(:posts)
162
+ def have_many_resources(relation)
163
+ HasManyMatcher.new(relation)
164
+ end
165
+
166
+ # @param [Symbol] relation
167
+ #
168
+ # @example expect(subject).to have_one_resource(:post)
169
+ # @example expect(subject).to have_one_resource(:post).with_options(foreign_key: :post_id, resource: PostResource)
170
+ # @example expect(subject).not_to have_one_resource(:post)
171
+ def have_one_resource(relation)
172
+ HasOneMatcher.new(relation)
173
+ end
174
+
175
+ # @param [Symbol] attribute
176
+ # @param [Symbol] type
177
+ #
178
+ # @example expect(subject).to expose_attribute(:name, :string)
179
+ # @example expect(subject).to expose_attribute(:name, :string).with_options(writable: false)
180
+ # @example expect(subject).not_to expose_attribute(:name, :string)
181
+ def expose_attribute(attribute, type)
182
+ ExposeAttributeMatcher.new(attribute, type)
183
+ end
184
+
185
+ # @param [Symbol] attribute
186
+ # @param [Symbol] type
187
+ #
188
+ # @example expect(subject).to filter_attribute(:name, :string)
189
+ # @example expect(subject).to filter_attribute(:name, :string).with_options(allow_nil: false)
190
+ # @example expect(subject).not_to filter_attribute(:name, :string)
191
+ def filter_attribute(attribute, type)
192
+ FilterAttributeMatcher.new(attribute, type)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -120,6 +120,7 @@ module Graphiti
120
120
 
121
121
  ::RSpec.configure do |rspec|
122
122
  rspec.include_context "graphiti resource testing", type: :resource
123
+ rspec.include Graphiti::SpecHelpers::Matchers, type: :resource
123
124
  end
124
125
  end
125
126
 
@@ -11,6 +11,7 @@ require "graphiti/spec_helpers/helpers"
11
11
  require "graphiti/spec_helpers/node"
12
12
  require "graphiti/spec_helpers/errors_proxy"
13
13
  require "graphiti/spec_helpers/errors"
14
+ require "graphiti/spec_helpers/matchers"
14
15
 
15
16
  module Graphiti
16
17
  module SpecHelpers
@@ -68,9 +68,9 @@ module Graphiti
68
68
  # sideload can resolve it to something the foreign key alone would
69
69
  # not predict, so the loaded records win. Only the un-included case
70
70
  # is worth short-circuiting.
71
- if sideload_ref.linkage_from_foreign_key? &&
71
+ if sideload_ref.resource_ids_from_foreign_key? &&
72
72
  !self_ref.send(:included_anywhere?, @proxy.query.include_hash, sideload_ref.name)
73
- linkage always: sideload_ref.always_include_resource_ids? do
73
+ linkage always: sideload_ref.render_resource_ids? do
74
74
  foreign_key = @object.public_send(sideload_ref.foreign_key)
75
75
 
76
76
  unless foreign_key.nil?
@@ -81,7 +81,7 @@ module Graphiti
81
81
  end
82
82
  end
83
83
  else
84
- linkage always: sideload_ref.always_include_resource_ids?
84
+ linkage always: sideload_ref.render_resource_ids?
85
85
  end
86
86
 
87
87
  if link_ref
@@ -107,8 +107,31 @@ module Graphiti
107
107
 
108
108
  def data_proc
109
109
  sideload_ref = @sideload
110
+ resource_class_ref = @resource_class
110
111
  ->(_) {
111
- if (records = @object.public_send(sideload_ref.association_name))
112
+ begin
113
+ records = @object.public_send(sideload_ref.association_name)
114
+ rescue NoMethodError => error
115
+ # #receiver raises ArgumentError when the error was built by hand
116
+ # rather than raised by a failed call, and a hand-built one can
117
+ # still carry a matching #name.
118
+ receiver = begin
119
+ error.receiver
120
+ rescue ArgumentError
121
+ nil
122
+ end
123
+
124
+ raise unless error.name == sideload_ref.association_name &&
125
+ receiver.equal?(@object)
126
+
127
+ # A private method exists, so "has no such method" would be a lie.
128
+ raise if @object.respond_to?(sideload_ref.association_name, true)
129
+
130
+ raise Errors::MissingRelationshipMethod
131
+ .new(resource_class_ref, sideload_ref, @object)
132
+ end
133
+
134
+ if records
112
135
  if records.respond_to?(:to_ary)
113
136
  records.each { |r| sideload_ref.resource.decorate_record(r) }
114
137
  else
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "2.0.0.beta.4"
2
+ VERSION = "2.0.0.beta.6"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -166,6 +166,8 @@ require "graphiti/configuration"
166
166
  require "graphiti/context"
167
167
  require "graphiti/errors"
168
168
  require "graphiti/types"
169
+ require "graphiti/audit"
170
+ require "graphiti/audit/report"
169
171
  require "graphiti/schema"
170
172
  require "graphiti/schema_diff"
171
173
  require "graphiti/adapters/abstract"
@@ -1,52 +1,36 @@
1
- namespace :graphiti do
2
- include Graphiti::Rails::TestHelpers
3
-
4
- def session
5
- @session ||= ActionDispatch::Integration::Session.new(Rails.application)
6
- end
7
-
8
- def setup_rails!
9
- Rails.application.eager_load!
10
- Rails.application.config.cache_classes = true
11
- Rails.application.config.action_controller.perform_caching = false
12
- end
1
+ require "graphiti/rails/rake_helpers"
13
2
 
14
- def make_request(path, debug = false)
15
- if path.split("/").length == 2
16
- path = "#{ApplicationResource.endpoint_namespace}#{path}"
17
- end
18
- path << if path.include?("?")
19
- "&cache=bust"
20
- else
21
- "?cache=bust"
22
- end
23
- path = "#{path}&debug=true" if debug
24
- handle_request_exceptions do
25
- headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
26
- session.get(path.to_s, headers: headers)
27
- end
28
- JSON.parse(session.response.body)
29
- end
3
+ namespace :graphiti do
4
+ helpers = Graphiti::Rails::RakeHelpers
30
5
 
31
6
  desc "Execute request without web server."
32
7
  task :request, [:path, :debug] => [:environment] do |_, args|
33
- setup_rails!
8
+ helpers.setup_rails!
34
9
  Graphiti.logger = Graphiti.stdout_logger
35
10
  Graphiti::Debugger.preserve = true
36
11
  require "pp"
37
12
  path, debug = args[:path], args[:debug]
38
13
  puts "Graphiti Request: #{path}"
39
- json = make_request(path, debug)
14
+ json = helpers.make_request(path, debug)
40
15
  pp json
41
16
  Graphiti::Debugger.flush if debug
42
17
  end
43
18
 
19
+ desc "Audit every relationship: what will raise, what loads to render ids, which render no ids, and which checks passed."
20
+ task audit: [:environment] do
21
+ helpers.setup_rails!
22
+ rows = Graphiti::Audit.run
23
+ puts Graphiti::Audit::Report.new(rows).to_s
24
+
25
+ exit 1 if rows.any?(&:error?)
26
+ end
27
+
44
28
  desc "Execute benchmark without web server."
45
29
  task :benchmark, [:path, :requests] => [:environment] do |_, args|
46
- setup_rails!
30
+ helpers.setup_rails!
47
31
  took = Benchmark.ms {
48
32
  args[:requests].to_i.times do
49
- make_request(args[:path])
33
+ helpers.make_request(args[:path])
50
34
  end
51
35
  }
52
36
  puts "Took: #{(took / args[:requests].to_f).round(2)}ms"
@@ -1,16 +1,41 @@
1
1
  // @ts-check
2
2
  const {themes} = require('prism-react-renderer');
3
3
 
4
+ const codeBlockTheme = (theme, backgroundColor) => ({
5
+ ...theme,
6
+ plain: {...theme.plain, backgroundColor},
7
+ });
8
+
9
+ // Docusaurus prepends baseUrl to navbar and Link hrefs, but not to raw HTML
10
+ // like announcementBar content, so that markup has to build its own absolute
11
+ // paths. Always has a trailing slash.
12
+ const baseUrl = process.env.DOCS_BASE_URL || '/';
13
+
14
+ // The theme's strikethrough and underline on diff lines are inline styles, so
15
+ // CSS cannot undo them. Colour and the +/- signs already carry it.
16
+ const withoutDiffDecoration = (theme) => ({
17
+ ...theme,
18
+ styles: theme.styles.map(({types, style}) =>
19
+ types.some((type) => type === 'inserted' || type === 'deleted')
20
+ ? {types, style: {...style, textDecorationLine: undefined}}
21
+ : {types, style}
22
+ ),
23
+ });
24
+
4
25
  /** @type {import('@docusaurus/types').Config} */
5
26
  const config = {
6
27
  title: 'Graphiti',
7
28
  tagline: 'Stylish Graph APIs',
8
29
  favicon: 'img/favicon.ico',
9
30
 
10
- url: 'https://www.graphiti.dev',
11
- // Project pages serve this repo under /graphiti/ until the domain moves
12
- // here from graphiti-api.github.io, so the deploy sets DOCS_BASE_URL.
13
- baseUrl: process.env.DOCS_BASE_URL || '/',
31
+ // Apex, matching the CNAME. This feeds canonical tags and sitemap.xml, so it
32
+ // has to agree with the host actually serving the site or the two split
33
+ // search ranking between them.
34
+ url: 'https://graphiti.dev',
35
+ // graphiti.dev resolves to this repo, so the site serves from the root.
36
+ // DOCS_BASE_URL still overrides it for a build served from the project-pages
37
+ // path, which is what /graphiti/ was during the beta.
38
+ baseUrl,
14
39
  organizationName: 'graphiti-api',
15
40
  projectName: 'graphiti',
16
41
 
@@ -75,7 +100,7 @@ const config = {
75
100
  {from: ['/cookbooks/customizing-sideloads'], to: '/topics/customizing-sideloads'},
76
101
  {from: ['/cookbooks/hopping-relationships'], to: '/topics/hopping-relationships'},
77
102
 
78
- {from: ['/js/introduction'], to: '/js'},
103
+ {from: ['/js/introduction'], to: '/js/'},
79
104
  {
80
105
  from: [
81
106
  '/js/reads/index',
@@ -100,11 +125,24 @@ const config = {
100
125
  to: '/js/writes',
101
126
  },
102
127
  ],
128
+
129
+ // The 2.0 docs were staged under /graphiti/ for the whole beta, so
130
+ // those URLs are in the wild. createRedirects sees every generated
131
+ // route, which the static /1.13/ tree is not: that one is by hand.
132
+ createRedirects(existingPath) {
133
+ return [`/graphiti${existingPath}`];
134
+ },
103
135
  },
104
136
  ],
105
137
  ],
106
138
 
107
139
  themeConfig: {
140
+ announcementBar: {
141
+ // Changing the id un-dismisses the bar for everyone who has closed it.
142
+ id: 'graphiti-2-0-beta',
143
+ content: `Graphiti 2.0 is in beta, and the docs have been reorganized to describe it. <a href="${baseUrl}1.13/">Docs for 1.x</a>.`,
144
+ isCloseable: true,
145
+ },
108
146
  colorMode: {
109
147
  // Dark for a first-time visitor. Flip respectPrefersColorScheme to true
110
148
  // to follow the OS setting instead, in which case defaultMode only
@@ -122,19 +160,37 @@ const config = {
122
160
  position: 'right',
123
161
  // 1.x stays as the original Jekyll site, frozen under /1.13.
124
162
  //
125
- // target '_self' is required. Without it the router treats this as
126
- // an in-app route, finds no match, and renders the 404 page even
127
- // though the file is served fine.
163
+ // The pathname:// prefix is required. It is Docusaurus' escape hatch
164
+ // for a path that is served statically but is not an app route:
165
+ // without it the link renders as a react-router push, which finds no
166
+ // match and shows the 404 page even though the file is served fine.
167
+ // The prefix is stripped before baseUrl is applied, and it also makes
168
+ // the link count as external, hence target '_self' to keep it from
169
+ // opening in a new tab.
128
170
  dropdownItemsAfter: [
129
- {href: '/1.13/', label: '1.x', target: '_self'},
171
+ {href: 'pathname:///1.13/', label: '1.x', target: '_self'},
130
172
  ],
131
173
  },
132
174
  {href: 'https://github.com/graphiti-api/graphiti', label: 'GitHub', position: 'right'},
133
175
  {href: 'https://discord.gg/wgqkMBsSRV', label: 'Discord', position: 'right'},
134
176
  ],
135
177
  },
136
- footer: {style: 'dark', copyright: `Graphiti is released under the MIT license.`},
137
- prism: {theme: themes.github, darkTheme: themes.dracula, additionalLanguages: ['ruby', 'bash', 'json', 'http']},
178
+ footer: {
179
+ style: 'dark',
180
+ copyright: [
181
+ 'Originally created by <a href="https://github.com/richmolj">Lee Richmond</a>,',
182
+ '<a href="https://github.com/wadetandy">Wade Tandy</a>, and',
183
+ '<a href="https://github.com/wagenet">Peter Wagenet</a>.',
184
+ 'Maintained by <a href="https://github.com/jkeen">Jeff Keen</a>',
185
+ 'with <a href="https://github.com/graphiti-api/graphiti/graphs/contributors">many contributors</a>.',
186
+ '<br />Graphiti is released under the MIT license.',
187
+ ].join(' '),
188
+ },
189
+ prism: {
190
+ theme: withoutDiffDecoration(themes.oneLight),
191
+ darkTheme: withoutDiffDecoration(codeBlockTheme(themes.oneDark, '#21252b')),
192
+ additionalLanguages: ['ruby', 'bash', 'json', 'http', 'diff'],
193
+ },
138
194
  },
139
195
  };
140
196