Skip to content

feat(cms): Attachments SDK helper + facade (record file attachments, PR-1) - #6370

Merged
delchev merged 1 commit into
masterfrom
feat/cms-attachments-sdk
Jul 22, 2026
Merged

feat(cms): Attachments SDK helper + facade (record file attachments, PR-1)#6370
delchev merged 1 commit into
masterfrom
feat/cms-attachments-sdk

Conversation

@delchev

@delchev delchev commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

First of the record-attachments series (design: kf-catalog PROPOSAL_ATTACHMENTS — attachments modeled as a function: Attachment composition child, bytes in the CMS). This PR is the reusable storage primitive, independently buildable and unit-tested; the intent function: Attachment + EDM injection (PR-2), the generated controller upload/download/delete verbs (PR-3), and the Harmonia Attachments panel (PR-4) build on it.

What's here

  • AttachmentsFacade (api-cms) — store(masterEntity, fileName, contentType, bytes) -> StoredAttachment{path,name,contentType,size,uuid}, read(path) -> InputStream, delete(path). Stores under /Attachments/<Master>/<yyyy>/<MM>/<uuid>/<file> (entity → date → uuid → file), reusing the engine-document CmsStore folder-ensure + createContentStream pattern over the existing CmisSessionFactory session — so writes land in the caller's tenant CMS and are browseable in the Documents perspective. No new engine.
  • AttachmentPath (api-cms) — pure, deterministic path builder (date + uuid supplied), with master-name + file-name path-safety (traversal/separators stripped). Unit-tested.
  • sdk.cms.Attachments (api-modules-java) — the client-facing delegate the generated controllers / custom/ code call; returns a plain Attachment record (no engine types leaked).

Verification

  • AttachmentPathTest — 6 tests green (layout, zero-padded month, traversal/separator stripping, blanks, master sanitization).
  • api-cms + api-modules-java compile against the real CMS types (CmisSession/CmisFolder/CmisContentStream/CmisObject.delete); release-profile javadoc clean.
  • The CMS round-trip (store→read bytes) is exercised end-to-end by the generated controller's IT in PR-3 (a request tenant scope makes it a clean HTTP test); the facade write path here mirrors the shipped, working CmsStore.

🤖 Generated with Claude Code

…ments in the CMS

The foundation (PR-1) for record file attachments: an AttachmentsFacade (api-cms) that
stores an uploaded file in the tenant CMS under the structured path
/Attachments/<Master>/<yyyy>/<MM>/<uuid>/<file> and reads/deletes it (reusing the
engine-document CmsStore folder-ensure + content-stream pattern), plus the client-facing
sdk.cms.Attachments delegate (store -> {path,name,contentType,size,uuid}; open(path) ->
stream; delete). Path building is a pure, unit-tested AttachmentPath (entity/date/uuid/file,
with master + file-name path-safety). No new engine — reuses the existing CmisSessionFactory
session, so writes land in the caller's tenant store.

This is the reusable primitive the generated attachment controller verbs (a later PR, once
function: Attachment marks the child entity) call; see kf-catalog PROPOSAL_ATTACHMENTS.
Unit test green (AttachmentPathTest, 6); both modules compile + release javadoc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@delchev
delchev merged commit f1b2adf into master Jul 22, 2026
10 checks passed
@delchev
delchev deleted the feat/cms-attachments-sdk branch July 22, 2026 08:17
delchev added a commit that referenced this pull request Jul 22, 2026
…d (end-to-end)

