Module: RivetCms

Defined in:
lib/rivet_cms/seeds.rb,
lib/rivet_cms.rb,
lib/rivet_cms/audit.rb,
lib/rivet_cms/hooks.rb,
lib/rivet_cms/engine.rb,
lib/rivet_cms/version.rb,
lib/rivet_cms/navigation.rb,
app/models/rivet_cms/user.rb,
app/models/rivet_cms/entry.rb,
app/models/rivet_cms/field.rb,
lib/rivet_cms/access_check.rb,
lib/rivet_cms/safe_pattern.rb,
app/models/rivet_cms/current.rb,
app/models/rivet_cms/category.rb,
app/models/rivet_cms/document.rb,
app/models/rivet_cms/relation.rb,
app/models/rivet_cms/api_token.rb,
app/models/rivet_cms/component.rb,
app/services/rivet_cms/webhooks.rb,
app/models/rivet_cms/media_asset.rb,
app/models/rivet_cms/content_type.rb,
app/models/rivet_cms/organization.rb,
app/jobs/rivet_cms/application_job.rb,
app/models/rivet_cms/content_value.rb,
app/services/rivet_cms/draft_writer.rb,
app/services/rivet_cms/content_query.rb,
app/models/rivet_cms/entry_collection.rb,
app/helpers/rivet_cms/dashboard_helper.rb,
app/models/rivet_cms/document_revision.rb,
app/services/rivet_cms/revision_pruner.rb,
app/helpers/rivet_cms/components_helper.rb,
app/jobs/rivet_cms/webhook_delivery_job.rb,
app/models/concerns/rivet_cms/sluggable.rb,
app/models/rivet_cms/application_record.rb,
app/models/rivet_cms/component_instance.rb,
app/helpers/rivet_cms/application_helper.rb,
app/mailers/rivet_cms/application_mailer.rb,
app/models/concerns/rivet_cms/has_fields.rb,
app/services/rivet_cms/content_validator.rb,
app/controllers/rivet_cms/auth_controller.rb,
app/models/concerns/rivet_cms/typed_value.rb,
app/services/rivet_cms/open_api_generator.rb,
app/controllers/rivet_cms/setup_controller.rb,
app/controllers/rivet_cms/trash_controller.rb,
app/controllers/rivet_cms/users_controller.rb,
app/helpers/rivet_cms/content_types_helper.rb,
app/controllers/rivet_cms/fields_controller.rb,
app/controllers/rivet_cms/content_controller.rb,
app/models/concerns/rivet_cms/revision_owned.rb,
app/models/concerns/rivet_cms/soft_deletable.rb,
app/serializers/rivet_cms/revision_preloader.rb,
app/controllers/rivet_cms/api_docs_controller.rb,
app/controllers/rivet_cms/sessions_controller.rb,
app/serializers/rivet_cms/revision_serializer.rb,
app/controllers/rivet_cms/dashboard_controller.rb,
app/controllers/rivet_cms/documents_controller.rb,
app/controllers/rivet_cms/api_tokens_controller.rb,
app/controllers/rivet_cms/components_controller.rb,
app/controllers/concerns/rivet_cms/inertia_props.rb,
app/controllers/rivet_cms/application_controller.rb,
app/controllers/rivet_cms/invitations_controller.rb,
app/controllers/rivet_cms/media_assets_controller.rb,
app/models/concerns/rivet_cms/organization_scoped.rb,
app/controllers/rivet_cms/content_types_controller.rb,
lib/generators/rivet_cms/install/install_generator.rb,
app/controllers/rivet_cms/content_manager_controller.rb

Overview

Content-type templates: reusable schema (content types, fields, components, categories) that a host app can load into an organization as a starting point. Templates live in db/seeds/templates and are applied idempotently, so loading the same template twice upserts rather than duplicating — safe in production.

Defined Under Namespace

Modules: ApplicationHelper, Audit, ComponentsHelper, ContentTypesHelper, DashboardHelper, Generators, HasFields, Hooks, InertiaProps, Navigation, OrganizationScoped, RevisionOwned, SafePattern, Seeds, Sluggable, SoftDeletable, TypedValue, Webhooks Classes: AccessCheck, AccessDenied, ApiDocsController, ApiToken, ApiTokensController, ApplicationController, ApplicationJob, ApplicationMailer, ApplicationRecord, AuditEvent, AuthController, Category, Component, ComponentInstance, ComponentsController, ContentController, ContentInvalidError, ContentManagerController, ContentQuery, ContentType, ContentTypesController, ContentValidator, ContentValue, Current, DashboardController, Document, DocumentRevision, DocumentsController, DraftWriter, Engine, Entry, EntryCollection, Field, FieldsController, InvitationsController, MediaAsset, MediaAssetsController, OpenApiGenerator, Organization, Relation, RemovedContentTypeError, RevisionImmutableError, RevisionPreloader, RevisionPruner, RevisionSerializer, SessionsController, SetupController, TrashController, TrashedEntryError, User, UsersController, WebhookDeliveryJob

Constant Summary collapse

DEFAULT_AUTHENTICATE =

authenticate returns truthy to allow the request and falsy to deny it; the engine turns a denial into a redirect to login_path or a 403 (a lambda may also render/redirect itself). Unconfigured means built-in authentication: this lambda is an identity sentinel for builtin_auth? and is never called (the controller branches to the built-in session flow first). Fails closed if something ever invokes it anyway.

->(_controller) { false }
VERSION =
"0.3.1"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.admin_scriptsObject



54
55
56
# File 'lib/rivet_cms.rb', line 54

def admin_scripts
  @admin_scripts ||= []
end

.admin_stylesheetsObject



58
59
60
# File 'lib/rivet_cms.rb', line 58

def admin_stylesheets
  @admin_stylesheets ||= []
end

.allowed_media_typesObject

MIME types the media library accepts, checked against the sniffed type, not the client-declared one. nil disables the check. SVG is excluded by default because scripted SVGs are an XSS vector; hosts that trust their editors can append "image/svg+xml".



23
24
25
# File 'lib/rivet_cms.rb', line 23

def allowed_media_types
  @allowed_media_types
end

.authenticateObject

Returns the value of attribute authenticate.



113
114
115
# File 'lib/rivet_cms.rb', line 113

def authenticate
  @authenticate
end

.canObject

Authorization seam: receives one RivetCms::AccessCheck and returns a boolean; default allow. check.action is :read, :write, :publish, or :delete; check.resource is a coarse domain (:content, :schema, :media, :api). The vocabulary grows over time, so policies should allowlist known pairs and deny anything unrecognized. A raising policy denies (fail closed) and logs. Governs the admin UI only; the delivery API is token-gated separately.



108
109
110
# File 'lib/rivet_cms.rb', line 108

def can
  @can
end

.current_userObject

Returns the value of attribute current_user.



114
115
116
# File 'lib/rivet_cms.rb', line 114

def current_user
  @current_user
end

.login_pathObject

Returns the value of attribute login_path.



117
118
119
# File 'lib/rivet_cms.rb', line 117

def 
  @login_path
end

.logout_methodObject

Returns the value of attribute logout_method.



119
120
121
# File 'lib/rivet_cms.rb', line 119

def logout_method
  @logout_method
end

.logout_pathObject

Returns the value of attribute logout_path.



118
119
120
# File 'lib/rivet_cms.rb', line 118

def logout_path
  @logout_path
end

.max_upload_sizeObject

Hard ceiling for library uploads (bytes); hosts can override in an initializer.



17
18
19
# File 'lib/rivet_cms.rb', line 17

def max_upload_size
  @max_upload_size
end

.media_hostObject

Host (e.g. "https://cms.example.com") used to build absolute media URLs in the public API. When nil, URLs are relative paths.



27
28
29
# File 'lib/rivet_cms.rb', line 27

def media_host
  @media_host
end

.parent_controllerObject

Authentication is delegated to the host app. See the initializer template (rails g rivet_cms:install) for a full example.



112
113
114
# File 'lib/rivet_cms.rb', line 112

def parent_controller
  @parent_controller
end

.public_apiObject

When true the delivery API allows anonymous reads of published content. When false (default) every request needs an API token. A preview-scoped token is always required to read drafts, regardless of this setting.



74
75
76
# File 'lib/rivet_cms.rb', line 74

def public_api
  @public_api
end

.revision_retentionObject

How many superseded published revisions to keep per document. The default :all never deletes anything: pruning is something a host opts into, because unbounded storage is a visible problem you can fix later and deleted content is not. Set an integer to prune on publish. The current published revision and the working draft are never pruned.



81
82
83
# File 'lib/rivet_cms.rb', line 81

def revision_retention
  @revision_retention
end

.setup_codeObject

Derived, not memoized: memoizing would make the explicit-config check below true after any read



177
178
179
# File 'lib/rivet_cms.rb', line 177

def setup_code
  @setup_code || Digest::SHA256.hexdigest("#{Rails.application.secret_key_base}:rivet-cms-setup")[0, 12]
end

.user_emailObject

Returns the value of attribute user_email.



116
117
118
# File 'lib/rivet_cms.rb', line 116

def user_email
  @user_email
end

.user_nameObject

Returns the value of attribute user_name.



115
116
117
# File 'lib/rivet_cms.rb', line 115

def user_name
  @user_name
end

.webhooksObject

Basic webhook endpoints: [{ url: "https://...", events: %w[entry.published] }]. events is optional (defaults to all). Delivered by WebhookDeliveryJob; no signing or retries.



32
33
34
# File 'lib/rivet_cms.rb', line 32

def webhooks
  @webhooks
end

Class Method Details

.asset_versionObject

Digest of the precompiled admin assets, used as the Inertia asset version so clients do a full reload when the gem ships a new build. Recomputed each request in development so a rebuild auto-reloads the browser; memoized elsewhere (assets are static at runtime).



289
290
291
292
293
# File 'lib/rivet_cms.rb', line 289

def self.asset_version
  return compute_asset_version if Rails.env.development?

  @asset_version ||= compute_asset_version
end

.builtin_auth?Boolean

Built-in authentication is the default: it is active exactly while the host has not configured its own authenticate lambda. Setting one (as every existing host integration does) opts out entirely.

Returns:

  • (Boolean)


165
166
167
# File 'lib/rivet_cms.rb', line 165

def builtin_auth?
  authenticate.equal?(DEFAULT_AUTHENTICATE)
end

.compute_asset_versionObject



295
296
297
298
299
300
301
302
303
# File 'lib/rivet_cms.rb', line 295

def self.compute_asset_version
  build_path = Engine.root.join("app/assets/builds")
  asset_digests = %w[rivet_cms.js rivet_cms.css].filter_map do |filename|
    asset = build_path.join(filename)
    Digest::MD5.file(asset).hexdigest if asset.exist?
  end

  asset_digests.any? ? Digest::MD5.hexdigest([ VERSION, *asset_digests ].join(":")) : VERSION
end

.configure {|_self| ... } ⇒ Object

Yields:

  • (_self)

Yield Parameters:

  • _self (RivetCms)

    the object that the method was called on



121
122
123
# File 'lib/rivet_cms.rb', line 121

def configure
  yield self
end

.entries(type_slug, organization: nil, **options) ⇒ Object

Published entries of a content type. Options mirror the API's list params; populate accepts an array of keys or :all.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/rivet_cms.rb', line 131

def entries(type_slug, organization: nil, **options)
  content_type = find_content_type!(type_slug, organization)
  query = ContentQuery.new(content_type, **options)
  populate = query.populate_fields

  page = query.documents
  revisions = page.map(&:published_revision)
  preload = RevisionPreloader.new(revisions, populate_fields: populate)

  wrapped = revisions.map do |revision|
    Entry.new(RevisionSerializer.new(revision, fields: query.field_keys, populate: populate, preload: preload).as_json)
  end
  EntryCollection.new(wrapped, page: page.current_page, per_page: page.limit_value,
                               total: page.total_count, total_pages: page.total_pages)
end

.entry(type_slug, entry_slug, organization: nil, preview: false, populate: nil, fields: nil) ⇒ Object

One entry by slug, or nil. preview: true serves the draft when present (Ruby callers are trusted host code — no token gate).



149
150
151
152
153
# File 'lib/rivet_cms.rb', line 149

def entry(type_slug, entry_slug, organization: nil, preview: false, populate: nil, fields: nil)
  content_type = find_content_type!(type_slug, organization)
  document = content_type.documents.find_by(slug: entry_slug)
  serialize_document(content_type, document, preview: preview, populate: populate, fields: fields)
end

.normalized_retention_for(document) ⇒ Object

What the pruner actually reads: an override is validated the same way the config setter is, so a bad override cannot silently destroy history.



97
98
99
# File 'lib/rivet_cms.rb', line 97

def normalized_retention_for(document)
  normalize_retention(retention_for(document))
end

.on(event, callable = nil, key: nil, &block) ⇒ Object

Subscribe to a lifecycle event; see RivetCms::Hooks for the event list and the key: contract for reload-safe registration. RivetCms.on(:publish) { |revision| ... }



67
68
69
# File 'lib/rivet_cms.rb', line 67

def on(event, callable = nil, key: nil, &block)
  Hooks.on(event, callable, key: key, &block)
end

.register_admin_script(name) ⇒ Object

Extra admin bundles the layout emits after core's tags. An extension's precompiled JS loads in order and can register its pages on window.RivetCMS before the admin app boots.

Raises:

  • (ArgumentError)


42
43
44
45
46
# File 'lib/rivet_cms.rb', line 42

def register_admin_script(name)
  raise ArgumentError, "asset name required" if name.to_s.strip.empty?

  admin_scripts << name.to_s unless admin_scripts.include?(name.to_s)
end

.register_admin_stylesheet(name) ⇒ Object

Raises:

  • (ArgumentError)


48
49
50
51
52
# File 'lib/rivet_cms.rb', line 48

def register_admin_stylesheet(name)
  raise ArgumentError, "asset name required" if name.to_s.strip.empty?

  admin_stylesheets << name.to_s unless admin_stylesheets.include?(name.to_s)
end

.register_nav(key, **options) ⇒ Object

Add a sidebar item; see RivetCms::Navigation for the full contract.



35
36
37
# File 'lib/rivet_cms.rb', line 35

def register_nav(key, **options)
  Navigation.register(key, **options)
end

.retention_for(_document) ⇒ Object

Retention for one document. Override to vary by organization or content type; the scalar config is the default resolver.



91
92
93
# File 'lib/rivet_cms.rb', line 91

def retention_for(_document)
  revision_retention
end

.setup_code_required?Boolean

Returns:

  • (Boolean)


181
182
183
# File 'lib/rivet_cms.rb', line 181

def setup_code_required?
  !(Rails.env.development? || Rails.env.test?) || @setup_code.present?
end

.single(type_slug, organization: nil, preview: false, populate: nil, fields: nil) ⇒ Object

The one entry of a single-type content type, or nil.



156
157
158
159
160
# File 'lib/rivet_cms.rb', line 156

def single(type_slug, organization: nil, preview: false, populate: nil, fields: nil)
  content_type = find_content_type!(type_slug, organization)
  document = content_type.documents.find_by(singleton_key: "singleton") || content_type.documents.first
  serialize_document(content_type, document, preview: preview, populate: populate, fields: fields)
end