Skip to content
Merged
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
2 changes: 1 addition & 1 deletion GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ a new concept gets its line here first.
| **Edit command** | One change a caller asks for, named after what it does: a model command (`AddElement`, `ConnectElements`, `SetProperties`, `RemoveElements`, `ChangeElementType`, `MoveElements`, `ReorderElements`, `EditExtensionElements`, `SplitIntoBranches`) or a diagram command (`PlaceShapes`, `AlignShapes`, `ColorElements`, `PlaceLabel`, `RerouteFlow`, `InsertSpace`, `TidyShapes`, `CompactDiagram`); the ops JSON of `apply` spells them as ops (`"op": "add"`) (`domain/commands/types.ts`). |
| **Use case** | The contract of one command line verb or API function: an interface with its `Command` and `Result` in `application/port/in/` (`EditDocumentUseCase`, `CreateDocumentUseCase`, `RelayoutDiagramUseCase`, `ValidateDocumentUseCase`, `ReadDocumentUseCase`, `ShowModelUseCase`, `ShowConventionsUseCase`, `FindElementsUseCase`, `RenderDiagramUseCase`, `RenderSvgUseCase`, `RunCommandsUseCase`, `AnalyzeLayoutUseCase`), implemented by the service of the same name in `application/service/` (`EditDocumentService`, ...). Every public function of the library that works on a document goes through one, even where the service only forwards to the domain. |
| **Service** | A class named `...Service`: in the domain, work the commands share or run (`ElementCreationService`, `SequenceFlowService`, ...); in `application/service/`, the implementation of one use case. |
| **Port** / **adapter** | The way in (`application/port/in/`, the use cases) and out (`application/port/out/`: `LoadDocumentPort`, `SaveDocumentPort`, `LoadProjectSettingsPort`, `WriteImagePort`) / what drives the application (`adapters/in/cli/`) or provides an outbound port (`adapters/out/files/` on disk, `adapters/out/memory/` for the in-memory API); only the entries wire them together. |
| **Port** / **adapter** | The way in (`application/port/in/`, the use cases) and out (`application/port/out/`: `LoadDocumentPort`, `SaveDocumentPort`, `LoadProjectSettingsPort`, `WriteImagePort`) / what drives the application (`adapters/in/cli/`) or provides an outbound port (`adapters/out/files/` on disk, `adapters/out/memory/` for the in-memory API, which has no project settings and passes no document ports); only the entries wire them together. |
| **Batch** | The commands of one `apply` call (or of one changing command), run on the document in memory in their order: a diagram command runs after the layout of the model commands before it. Guard, commands, validation, layout, validators, result, all or nothing (`domain/batch/EditBatchService.ts`); the use case then saves the result through `SaveDocumentPort`. |
| **Edit context** | The state of one edit: the call's settings (id conventions, platform, kept flow ids) and what the running batch recorded (`domain/document/EditContext.ts`). The editing services receive it next to the document; `new EditContext(doc)` starts a fresh one. |
| **Model command service** | Hands each model command of a batch to its domain service in an edit context; a batch runs in segments, the drawing brought up to date between them (`domain/batch/ModelCommandService.ts`). |
Expand Down
21 changes: 11 additions & 10 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ flowchart TB
service --> domain["Domain<br/>domain/"]
service --> portOut{{"Port<br/>application/port/out"}}
portOut --> files["Files<br/>adapters/out/files"]
portOut --> memory["Memory<br/>adapters/out/memory"]
portOut --> memory["No project<br/>adapters/out/memory"]

classDef adapter stroke:#2f80ed,stroke-width:2px
classDef app stroke:#f2994a,stroke-width:2px
Expand All @@ -42,7 +42,7 @@ container.
| rule (checked by the test) | why |
| --- | --- |
| `domain/` imports no application, adapter or entry | the domain runs anywhere, also in a browser |
| `application/` imports no adapter | a service does not know whether it writes to disk or to memory |
| `application/` imports no adapter | a service does not know whether it writes to disk or does not write at all |
| only adapters and entries import `node:*` | the browser bundle stays free of Node |
| `adapters/in` knows use cases only by their interface | the entry decides which service is behind it |
| each service implements one use case and calls no other service | one way in, no hidden orchestration |
Expand Down Expand Up @@ -158,22 +158,23 @@ export declare namespace EditDocumentUseCase {
### 3. Load, run, save

The service is short on purpose: get the document, let the domain work, write the result.
`load`, `save` and `settings` are outbound ports, handed in by the constructor.
`settings`, `load` and `save` are outbound ports, handed in by the constructor; without `load`
or `save` the service takes only a document and writes nothing.

```ts
// src/application/service/EditDocumentService.ts
export class EditDocumentService implements EditDocumentUseCase {
constructor(
private readonly load: LoadDocumentPort,
private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
private readonly load?: LoadDocumentPort,
private readonly save?: SaveDocumentPort,
) {}

async edit({ source, commands, ...opts }: EditDocumentUseCase.Command) {
// step 7: read the file
const doc = 'doc' in source
? source.doc
: await Doc.fromXml(await this.load.read(source.file), source.file)
: await Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file)
const target = opts.out ?? doc.file

// the project's .bpmnlintrc, if any
Expand All @@ -186,15 +187,15 @@ export class EditDocumentService implements EditDocumentUseCase {

// step 7: write the file
if (!opts.dryRun) {
result.written = await this.save.save(target, result.xml, { … })
result.written = await this.savePort().save(target, result.xml, { … })
}
return result
}
}
```

In the CLI the ports are a `FileDocumentStore` and `BpmnlintProjectSettings`; in `applyToXml()`
they are an `InMemoryDocumentStore` and `NoProjectSettings`. The service cannot tell.
In the CLI the ports are a `FileDocumentStore` and `BpmnlintProjectSettings`; `applyToXml()`
passes only `NoProjectSettings` and a `{ doc }` with `dryRun`, so nothing is read or written.

### 4. The batch

Expand Down Expand Up @@ -332,7 +333,7 @@ written: order.bpmn

- `bpmn apply` differs only in step 1: it reads the commands as JSON instead of flags.
- `mutateFile()` (`node.ts`) starts at step 2.
- `applyToXml()` (`api.ts`) starts at step 2, with memory in step 7 and no project
- `applyToXml()` (`api.ts`) starts at step 2, with nothing in step 7 and no project
`.bpmnlintrc`, and returns the XML instead of saving it.

From step 2 onwards, all of them run the same code.
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/in/cli/NewFileCommand.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InvalidArgumentError, type Command as CommandType } from 'commander';
import { Doc } from '../../../domain/document/Doc.js';
import type { Target } from '../../../domain/document/vendor/PlatformDetection.js';
import type { RawOpts } from './CliCommand.js';
import { MutationCommand } from './MutationCommand.js';

Expand Down Expand Up @@ -35,7 +36,7 @@ export class NewFileCommand extends MutationCommand {
processName: options['name'] as string | undefined,
processId: options['id'] as string | undefined,
executable: options['executable'] !== false,
target: options['target'] as 'camunda8' | 'camunda7' | undefined,
target: options['target'] as Target | undefined,
mustNotExist: true,
...(stdout ? { out: undefined, dryRun: true } : {}),
});
Expand Down
31 changes: 0 additions & 31 deletions src/adapters/out/memory/InMemoryDocumentStore.ts

This file was deleted.

20 changes: 9 additions & 11 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type { LayoutMode } from './domain/diagram/types.js';
import type { CheckOptions, CheckResult } from './application/port/in/ValidateDocumentUseCase.js';
import type { LayoutDocOptions } from './application/port/in/RelayoutDiagramUseCase.js';
import { EditDocumentService } from './application/service/EditDocumentService.js';
import { InMemoryDocumentStore } from './adapters/out/memory/InMemoryDocumentStore.js';
import { NoProjectSettings } from './adapters/out/memory/NoProjectSettings.js';
import { RelayoutDiagramService } from './application/service/RelayoutDiagramService.js';
import { ValidateDocumentService } from './application/service/ValidateDocumentService.js';
Expand All @@ -41,9 +40,8 @@ import { MutationReportBuilder } from './adapters/in/report/MutationReportBuilde
import { ShownText } from './adapters/in/report/ShownText.js';

/** The in-memory API runs the use cases without files and without a project around the document. */
const IN_MEMORY = new InMemoryDocumentStore();
const NO_PROJECT = new NoProjectSettings();
const READ_DOCUMENT = new ReadDocumentService(IN_MEMORY);
const READ_DOCUMENT = new ReadDocumentService();

export type { ShownView, ViewOptions } from './domain/views/ViewRequest.js';

Expand Down Expand Up @@ -104,12 +102,12 @@ function parseXml(xml: string, file?: string): Promise<Doc> {
export async function applyToXml(xml: string, ops: unknown, opts: EditOptions = {}): Promise<EditResult> {
const list = readOps(ops);
const doc = await loadXml(xml, opts);
return toEditResult(xml, await new EditDocumentService(IN_MEMORY, IN_MEMORY, NO_PROJECT).edit({ ...opts, source: { doc }, commands: list, dryRun: true }));
return toEditResult(xml, await new EditDocumentService(NO_PROJECT).edit({ ...opts, source: { doc }, commands: list, dryRun: true }));
}

/** `bpmn new`: a laid-out document with one empty process. */
export async function newXml(opts: NewDocOptions & EditOptions = {}): Promise<EditResult> {
return toEditResult(undefined, await new CreateDocumentService(IN_MEMORY, NO_PROJECT).create({ ...opts, dryRun: true }));
return toEditResult(undefined, await new CreateDocumentService(NO_PROJECT).create({ ...opts, dryRun: true }));
}

export interface LayoutXmlOptions extends Omit<EditOptions, 'layout'> {
Expand All @@ -123,7 +121,7 @@ export interface LayoutXmlOptions extends Omit<EditOptions, 'layout'> {
export async function layoutXml(xml: string, opts: LayoutXmlOptions = {}): Promise<EditResult> {
RelayoutDiagramService.assertOptions(opts);
const doc = await loadXml(xml, opts);
return toEditResult(xml, await new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true }));
return toEditResult(xml, await new RelayoutDiagramService(NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true }));
}

export interface ValidateXmlOptions {
Expand All @@ -136,7 +134,7 @@ export interface ValidateXmlOptions {

/** `bpmn validate --json`: structure, lint, platform profile, validators and a layout dry run. */
export async function validateXml(xml: string, opts: ValidateXmlOptions = {}): Promise<ValidationReport> {
return new ValidationReportBuilder(await new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validate({ ...opts, source: { doc: await parseXml(xml, opts.file) } })).report();
return new ValidationReportBuilder(await new ValidateDocumentService(NO_PROJECT).validate({ ...opts, source: { doc: await parseXml(xml, opts.file) } })).report();
}

/** `bpmn show` on a parsed document. */
Expand Down Expand Up @@ -187,20 +185,20 @@ export async function extensionsXml(xml: string, id: string): Promise<ExtensionI

/** Sets or, without an argument, clears the process-wide sink of layout diagnostics. */
export function setLayoutDebug(sink?: DebugSink): void {
new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).setDebugSink(sink !== undefined ? { sink } : {});
new RelayoutDiagramService(NO_PROJECT).setDebugSink(sink !== undefined ? { sink } : {});
}

/** Applies ops to an in-memory document in one batch. */
export function mutateDoc(doc: Doc, ops: EditCommand[], opts: MutationOptions = {}): Promise<MutationResult> {
return new EditDocumentService(IN_MEMORY, IN_MEMORY, NO_PROJECT).edit({ ...opts, source: { doc }, commands: ops, dryRun: true });
return new EditDocumentService(NO_PROJECT).edit({ ...opts, source: { doc }, commands: ops, dryRun: true });
}

/** `bpmn layout` on an in-memory document. */
export function layoutDoc(doc: Doc, opts: LayoutDocOptions = {}): Promise<MutationResult> {
return new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true });
return new RelayoutDiagramService(NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true });
}

/** What `bpmn validate` finds in a document. */
export function checkDoc(doc: Doc, opts: CheckOptions = {}): Promise<CheckResult> {
return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validate({ ...opts, source: { doc } });
return new ValidateDocumentService(NO_PROJECT).validate({ ...opts, source: { doc } });
}
11 changes: 8 additions & 3 deletions src/application/service/CreateDocumentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@ import { EditBatchService } from '../../domain/batch/EditBatchService.js';

export class CreateDocumentService implements CreateDocumentUseCase {
constructor(
private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
private readonly save?: SaveDocumentPort,
) {}

private savePort(): SaveDocumentPort {
if (!this.save) throw ioError('E_NO_FILE', 'No output file given');
return this.save;
}

/** An existing file fails before the .bpmnlintrc is read. */
async create(command: CreateDocumentUseCase.Command): Promise<CreateDocumentUseCase.Result> {
const { destination, ...opts } = command;
if (opts.processId !== undefined) Doc.assertProcessId(opts.processId);
const target = opts.out ?? destination;
if (!opts.dryRun && !target) throw ioError('E_NO_FILE', 'No output file given');
if (opts.mustNotExist) await this.save.assertNew(target, opts);
if (opts.mustNotExist) await this.save?.assertNew(target, opts);
const lint = await this.settings.lint(opts.file ?? target, opts);
const conventions = opts.conventions ?? (await this.settings.idConventions(target, opts))?.conventions;
const doc = Doc.create({ processName: opts.processName, processId: opts.processId, executable: opts.executable, target: opts.target, ...(conventions ? { conventions } : {}) }, destination);
const result = await new EditBatchService(doc, [], lint.applyTo(opts, target)).run(target);
if (!opts.dryRun && target) result.written = await this.save.save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
if (!opts.dryRun && target) result.written = await this.savePort().save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
result.changes.create(newProcessChange(doc, opts.processName));
return result;
}
Expand Down
20 changes: 15 additions & 5 deletions src/application/service/EditDocumentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,31 @@ import { TextPreservation, type PreservedText } from '../../domain/document/xml/

export class EditDocumentService implements EditDocumentUseCase {
constructor(
private readonly load: LoadDocumentPort,
private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
private readonly load?: LoadDocumentPort,
private readonly save?: SaveDocumentPort,
) {}

private loadPort(file: string): LoadDocumentPort {
if (!this.load) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
return this.load;
}

private savePort(): SaveDocumentPort {
if (!this.save) throw ioError('E_NO_FILE', 'No output file given');
return this.save;
}

async edit(command: EditDocumentUseCase.Command): Promise<EditDocumentUseCase.Result> {
const { source, commands, ...opts } = command;
const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.load.read(source.file), source.file);
const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file);
doc.assertLossless(opts);
const target = opts.out ?? doc.file;
if (!opts.dryRun && !target) throw ioError('E_NO_FILE', 'No output file given');
if (opts.mustNotExist) await this.save.assertNew(target, opts);
if (opts.mustNotExist) await this.save?.assertNew(target, opts);
const lint = await this.settings.lint(opts.file ?? target, opts);
const result = await new EditBatchService(doc, commands, lint.applyTo(opts, target)).run(target);
if (!opts.dryRun && target) result.written = await this.save.save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
if (!opts.dryRun && target) result.written = await this.savePort().save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
return result;
}

Expand Down
11 changes: 9 additions & 2 deletions src/application/service/ReadDocumentService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Doc } from '../../domain/document/Doc.js';
import type { ReadDocumentUseCase } from '../port/in/ReadDocumentUseCase.js';
import { ioError } from '../../domain/document/utils/errors.js';
import type { LoadDocumentPort } from '../port/out/LoadDocumentPort.js';

/** Decoded in the encoding the XML declaration names, so ISO-8859-1 and Windows-1252 characters survive. */
export class ReadDocumentService implements ReadDocumentUseCase {
constructor(private readonly load: LoadDocumentPort) {}
constructor(private readonly load?: LoadDocumentPort) {}

private loadPort(file: string): LoadDocumentPort {
if (!this.load) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
return this.load;
}


async read(command: ReadDocumentUseCase.Command): Promise<ReadDocumentUseCase.Result> {
const doc = await this.parse(command.source);
Expand All @@ -18,6 +25,6 @@ export class ReadDocumentService implements ReadDocumentUseCase {

private async parse(source: ReadDocumentUseCase.Command['source']): Promise<Doc> {
if ('xml' in source) return Doc.fromXml(source.xml, source.file);
return 'file' in source ? Doc.fromXml(await this.load.read(source.file), source.file) : Doc.fromXml(this.load.decode(source.bytes, source.name));
return 'file' in source ? Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file) : Doc.fromXml(this.loadPort(source.name).decode(source.bytes, source.name));
}
}
Loading
Loading