A `function: Attachment` entity is a first-class composition child of its master (native
1:n, master-detail wiring unchanged), turning file attachments into a modeled, type-safe
detail rather than a bespoke subsystem. Builds on the merged CMS Attachments SDK (#6370).

Intent/EDM (engine-intent):
- `attachment` added to IntentParser ENTITY_FUNCTIONS; EntityIntent.isAttachment().
- EdmIntentGenerator injects the standard file-metadata columns (FileName [major],
  ContentType, FileSize [BIGINT], StoragePath, Uuid — all read-only, upload-set) + implicit
  audit, and marks the entity attachmentEntity="true". The author declares no primary key on
  an attachment child, so a generated integer Id is synthesized (otherwise the entity/
  controller would have no PK).

Controller (template-application-rest-java):
- Gated on attachmentEntity, the generated controller gains POST /upload (multipart →
  stores each file in the tenant CMS at /Attachments/<Master>/<yyyy>/<MM>/<uuid>/<file>,
  persists one row per file with the reference + metadata), GET /{id}/download (permission-
  scoped stream, never a raw documents?path=), and deleteById also removes the CMS file.

SDK (api-modules-java):
- Attachments.storeUploads(master) reads the servlet parts Spring's multipart resolver has
  already parsed (the controller is served by a Spring @PostMapping, which consumes the body
  before commons-fileupload could), so the generated controller only ever sees Attachment.

Test: EdmIntentGeneratorTest asserts the injected metadata + Id PK; RecordAttachmentIT drives
the full HTTP round-trip (multipart upload → verbatim download → delete → gone) through the
real multipart servlet path and tenant CMS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delchev added a commit that referenced this pull request Jul 22, 2026
…d (end-to-end) (#6371)

A `function: Attachment` entity is a first-class composition child of its master (native
1:n, master-detail wiring unchanged), turning file attachments into a modeled, type-safe
detail rather than a bespoke subsystem. Builds on the merged CMS Attachments SDK (#6370).

Intent/EDM (engine-intent):
- `attachment` added to IntentParser ENTITY_FUNCTIONS; EntityIntent.isAttachment().
- EdmIntentGenerator injects the standard file-metadata columns (FileName [major],
  ContentType, FileSize [BIGINT], StoragePath, Uuid — all read-only, upload-set) + implicit
  audit, and marks the entity attachmentEntity="true". The author declares no primary key on
  an attachment child, so a generated integer Id is synthesized (otherwise the entity/
  controller would have no PK).

Controller (template-application-rest-java):
- Gated on attachmentEntity, the generated controller gains POST /upload (multipart →
  stores each file in the tenant CMS at /Attachments/<Master>/<yyyy>/<MM>/<uuid>/<file>,
  persists one row per file with the reference + metadata), GET /{id}/download (permission-
  scoped stream, never a raw documents?path=), and deleteById also removes the CMS file.

SDK (api-modules-java):
- Attachments.storeUploads(master) reads the servlet parts Spring's multipart resolver has
  already parsed (the controller is served by a Spring @PostMapping, which consumes the body
  before commons-fileupload could), so the generated controller only ever sees Attachment.

Test: EdmIntentGeneratorTest asserts the injected metadata + Id PK; RecordAttachmentIT drives
the full HTTP round-trip (multipart upload → verbatim download → delete → gone) through the
real multipart servlet path and tenant CMS.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
delchev added a commit that referenced this pull request Jul 22, 2026
…nt UI) (#6373)

* feat(intent): function: Attachment — file-attachment composition child (end-to-end)

A `function: Attachment` entity is a first-class composition child of its master (native
1:n, master-detail wiring unchanged), turning file attachments into a modeled, type-safe
detail rather than a bespoke subsystem. Builds on the merged CMS Attachments SDK (#6370).

Intent/EDM (engine-intent):
- `attachment` added to IntentParser ENTITY_FUNCTIONS; EntityIntent.isAttachment().
- EdmIntentGenerator injects the standard file-metadata columns (FileName [major],
  ContentType, FileSize [BIGINT], StoragePath, Uuid — all read-only, upload-set) + implicit
  audit, and marks the entity attachmentEntity="true". The author declares no primary key on
  an attachment child, so a generated integer Id is synthesized (otherwise the entity/
  controller would have no PK).

Controller (template-application-rest-java):
- Gated on attachmentEntity, the generated controller gains POST /upload (multipart →
  stores each file in the tenant CMS at /Attachments/<Master>/<yyyy>/<MM>/<uuid>/<file>,
  persists one row per file with the reference + metadata), GET /{id}/download (permission-
  scoped stream, never a raw documents?path=), and deleteById also removes the CMS file.

SDK (api-modules-java):
- Attachments.storeUploads(master) reads the servlet parts Spring's multipart resolver has
  already parsed (the controller is served by a Spring @PostMapping, which consumes the body
  before commons-fileupload could), so the generated controller only ever sees Attachment.

Test: EdmIntentGeneratorTest asserts the injected metadata + Id PK; RecordAttachmentIT drives
the full HTTP round-trip (multipart upload → verbatim download → delete → gone) through the
real multipart servlet path and tenant CMS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(harmonia): Attachments / Snapshot files panel for function:Attachment children

Renders a composition child declared `function: Attachment` (or, forthcoming, `function:
Snapshot`) as a purpose-built Files panel instead of a generic row table — the UI half of
the record-attachments feature (backend in #6371, storage SDK in #6370).

Shared runtime (application-core):
- detailPanel gains a `files` mode (parallel to the existing `calendar` mode): lists the
  master-filtered files with per-file Download (a plain browser GET to the controller's
  `/{id}/download`), and — unless read-only — an Upload control (multipart POST to `/upload`)
  and per-file Remove (reuses the detail delete, which also drops the CMS file). Like a
  calendar, a files panel always renders (its empty state is meaningful).
- api client: request() now sends a FormData body as-is (browser sets the multipart boundary)
  instead of JSON-encoding it, so the upload goes through the same client + auth/error path.

Harmonia templates:
- detail-register emits `files: { readOnly: <attachmentReadOnly> }` for an `attachmentEntity`
  child (read-only flips it to download-only — the generated Snapshot case).
- form-view (editable), document-view (editable), master-view (read-only browse) each render
  the files branch: an x-h-info-page empty state + file cards (name/size/Download[/Remove]),
  gated on `def.files`; the row table + Add button are suppressed for a files detail. $refs is
  ${dollar}-escaped so Velocity leaves the Alpine ref alone.

Editable path verified end-to-end on a generated CompanyAttachment (upload → master-filtered
list → verbatim download → remove). Read-only (Snapshot) branch is in place, activated once the
`attachmentReadOnly` marker lands with `function: Snapshot`.

Depends on #6371 (function: Attachment backend + controller verbs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
delchev added a commit that referenced this pull request Jul 22, 2026
…opy on issue

Part D of the Sales-Invoice snapshot flow: generates the JavaDelegate a process wires as a
`delegate:` service task to mint an immutable printed copy of a document on issue (the number
stays across amendments; only the snapshot Version increments).

- SnapshotSupport builds the `snapshots` glue collection — one descriptor per function: Snapshot
  child of a DOCUMENT master (which has a PrintFeeder to render from): master, master PK (the
  process variable), the snapshot entity/perspective/master-FK, language.
- GlueIntentGenerator emits the `snapshots` collection into the .glue.
- generateUtils adds the `snapshots` case (sanitizes the perspective → javaPerspective, its own loop
  like printFeeders/settlements).
- Snapshot.java.template generates gen/events/<Master>SnapshotGenerator.java: reads the master id
  from the process variable, reuses the master's generated PrintFeeder (same gen.events package) for
  the {document, items} payload, renders server-side via sdk.print.Print (#6376), stores the PDF via
  sdk.cms.Attachments (#6370), and writes a <Snapshot> row with Version = max existing + 1 (gap-free
  — snapshots are immutable). Bound by the BPMN via the existing `delegate:` (flowable:class) path —
  no new parser/BPMN wiring, and ServiceTaskHandlerGenerator already skips a delegate: task.

Verified: on the SalesInvoice document master with a function: Snapshot child + a `generateSnapshot`
delegate step, the .glue carries the snapshots descriptor, gen/events/SalesInvoiceSnapshotGenerator
.java is emitted, and it compiles cleanly into the client bean container (538 beans, no javac errors).

Depends on: #6374 (function: Snapshot type — the PR base), #6376 (sdk.print.Print), #6371/#6370
(sdk.cms.Attachments). Follow-up: KF adoption wires generateSnapshot into the SalesInvoice process
after markIssued + the Confirm/Amend loop; a full process-run e2e mints the first snapshot on issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delchev added a commit that referenced this pull request Jul 22, 2026
…opy on issue (#6377)

* feat(intent): function: Snapshot — generated read-only versioned record copies

A sibling of function: Attachment (#6371) for SYSTEM-GENERATED copies (e.g. the printed
invoice stored on issue) rather than user-uploaded files. Same composition-child + CMS +
file-metadata machinery, but read-only and versioned.

- IntentParser: `snapshot` added to ENTITY_FUNCTIONS.
- EntityIntent: isSnapshot() + isFileChild() (attachment or snapshot — shared injection).
- EdmIntentGenerator: a file-child gets the injected metadata columns + synthesized Id + audit
  (now keyed on isFileChild). A Snapshot additionally gets a read-only, major `Version` column
  carrying the new DOCUMENT_VERSION widget (the copy's sequence within its master, for special
  treatment in forms/print), and is marked attachmentReadOnly="true" alongside attachmentEntity.
- EntityController template: the `/upload` verb is gated off when attachmentReadOnly — a Snapshot
  controller exposes download + list only (no upload; copies are created server-side by the
  forthcoming generateSnapshot serviceTask). The read-only Harmonia Files panel (#6373) already
  keys on attachmentReadOnly (download only, no upload/remove).

Verified: EdmIntentGeneratorTest (metadata + Id + Version/DOCUMENT_VERSION + attachmentReadOnly);
a generated CompanySnapshotController has @get /{id}/download and NO @post /upload, and the model
carries Version(DOCUMENT_VERSION) + attachmentReadOnly=true.

Part A of the Sales-Invoice snapshot flow. Follow-ups: C (server-side print render) and D
(generateSnapshot BPM serviceTask + delegate, mint on issue). Depends on #6371.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(intent): generateSnapshot delegate — render + store a document copy on issue

Part D of the Sales-Invoice snapshot flow: generates the JavaDelegate a process wires as a
`delegate:` service task to mint an immutable printed copy of a document on issue (the number
stays across amendments; only the snapshot Version increments).

- SnapshotSupport builds the `snapshots` glue collection — one descriptor per function: Snapshot
  child of a DOCUMENT master (which has a PrintFeeder to render from): master, master PK (the
  process variable), the snapshot entity/perspective/master-FK, language.
- GlueIntentGenerator emits the `snapshots` collection into the .glue.
- generateUtils adds the `snapshots` case (sanitizes the perspective → javaPerspective, its own loop
  like printFeeders/settlements).
- Snapshot.java.template generates gen/events/<Master>SnapshotGenerator.java: reads the master id
  from the process variable, reuses the master's generated PrintFeeder (same gen.events package) for
  the {document, items} payload, renders server-side via sdk.print.Print (#6376), stores the PDF via
  sdk.cms.Attachments (#6370), and writes a <Snapshot> row with Version = max existing + 1 (gap-free
  — snapshots are immutable). Bound by the BPMN via the existing `delegate:` (flowable:class) path —
  no new parser/BPMN wiring, and ServiceTaskHandlerGenerator already skips a delegate: task.

Verified: on the SalesInvoice document master with a function: Snapshot child + a `generateSnapshot`
delegate step, the .glue carries the snapshots descriptor, gen/events/SalesInvoiceSnapshotGenerator
.java is emitted, and it compiles cleanly into the client bean container (538 beans, no javac errors).

Depends on: #6374 (function: Snapshot type — the PR base), #6376 (sdk.print.Print), #6371/#6370
(sdk.cms.Attachments). Follow-up: KF adoption wires generateSnapshot into the SalesInvoice process
after markIssued + the Confirm/Amend loop; a full process-run e2e mints the first snapshot on issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant