Module: Basecamp::Services::DocumentsExtensions

Defined in:
lib/basecamp/services/documents_extensions.rb

Overview

Merge-safe update and read-modify-write edit for documents, prepended onto the generated DocumentsService (see the on_load hook in basecamp.rb).

BC3's DocumentsController#update builds a brand-new Document from only the permitted params and swaps the recordable wholesale, so PUT /documents/{id} is a full replace: a body that omits content ERASES it, and one that omits title erases that too — the document then reads back as "Untitled", because Document#title falls back when blank. Neither attribute is presence-validated, so neither omission is a 422; both are a 200 that quietly clears. What BC3 does require is the wrapping document object, so a body naming neither field is a 400. The sparse PUT — the natural thing to write — is therefore destructive on the raw endpoint, which stays available as #replace.

Both compose the public get and replace methods, so hooks observe the two wire operations (+get+ then replace), not a synthetic composite.

Neither is atomic: there is no conditional-update signal on this endpoint, so a concurrent write between the GET and PUT is overwritten — last write wins for the whole representation. The window is one round-trip. Use replace to overwrite deliberately.

Defined Under Namespace

Classes: DocumentFields

Constant Summary collapse

ESCAPE_HATCH =

The deliberate-overwrite escape hatch named in every malformed-response hint raised out of this composite.

"replace"

Instance Method Summary collapse

Instance Method Details

#edit(document_id:) {|fields| ... } ⇒ Hash

Applies a read-modify-write block to a document: GETs the current document, yields its full writable state (DocumentFields), and PUTs the whole thing back. Clearing a field means setting it empty ("") — an untouched field keeps its current value. If the block raises, the edit aborts and nothing is written.

Not atomic — see the module docs for the GET→PUT race.

Examples:

.documents.edit(document_id: 123) do |doc|
  doc.title = "🚨 #{doc.title}"
  doc.content = "" # clearing = setting empty on a full object
end

Parameters:

  • document_id (Integer)

    document id

Yield Parameters:

  • fields (DocumentFields)

    the document's writable state, to mutate in place

Returns:

  • (Hash)

    the updated document

Raises:

  • (ArgumentError)

    if no block is given



78
79
80
81
82
83
84
# File 'lib/basecamp/services/documents_extensions.rb', line 78

def edit(document_id:)
  raise ArgumentError, "edit requires a block" unless block_given?

  fields = fields_from_document(get(document_id: document_id))
  yield fields
  put_fields(document_id, fields)
end

#update(document_id:, title: nil, content: nil) ⇒ Hash

Sets the given fields on a document and preserves everything else: GETs the current document, overlays the explicitly-passed keyword arguments, and PUTs the full representation back. An omitted (+nil+) field is untouched, guaranteed; an explicitly-passed "" clears.

Not atomic — see the module docs for the GET→PUT race. Use #replace to overwrite deliberately.

Parameters:

  • document_id (Integer)

    document id

  • title (String, nil) (defaults to: nil)

    new title (nil = keep current, "" clears)

  • content (String, nil) (defaults to: nil)

    new content (nil = keep current, "" clears)

Returns:

  • (Hash)

    the updated document



53
54
55
56
57
58
# File 'lib/basecamp/services/documents_extensions.rb', line 53

def update(document_id:, title: nil, content: nil)
  fields = fields_from_document(get(document_id: document_id))
  fields.title = title unless title.nil?
  fields.content = content unless content.nil?
  put_fields(document_id, fields)
end