From e7ea7d52ef563d9f760ef385f238a6d367b8341f Mon Sep 17 00:00:00 2001 From: Marco Schaeck Date: Tue, 27 Oct 2026 16:20:11 +0100 Subject: [PATCH 1/2] refactor!: use c7, c8 and none as the only internal platform value BREAKING CHANGE: Doc.platform() returns 'c7' | 'c8' | 'none' instead of 'camunda7' | 'camunda8' | undefined. TARGETS is now exported from the platform detection module; the --target spelling is unchanged. --- src/adapters/in/cli/NewFileCommand.ts | 3 ++- src/domain/document/Doc.ts | 17 +++++++-------- src/domain/document/EditContext.ts | 9 ++++---- .../document/vendor/Camunda8Defaults.ts | 4 ++-- .../document/vendor/PlatformDetection.ts | 13 ++++++++---- src/domain/editing/DecisionLinkWriter.ts | 10 ++++----- src/domain/editing/NestedHosts.ts | 2 +- src/domain/editing/events/EventGatewayRule.ts | 2 +- .../editing/utils/decision-link-warnings.ts | 2 +- src/domain/validation/BpmnlintValidator.ts | 2 +- src/index.ts | 4 ++-- test/domain/editing/c7-semantic.test.ts | 9 ++++---- .../validation/c7-profile-rules.test.ts | 21 +++++++++---------- 13 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/adapters/in/cli/NewFileCommand.ts b/src/adapters/in/cli/NewFileCommand.ts index 651c7eb9..9a7d57ae 100644 --- a/src/adapters/in/cli/NewFileCommand.ts +++ b/src/adapters/in/cli/NewFileCommand.ts @@ -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'; @@ -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 } : {}), }); diff --git a/src/domain/document/Doc.ts b/src/domain/document/Doc.ts index 1502e966..c9ffdfe2 100644 --- a/src/domain/document/Doc.ts +++ b/src/domain/document/Doc.ts @@ -16,7 +16,7 @@ import { createDefinitions, createModdle, indexById, is, isBpmnElement, many, pa import { ModelError } from './ModelError.js'; import { C7_DEFAULT_TTL, C7_PLATFORM_VERSION } from './vendor/CamundaDescriptor.js'; import { C8_PLATFORM_VERSION } from './vendor/ZeebeDescriptor.js'; -import { PlatformDetection } from './vendor/PlatformDetection.js'; +import { PlatformDetection, platformOfTarget, TARGETS, type Platform, type Target } from './vendor/PlatformDetection.js'; /** `operaton` is Operaton's copy of the camunda namespace (only Operaton reads it). */ export const KNOWN_NAMESPACES: Record = { @@ -37,13 +37,11 @@ export interface NewDocOptions { processName?: string; executable?: boolean; /** declares vendor namespaces, the platform and (camunda7) the Modeler's process defaults */ - target?: 'camunda8' | 'camunda7' | 'none'; + target?: Target; /** process and definitions ids follow them only where they name that type exactly */ conventions?: IdConventions; } -export const TARGETS = ['camunda8', 'camunda7', 'none'] as const; - export interface DocSource { text: string; /** the incoming / outgoing lists as read, before the in-memory repair */ @@ -142,12 +140,13 @@ export class Doc { }); const doc = new Doc({ moddle, definitions, importWarnings: [] }, ids, file); if (opts.conventions) doc.conventions = opts.conventions; - if (opts.target === 'camunda8') { + const platform = platformOfTarget(opts.target); + if (platform === 'c8') { doc.declareNamespace('zeebe'); doc.declareNamespace('modeler'); definitions.$attrs['modeler:executionPlatform'] = 'Camunda Cloud'; definitions.$attrs['modeler:executionPlatformVersion'] = C8_PLATFORM_VERSION; - } else if (opts.target === 'camunda7') { + } else if (platform === 'c7') { doc.declareNamespace('camunda'); doc.declareNamespace('modeler'); definitions.$attrs['modeler:executionPlatform'] = 'Camunda Platform'; @@ -157,8 +156,8 @@ export class Doc { return doc; } - platform(): 'camunda7' | 'camunda8' | undefined { - return new PlatformDetection(this.definitions).target(); + platform(): Platform { + return new PlatformDetection(this.definitions).info().platform; } /** @@ -167,7 +166,7 @@ export class Doc { * Operaton-only files). An existing value is never touched. */ initProcess(process: El, platform = this.platform()): void { - if (platform !== 'camunda7') return; + if (platform !== 'c7') return; const camunda = KNOWN_NAMESPACES['camunda']!; const operaton = KNOWN_NAMESPACES['operaton']!; const uri = !this.prefixFor(camunda) && this.prefixFor(operaton) ? operaton : camunda; diff --git a/src/domain/document/EditContext.ts b/src/domain/document/EditContext.ts index 0438b479..333ff966 100644 --- a/src/domain/document/EditContext.ts +++ b/src/domain/document/EditContext.ts @@ -4,6 +4,7 @@ import { ChangeSet, type Rename } from './ChangeSet.js'; import type { Doc } from './Doc.js'; import { IdAllocator } from './IdAllocator.js'; import type { El } from './utils/moddle.js'; +import type { Platform } from './vendor/PlatformDetection.js'; /** What a running `apply` batch did so far: E_NOT_FOUND points to it. */ export interface BatchContext { @@ -50,7 +51,7 @@ export interface DroppedContent { export interface EditSettings { conventions?: IdConventions; /** so the ops follow the same engine as the profile that reports on them */ - platformChoice?: 'c7' | 'c8' | 'none'; + platformChoice?: Platform; keepFlowIds?: boolean; } @@ -82,10 +83,8 @@ export class EditContext { } /** The call's platform choice, else the document's detected one. */ - platform(): 'camunda7' | 'camunda8' | undefined { - const choice = this.settings.platformChoice; - if (choice) return choice === 'c7' ? 'camunda7' : choice === 'c8' ? 'camunda8' : undefined; - return this.doc.platform(); + platform(): Platform { + return this.settings.platformChoice ?? this.doc.platform(); } /** Like Doc.require; inside a batch E_NOT_FOUND points to what the batch did. */ diff --git a/src/domain/document/vendor/Camunda8Defaults.ts b/src/domain/document/vendor/Camunda8Defaults.ts index 98af312d..2d44305c 100644 --- a/src/domain/document/vendor/Camunda8Defaults.ts +++ b/src/domain/document/vendor/Camunda8Defaults.ts @@ -21,7 +21,7 @@ export class Camunda8Defaults { * which the v2 user task API does not list. */ userTask(el: El): boolean { - if (this.edit.platform() !== 'camunda8' || !is(el, 'bpmn:UserTask')) return false; + if (this.edit.platform() !== 'c8' || !is(el, 'bpmn:UserTask')) return false; const container = el.get('extensionElements'); const values = container ? many(container, 'values') : []; if (values.some((extension) => extension.$type.endsWith(':userTask') && this.doc.namespaceUri(extension.$type.slice(0, extension.$type.indexOf(':'))) === ZEEBE_URI)) return false; @@ -42,7 +42,7 @@ export class Camunda8Defaults { * definitions without an id; the Modeler gives every definition one. */ definitionId(event: El, def: El): void { - if (this.edit.platform() !== 'camunda8' || def.get('id')) return; + if (this.edit.platform() !== 'c8' || def.get('id')) return; const eventId = event.get('id') ?? ''; const label = (eventId && speakingStem(eventId)) || new ElementLabels(event).label(); def.set('id', this.edit.allocator.allocate(typeRequest(def.$type, { context: label })).id); diff --git a/src/domain/document/vendor/PlatformDetection.ts b/src/domain/document/vendor/PlatformDetection.ts index 6e830995..c890049a 100644 --- a/src/domain/document/vendor/PlatformDetection.ts +++ b/src/domain/document/vendor/PlatformDetection.ts @@ -6,6 +6,15 @@ import { ZEEBE_URI } from './ZeebeDescriptor.js'; export type Platform = 'c7' | 'c8' | 'none'; export type PlatformChoice = Platform | 'auto'; + +/** How `bpmn new --target` and `NewDocOptions.target` spell a platform; camunda7 also covers CIB seven and Operaton. */ +export const TARGETS = ['camunda8', 'camunda7', 'none'] as const; +export type Target = (typeof TARGETS)[number]; +const PLATFORM_OF_TARGET: Readonly> = { camunda8: 'c8', camunda7: 'c7', none: 'none' }; + +export function platformOfTarget(target: Target | undefined): Platform { + return target ? PLATFORM_OF_TARGET[target] : 'none'; +} export const PLATFORM_CHOICES: readonly PlatformChoice[] = ['auto', 'c7', 'c8', 'none']; export interface PlatformInfo { @@ -73,10 +82,6 @@ export class PlatformDetection { return detected.operaton ? { ...info, operaton: true, detail: `--platform option; the file uses the operaton namespace: ${OPERATON_NOTE}` } : info; } - target(): 'camunda7' | 'camunda8' | undefined { - const platform = this.info().platform; - return platform === 'c7' ? 'camunda7' : platform === 'c8' ? 'camunda8' : undefined; - } private fromExecutionPlatform(value: string): Platform | undefined { const normalized = value.trim().toLowerCase(); diff --git a/src/domain/editing/DecisionLinkWriter.ts b/src/domain/editing/DecisionLinkWriter.ts index d000cf61..7050c5d0 100644 --- a/src/domain/editing/DecisionLinkWriter.ts +++ b/src/domain/editing/DecisionLinkWriter.ts @@ -10,7 +10,7 @@ import { DecisionLinkReader } from '../document/DecisionLinkReader.js'; import { CAMUNDA_URI, OPERATON_URI } from '../document/vendor/CamundaDescriptor.js'; import { ZEEBE_URI } from '../document/vendor/ZeebeDescriptor.js'; -type EngineSpelling = { kind: 'c7'; uri: string; prefix: string } | { kind: 'zeebe'; prefix: string }; +type EngineSpelling = { kind: 'c7'; uri: string; prefix: string } | { kind: 'c8'; prefix: string }; /** * Writes a business rule task's decision link in one spelling and removes the others, so a @@ -100,11 +100,11 @@ export class DecisionLinkWriter { dropAttr('calledDecision'); dropC7(); dropZeebe(); - } else if (platform === 'camunda7') { + } else if (platform === 'c7') { const operatonOnly = !doc.prefixFor(CAMUNDA_URI) && !!doc.prefixFor(OPERATON_URI); const prefix = operatonOnly ? this.prefixFor(OPERATON_URI, 'operaton') : this.prefixFor(CAMUNDA_URI, 'camunda'); writeC7(`${prefix}:decisionRef`); - } else if (platform === 'camunda8') { + } else if (platform === 'c8') { const call = writeZeebe(this.reader.zeebeCalls(el)[0], 'zeebe'); if (!textValue((call as unknown as Record)['resultVariable'])) { cs.warn( @@ -130,7 +130,7 @@ export class DecisionLinkWriter { if (ownC7) writeC7(ownC7); else if (ownCall) writeZeebe(ownCall, 'zeebe'); else if (follow?.kind === 'c7') writeC7(`${this.prefixFor(follow.uri, follow.prefix)}:decisionRef`); - else if (follow?.kind === 'zeebe') writeZeebe(this.reader.zeebeCalls(el)[0], follow.prefix); + else if (follow?.kind === 'c8') writeZeebe(this.reader.zeebeCalls(el)[0], follow.prefix); else { dropC7(); dropZeebe(); @@ -163,7 +163,7 @@ export class DecisionLinkWriter { if (!textValue((call as unknown as Record)['decisionId'])) continue; const prefix = call.$type.split(':')[0]!; const declared = ((call.$descriptor as { ns?: { uri?: string } }).ns?.uri ?? this.reader.uriOf(task, prefix)) === ZEEBE_URI; - seen.set(declared ? 'zeebe' : call.$type, declared ? { kind: 'zeebe', prefix } : undefined); + seen.set(declared ? 'zeebe' : call.$type, declared ? { kind: 'c8', prefix } : undefined); } for (const key of ['calledDecision', 'calledElement']) if (textValue(task.$attrs?.[key])) seen.set(key, undefined); if (seen.size > 1) return undefined; diff --git a/src/domain/editing/NestedHosts.ts b/src/domain/editing/NestedHosts.ts index 9df0e79b..8c1a6200 100644 --- a/src/domain/editing/NestedHosts.ts +++ b/src/domain/editing/NestedHosts.ts @@ -92,7 +92,7 @@ export class NestedHosts { const first: Record = { definition: `give it a trigger first: \`bpmn set ${id} trigger=\``, loop: - edit.platform() === 'camunda8' + edit.platform() === 'c8' ? `make it a multi-instance first: \`bpmn ext add ${id} loop.zeebe:loopCharacteristics inputCollection== inputElement=\` (creates a parallel loop; \`bpmn set ${id} loop=sequential\` for one after the other)` : `make it a loop first: \`bpmn set ${id} loop=parallel\` (or sequential, or 'loop.camunda:collection=\${items}')`, condition: `give it a condition first: \`bpmn set ${id} 'condition=\${...}'\` (conditional events: when=)`, diff --git a/src/domain/editing/events/EventGatewayRule.ts b/src/domain/editing/events/EventGatewayRule.ts index 35786a5d..86f0bac1 100644 --- a/src/domain/editing/events/EventGatewayRule.ts +++ b/src/domain/editing/events/EventGatewayRule.ts @@ -18,7 +18,7 @@ export class EventGatewayRule { ) {} get rule(): 'engines' | 'bpmn' { - return this.edit.platform() === 'camunda7' ? 'engines' : 'bpmn'; + return this.edit.platform() === 'c7' ? 'engines' : 'bpmn'; } /** `leaving` are the flows that disappear with the change (e.g. the flow a bridge replaces). */ diff --git a/src/domain/editing/utils/decision-link-warnings.ts b/src/domain/editing/utils/decision-link-warnings.ts index 7c8e0bf7..b1b30d68 100644 --- a/src/domain/editing/utils/decision-link-warnings.ts +++ b/src/domain/editing/utils/decision-link-warnings.ts @@ -11,6 +11,6 @@ export function isDecisionLinkWarning(warning: ImportWarning): boolean { * content. In a Camunda 7 / 8 file it stays a warning because the engines refuse it. */ export function reportedImportWarnings(doc: Doc): ImportWarning[] { - if (!doc.importWarnings.some(isDecisionLinkWarning) || doc.platform() !== undefined) return doc.importWarnings; + if (!doc.importWarnings.some(isDecisionLinkWarning) || doc.platform() !== 'none') return doc.importWarnings; return doc.importWarnings.filter((warning) => !isDecisionLinkWarning(warning)); } diff --git a/src/domain/validation/BpmnlintValidator.ts b/src/domain/validation/BpmnlintValidator.ts index 387a1411..be089b73 100644 --- a/src/domain/validation/BpmnlintValidator.ts +++ b/src/domain/validation/BpmnlintValidator.ts @@ -88,7 +88,7 @@ export class BpmnlintValidator { const msg = warning.message.split('\n')[0]!; if (own.has(msg)) continue; const id = (warning.element as { id?: string } | undefined)?.id; - const hint = isDecisionLinkWarning(warning) && doc.platform() === undefined ? DECISION_LINK_HINT : IMPORT_HINT; + const hint = isDecisionLinkWarning(warning) && doc.platform() === 'none' ? DECISION_LINK_HINT : IMPORT_HINT; out.push({ severity: 'error', code: 'import-warning', message: `Import warning: ${msg}`, ...(id ? { element: id } : {}), key: `import|${id ?? ''}|${msg}`, hint }); } } diff --git a/src/index.ts b/src/index.ts index 202254db..de4c869f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,7 +92,7 @@ export function aroundView(doc: Doc, id: string, opts: AroundOptions = {}): Arou export function elementContext(doc: Doc, id: string): ElementContext { return new ShowModelService().elementContext({ doc, id }); } export function implementationOf(el: El): Implementation | undefined { return new ShowModelService().implementation({ element: el }); } /* the building blocks */ -export { Doc, TARGETS, type DocSource, type NewDocOptions } from './domain/document/Doc.js'; +export { Doc, type DocSource, type NewDocOptions } from './domain/document/Doc.js'; export { CliError, EXIT_CODES, type ErrorCategory, type Warning } from './domain/document/CliError.js'; export { KINDS, parseKind, kindOf, kindLabel } from './domain/document/utils/kinds.js'; export { FORMAT_OP_NAMES, isDiagramCommand } from './domain/commands/utils/op-names.js'; @@ -170,7 +170,7 @@ export type { ValidateOptions, ValidationResult } from './domain/validation/Stru /** Structural validation and lint of a document, with the platform profile on request. */ export function validateDoc(doc: Doc, opts: ValidateOptions = {}): ValidationResult { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validateStructure({ ...opts, doc }); } /* the platform profile: Camunda 7 / 8 rules, loaded on demand */ -export { PLATFORM_CHOICES, type Platform, type PlatformChoice, type PlatformInfo } from './domain/document/vendor/PlatformDetection.js'; +export { PLATFORM_CHOICES, TARGETS, type Platform, type PlatformChoice, type PlatformInfo } from './domain/document/vendor/PlatformDetection.js'; export type { PlatformSummary, ProfileReport } from './domain/validation/profiles/PlatformProfiles.js'; export type { ProfileFinding, Severity } from './domain/document/utils/finding.js'; export type { ExtensionInfo } from './domain/document/ExtensionLister.js'; diff --git a/test/domain/editing/c7-semantic.test.ts b/test/domain/editing/c7-semantic.test.ts index 3bf65030..88139da3 100644 --- a/test/domain/editing/c7-semantic.test.ts +++ b/test/domain/editing/c7-semantic.test.ts @@ -19,7 +19,6 @@ import { renderDetail, renderView, runOps, mutateDoc, buildView, elementDetail, import type { EditCommand } from '../../../src/domain/commands/types.js'; import { definitionsXml, flowBetween } from '../../helpers/helpers.js'; import { CAMUNDA as C7_DESCRIPTOR } from '../../../src/domain/document/vendor/CamundaDescriptor.js'; -import { PlatformDetection } from '../../../src/domain/document/vendor/PlatformDetection.js'; import { VendorDescriptor } from '../../../src/domain/document/vendor/VendorDescriptor.js'; import { ElementProperties } from '../../../src/domain/editing/ElementProperties.js'; import { caught } from '../../helpers/errors.js'; @@ -62,14 +61,14 @@ describe('C7 target: history time to live and platform version', () => { expect(xml).toMatch(/]*camunda:historyTimeToLive="180"/); expect(xml).toContain('modeler:executionPlatform="Camunda Platform"'); expect(xml).toContain('modeler:executionPlatformVersion="7.24.0"'); - expect(new PlatformDetection(doc.definitions).target()).toBe('camunda7'); + expect(doc.platform()).toBe('c7'); }); it('other targets get no TTL', async () => { expect(await Doc.create({ target: 'camunda8' }).toXml()).not.toContain('historyTimeToLive'); expect(await Doc.create({}).toXml()).not.toContain('historyTimeToLive'); - expect(new PlatformDetection(Doc.create({ target: 'camunda8' }).definitions).target()).toBe('camunda8'); - expect(new PlatformDetection(Doc.create({}).definitions).target()).toBeUndefined(); + expect(Doc.create({ target: 'camunda8' }).platform()).toBe('c8'); + expect(Doc.create({}).platform()).toBe('none'); }); it('a process created later in a C7 file (participant) gets the TTL too, in other files not', async () => { @@ -93,7 +92,7 @@ describe('C7 target: history time to live and platform version', () => { it('a loaded C7 file (modeler:executionPlatform) is recognised; an explicit TTL is never overwritten', async () => { const xml = definitionsXml('', { nsDecl: `${CAMUNDA} xmlns:modeler="http://camunda.org/schema/modeler/1.0" modeler:executionPlatform="Camunda Platform"` }); const doc = await Doc.fromXml(xml); - expect(new PlatformDetection(doc.definitions).target()).toBe('camunda7'); + expect(doc.platform()).toBe('c7'); const cs = runOps(doc, [{ op: 'add', kind: 'participant', name: 'Other' }, { op: 'add', kind: 'participant', name: 'Third' }]); const processElement = doc.require(cs.created.find((change) => change.kind === 'process')!.id); expect(processElement.$attrs['camunda:historyTimeToLive']).toBe('180'); diff --git a/test/domain/validation/c7-profile-rules.test.ts b/test/domain/validation/c7-profile-rules.test.ts index 4990e4dc..22e186f4 100644 --- a/test/domain/validation/c7-profile-rules.test.ts +++ b/test/domain/validation/c7-profile-rules.test.ts @@ -22,7 +22,7 @@ import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { Doc } from '../../../src/domain/document/Doc.js'; import { detectPlatform, loadProfiles, runProfile } from '../../../src/index.js'; -import { PlatformDetection } from '../../../src/domain/document/vendor/PlatformDetection.js'; +import type { Platform } from '../../../src/domain/document/vendor/PlatformDetection.js'; import { validateDoc } from '../../../src/index.js'; import { cli } from '../../helpers/cli.js'; @@ -252,19 +252,18 @@ describe('profile follow-ups: one synthetic model per rule', () => { }); describe('one platform detector', () => { - const cases: Array<[string, string, string, 'camunda7' | 'camunda8' | undefined]> = [ - ['Camunda Platform', C7, chain([UT()]), 'camunda7'], - ['Operaton namespace only', OPERATON, chain([UT('T')]).replace('', ''), 'camunda7'], - ['camunda content with both namespaces declared', `${CAMUNDA} ${ZEEBE}`, chain([UT('T')]).replace('', ''), 'camunda7'], - ['executionPlatform Operaton', `${MODELER} modeler:executionPlatform="Operaton"`, chain([UT()]), 'camunda7'], - ['zeebe content', `${ZEEBE} ${CAMUNDA}`, chain([UT('T', ext(''))]), 'camunda8'], - ['nothing', '', chain([UT()]), undefined], + const cases: Array<[string, string, string, Platform]> = [ + ['Camunda Platform', C7, chain([UT()]), 'c7'], + ['Operaton namespace only', OPERATON, chain([UT('T')]).replace('', ''), 'c7'], + ['camunda content with both namespaces declared', `${CAMUNDA} ${ZEEBE}`, chain([UT('T')]).replace('', ''), 'c7'], + ['executionPlatform Operaton', `${MODELER} modeler:executionPlatform="Operaton"`, chain([UT()]), 'c7'], + ['zeebe content', `${ZEEBE} ${CAMUNDA}`, chain([UT('T', ext(''))]), 'c8'], + ['nothing', '', chain([UT()]), 'none'], ]; it.each(cases)('Doc.platform() follows detectPlatform: %s', async (_name, ns, body, expected) => { const doc = await Doc.fromXml(xml(body, { ns, attrs: '' })); - const platform = detectPlatform(doc).platform; - expect(new PlatformDetection(doc.definitions).target()).toBe(expected); - expect(doc.platform()).toBe(platform === 'c7' ? 'camunda7' : platform === 'c8' ? 'camunda8' : undefined); + expect(detectPlatform(doc).platform).toBe(expected); + expect(doc.platform()).toBe(expected); }); it('says when the file is read as Operaton reads it, and names both declared namespaces', async () => { From 0307ef2ba25045b6c9f4056ea32eb0d902a2d3de Mon Sep 17 00:00:00 2001 From: Marco Schaeck Date: Tue, 27 Oct 2026 16:20:54 +0100 Subject: [PATCH 2/2] refactor: drop the unused in-memory document store The in-memory API only passes loaded documents and never writes, so the services take their load and save ports as optional instead of a store that was never read or written. --- GLOSSARY.md | 2 +- docs/architecture.md | 21 +++++++------ .../out/memory/InMemoryDocumentStore.ts | 31 ------------------- src/api.ts | 20 ++++++------ .../service/CreateDocumentService.ts | 11 +++++-- .../service/EditDocumentService.ts | 20 +++++++++--- .../service/ReadDocumentService.ts | 11 +++++-- .../service/RelayoutDiagramService.ts | 20 +++++++++--- .../service/ValidateDocumentService.ts | 10 ++++-- src/cli.ts | 8 ++--- src/index.ts | 22 ++++++------- src/node.ts | 12 +++---- test/helpers/helpers.ts | 8 ++--- 13 files changed, 100 insertions(+), 96 deletions(-) delete mode 100644 src/adapters/out/memory/InMemoryDocumentStore.ts diff --git a/GLOSSARY.md b/GLOSSARY.md index 5cf4b573..09fbd483 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -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`). | diff --git a/docs/architecture.md b/docs/architecture.md index 7c79498d..eca263d6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,7 +23,7 @@ flowchart TB service --> domain["Domain
domain/"] service --> portOut{{"Port
application/port/out"}} portOut --> files["Files
adapters/out/files"] - portOut --> memory["Memory
adapters/out/memory"] + portOut --> memory["No project
adapters/out/memory"] classDef adapter stroke:#2f80ed,stroke-width:2px classDef app stroke:#f2994a,stroke-width:2px @@ -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 | @@ -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 @@ -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 @@ -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. diff --git a/src/adapters/out/memory/InMemoryDocumentStore.ts b/src/adapters/out/memory/InMemoryDocumentStore.ts deleted file mode 100644 index 63779414..00000000 --- a/src/adapters/out/memory/InMemoryDocumentStore.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { LoadDocumentPort } from '../../../application/port/out/LoadDocumentPort.js'; -import type { SaveDocumentPort } from '../../../application/port/out/SaveDocumentPort.js'; -import { ioError } from '../../../domain/document/utils/errors.js'; - -/** Documents as texts by name, for hosts without files (the browser, the in-memory API). */ -export class InMemoryDocumentStore implements LoadDocumentPort, SaveDocumentPort { - private readonly texts = new Map(); - - async read(file: string): Promise { - const text = this.texts.get(file); - if (text === undefined) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file }); - return text; - } - - decode(bytes: Uint8Array): string { - return new TextDecoder('utf-8').decode(bytes); - } - - async save(target: string, xml: string, opts: { source?: string; unchanged: boolean; backup?: boolean }): Promise { - if (opts.unchanged && opts.source === target) return false; - const original = opts.backup && opts.source ? this.texts.get(opts.source) : undefined; - if (original !== undefined) this.texts.set(`${opts.source}.bak`, original); - this.texts.set(target, xml); - return true; - } - - async assertNew(target: string | undefined, opts: { force?: boolean; dryRun?: boolean }): Promise { - if (!target || opts.force || opts.dryRun || !this.texts.has(target)) return; - throw ioError('E_FILE_EXISTS', `${target} already exists`, { file: target, hint: 'Choose another name or pass force to overwrite.' }); - } -} diff --git a/src/api.ts b/src/api.ts index 2257d367..96fd504a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -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'; @@ -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'; @@ -104,12 +102,12 @@ function parseXml(xml: string, file?: string): Promise { export async function applyToXml(xml: string, ops: unknown, opts: EditOptions = {}): Promise { 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 { - 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 { @@ -123,7 +121,7 @@ export interface LayoutXmlOptions extends Omit { export async function layoutXml(xml: string, opts: LayoutXmlOptions = {}): Promise { 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 { @@ -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 { - 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. */ @@ -187,20 +185,20 @@ export async function extensionsXml(xml: string, id: string): Promise { - 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 { - 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 { - return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validate({ ...opts, source: { doc } }); + return new ValidateDocumentService(NO_PROJECT).validate({ ...opts, source: { doc } }); } diff --git a/src/application/service/CreateDocumentService.ts b/src/application/service/CreateDocumentService.ts index 992bdf99..03f119a4 100644 --- a/src/application/service/CreateDocumentService.ts +++ b/src/application/service/CreateDocumentService.ts @@ -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 { 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; } diff --git a/src/application/service/EditDocumentService.ts b/src/application/service/EditDocumentService.ts index a454001b..64a4ac9e 100644 --- a/src/application/service/EditDocumentService.ts +++ b/src/application/service/EditDocumentService.ts @@ -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 { 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; } diff --git a/src/application/service/ReadDocumentService.ts b/src/application/service/ReadDocumentService.ts index 6ab1bfbe..840bb437 100644 --- a/src/application/service/ReadDocumentService.ts +++ b/src/application/service/ReadDocumentService.ts @@ -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 { const doc = await this.parse(command.source); @@ -18,6 +25,6 @@ export class ReadDocumentService implements ReadDocumentUseCase { private async parse(source: ReadDocumentUseCase.Command['source']): Promise { 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)); } } diff --git a/src/application/service/RelayoutDiagramService.ts b/src/application/service/RelayoutDiagramService.ts index ba556be7..0366fe9b 100644 --- a/src/application/service/RelayoutDiagramService.ts +++ b/src/application/service/RelayoutDiagramService.ts @@ -15,19 +15,29 @@ import type { SaveDocumentPort } from '../port/out/SaveDocumentPort.js'; /** Ids that are no sub-process are errors, not silent no-ops. */ export class RelayoutDiagramService implements RelayoutDiagramUseCase { 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 relayout(command: RelayoutDiagramUseCase.Command): Promise { const { source, ...opts } = command; RelayoutDiagramService.assertOptions(opts); - 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 project = (await this.settings.lint(opts.file ?? target, opts)).applyTo(opts, target); if (project.tidy) return this.write(doc, new EditBatchService(doc, [{ op: 'tidy' }], { ...project, layout: 'auto' }), target, opts); return this.redraw(doc, project, target, opts); @@ -66,7 +76,7 @@ export class RelayoutDiagramService implements RelayoutDiagramUseCase { private async write(doc: Doc, batch: EditBatchService, target: string | undefined, opts: FileWriteOptions): Promise { const result = await batch.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; } } diff --git a/src/application/service/ValidateDocumentService.ts b/src/application/service/ValidateDocumentService.ts index f49006de..2e3d2c24 100644 --- a/src/application/service/ValidateDocumentService.ts +++ b/src/application/service/ValidateDocumentService.ts @@ -16,19 +16,25 @@ import { once } from '../../domain/validation/utils/once.js'; import type { ValidatorReport, ValidatorRun } from '../../domain/validation/validators.js'; import { toCliError } from '../../domain/document/utils/cli-error.js'; import type { CheckOptions, CheckResult, ValidateDocumentUseCase } from '../port/in/ValidateDocumentUseCase.js'; +import { ioError } from '../../domain/document/utils/errors.js'; import type { LoadDocumentPort } from '../port/out/LoadDocumentPort.js'; import type { LoadProjectSettingsPort } from '../port/out/LoadProjectSettingsPort.js'; /** The layout dry run redraws the in-memory document: pass a document of its own. */ export class ValidateDocumentService implements ValidateDocumentUseCase { constructor( - private readonly load: LoadDocumentPort, private readonly settings: LoadProjectSettingsPort, + 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 validate(command: ValidateDocumentUseCase.Command): Promise { const { source, ...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); return this.check(doc, (await this.settings.lint(opts.file ?? doc.file, opts)).applyTo(opts, doc.file)); } diff --git a/src/cli.ts b/src/cli.ts index dfbd9b8d..c6e7a232 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -38,10 +38,10 @@ function exitWhenWritten(): void { const files = new FileDocumentStore(); const project = new BpmnlintProjectSettings(); const cli = new Cli(VERSION, { - editDocument: new EditDocumentService(files, files, project), - createDocument: new CreateDocumentService(files, project), - relayoutDiagram: new RelayoutDiagramService(files, files, project), - validateDocument: new ValidateDocumentService(files, project), + editDocument: new EditDocumentService(project, files, files), + createDocument: new CreateDocumentService(project, files), + relayoutDiagram: new RelayoutDiagramService(project, files, files), + validateDocument: new ValidateDocumentService(project, files), readDocument: new ReadDocumentService(files), showModel: new ShowModelService(), showConventions: new ShowConventionsService(project), diff --git a/src/index.ts b/src/index.ts index de4c869f..740d03a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,6 @@ import { RenderSvgService } from './application/service/RenderSvgService.js'; import { EditDocumentService } from './application/service/EditDocumentService.js'; import { RelayoutDiagramService } from './application/service/RelayoutDiagramService.js'; import { ReadDocumentService } from './application/service/ReadDocumentService.js'; -import { InMemoryDocumentStore } from './adapters/out/memory/InMemoryDocumentStore.js'; import { NoProjectSettings } from './adapters/out/memory/NoProjectSettings.js'; import { AroundText } from './adapters/in/report/AroundText.js'; import { ContextText } from './adapters/in/report/ContextText.js'; @@ -58,7 +57,6 @@ import type { DecisionLink } from './domain/document/DecisionLinkReader.js'; import type { ExtensionInfo } from './domain/document/ExtensionLister.js'; import type { LayoutOptions, LayoutResult } from './domain/diagram/layout/FullLayout.js'; import type { FormatEntry, FormatResult } from './domain/diagram/DiagramCommandService.js'; -const IN_MEMORY = new InMemoryDocumentStore(); const NO_PROJECT = new NoProjectSettings(); /* the in-memory API: strings in, strings / data out */ export { mutateDoc, layoutDoc, checkDoc, applyToXml, newXml, layoutXml, validateXml, viewXml, showXml, conventionsXml, metricsXml, findXml, extensionsXml, viewDoc, renderShown, type EditOptions, type EditResult, type EditReport, type LayoutXmlOptions, type ShownView, type ValidateXmlOptions, type ViewOptions } from './api.js'; @@ -123,19 +121,19 @@ export { type LayoutDocOptions } from './application/port/in/RelayoutDiagramUseC export type { ValidationDelta } from './domain/validation/FindingBaseline.js'; /* text-preserving output, for hosts that serialise models themselves */ export { type PreservedText } from './domain/document/xml/TextPreservation.js'; -export function preserveText(original: string, baseline: string, next: string): Promise { return new EditDocumentService(IN_MEMORY, IN_MEMORY, NO_PROJECT).preserveText({ original, baseline, next }); } +export function preserveText(original: string, baseline: string, next: string): Promise { return new EditDocumentService(NO_PROJECT).preserveText({ original, baseline, next }); } /* a host's validators, run inside the write transaction and by validate */ export type { NamedValidator, Validator, ValidatorContext, ValidatorFinding, ValidatorFn, ValidatorIssue, ValidatorReport, ValidatorSeverity } from './domain/validation/validators.js'; /* the project's bpmnlint configuration as an advisory validator; bpmnlint itself is injected */ export { BPMNLINT_COVERS, BPMNLINT_VALIDATOR, type BpmnlintValidatorOptions } from './domain/validation/BpmnlintValidator.js'; export type { BpmnlintConfig, BpmnlintEngine, BpmnlintLinter, BpmnlintLinterClass, BpmnlintReport, BpmnlintResolver, BpmnlintRule, BpmnlintRuleSetting, BpmnlintSeverity, BpmnlintUnavailable, ResolvedBpmnlintConfig } from './domain/validation/bpmnlint-validator.js'; /** The advisory validator of a resolved bpmnlint configuration. */ -export function bpmnlintValidator(engine: BpmnlintEngine, resolved: ResolvedBpmnlintConfig, opts: BpmnlintValidatorOptions = {}): NamedValidator { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).bpmnlintValidator({ ...opts, engine, resolved }); } +export function bpmnlintValidator(engine: BpmnlintEngine, resolved: ResolvedBpmnlintConfig, opts: BpmnlintValidatorOptions = {}): NamedValidator { return new ValidateDocumentService(NO_PROJECT).bpmnlintValidator({ ...opts, engine, resolved }); } /** * The effective bpmnlint configuration; `opts.moddleExtensions` holds the descriptors the host * resolved for the module paths of `config.moddleExtensions`. */ -export async function resolveBpmnlintConfig(engine: BpmnlintEngine, config: BpmnlintConfig, opts: { moddleExtensions?: Record } = {}): Promise { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).resolveBpmnlintConfig({ ...opts, engine, config }); } +export async function resolveBpmnlintConfig(engine: BpmnlintEngine, config: BpmnlintConfig, opts: { moddleExtensions?: Record } = {}): Promise { return new ValidateDocumentService(NO_PROJECT).resolveBpmnlintConfig({ ...opts, engine, config }); } /* declared id conventions: what element-id-naming in the .bpmnlintrc says about new ids */ export { renderConventions } from './domain/document/utils/conventions-text.js'; export { ID_NAMING_RULE, type IdConventions, type IdNamingHelpers, type QualifierMode } from './domain/document/ids/IdConventionsReader.js'; @@ -168,7 +166,7 @@ export function decisionLinkOf(el: El, doc?: Doc): DecisionLink | undefined { export { ChangeSet, type Change } from './domain/document/ChangeSet.js'; export type { ValidateOptions, ValidationResult } from './domain/validation/StructuralValidator.js'; /** Structural validation and lint of a document, with the platform profile on request. */ -export function validateDoc(doc: Doc, opts: ValidateOptions = {}): ValidationResult { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validateStructure({ ...opts, doc }); } +export function validateDoc(doc: Doc, opts: ValidateOptions = {}): ValidationResult { return new ValidateDocumentService(NO_PROJECT).validateStructure({ ...opts, doc }); } /* the platform profile: Camunda 7 / 8 rules, loaded on demand */ export { PLATFORM_CHOICES, TARGETS, type Platform, type PlatformChoice, type PlatformInfo } from './domain/document/vendor/PlatformDetection.js'; export type { PlatformSummary, ProfileReport } from './domain/validation/profiles/PlatformProfiles.js'; @@ -222,7 +220,7 @@ export function parseOps(input: unknown): EditCommand[] { /** Throws E_IMPORT_LOSSY when bpmn-moddle dropped content on import, unless `force`. */ export function assertLossless(doc: Doc, opts: { force?: boolean } = {}): void { - new ReadDocumentService(IN_MEMORY).assertLossless({ ...opts, doc }); + new ReadDocumentService().assertLossless({ ...opts, doc }); } /** @@ -230,17 +228,17 @@ export function assertLossless(doc: Doc, opts: { force?: boolean } = {}): void { * validateDoc; the async API loads them itself. A failed load (E_PROFILE_LOAD) is retried later. */ export function loadProfile(platform: Platform): Promise { - return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).loadProfile({ platform }); + return new ValidateDocumentService(NO_PROJECT).loadProfile({ platform }); } /** Loads the engine rules of several platforms (default: all). */ export function loadProfiles(platforms?: readonly Exclude[]): Promise { - return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).loadProfiles(platforms !== undefined ? { platforms } : {}); + return new ValidateDocumentService(NO_PROJECT).loadProfiles(platforms !== undefined ? { platforms } : {}); } /** Runs the profile of the document's platform (or `choice`); its rules must be loaded. */ export function runProfile(doc: Doc, choice?: PlatformChoice): ProfileReport { - return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).runProfile({ doc, ...(choice !== undefined ? { choice } : {}) }); + return new ValidateDocumentService(NO_PROJECT).runProfile({ doc, ...(choice !== undefined ? { choice } : {}) }); } /** @@ -248,7 +246,7 @@ export function runProfile(doc: Doc, choice?: PlatformChoice): ProfileReport { * uses, else a declared one. */ export function detectPlatform(doc: Doc): PlatformInfo { - return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).detectPlatform({ doc }); + return new ValidateDocumentService(NO_PROJECT).detectPlatform({ doc }); } /** `bpmn guide`: the agent cheat sheet. */ @@ -319,5 +317,5 @@ export function runFormatOps(doc: Doc, entries: readonly FormatEntry[], asFile = * treat the returned XML as the truth and re-parse it if you need DI. */ export function layoutModel(model: Model, opts: LayoutOptions = {}): Promise { - return new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).layoutModel({ ...opts, model }); + return new RelayoutDiagramService(NO_PROJECT).layoutModel({ ...opts, model }); } diff --git a/src/node.ts b/src/node.ts index 455e16ae..2d4619ef 100644 --- a/src/node.ts +++ b/src/node.ts @@ -37,20 +37,20 @@ export function readDoc(file: string): Promise { return new ReadDocumentSer /** Loads a file; fails with E_IMPORT_LOSSY when bpmn-moddle dropped content, unless force. */ export function loadDoc(file: string, opts: { force?: boolean } = {}): Promise { return new ReadDocumentService(FILES).read({ source: { file }, lossless: true, ...opts }); } /** Loads a file, applies ops and writes it back (or to opts.out). */ -export function mutateFile(file: string, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(FILES, FILES, PROJECT).edit({ ...opts, source: { file }, commands: ops }); } +export function mutateFile(file: string, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(PROJECT, FILES, FILES).edit({ ...opts, source: { file }, commands: ops }); } /** Applies ops to a loaded document and writes it to opts.out ?? doc.file. */ -export function mutateDocToFile(doc: Doc, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(FILES, FILES, PROJECT).edit({ ...opts, source: { doc }, commands: ops }); } +export function mutateDocToFile(doc: Doc, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(PROJECT, FILES, FILES).edit({ ...opts, source: { doc }, commands: ops }); } /** `bpmn layout`: redraws a file (or with `tidy` only removes overlaps) and writes it back. */ -export function layoutFile(file: string, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(FILES, FILES, PROJECT).relayout({ ...opts, source: { file } }); } +export function layoutFile(file: string, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(PROJECT, FILES, FILES).relayout({ ...opts, source: { file } }); } /** `bpmn layout` on a loaded document, written to opts.out ?? doc.file unless dryRun. */ -export function layoutDocToFile(doc: Doc, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(FILES, FILES, PROJECT).relayout({ ...opts, source: { doc } }); } +export function layoutDocToFile(doc: Doc, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(PROJECT, FILES, FILES).relayout({ ...opts, source: { doc } }); } /** `bpmn validate`: validates a file and dry-runs the layout without writing anything. */ -export function checkFile(file: string, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(FILES, PROJECT).validate({ ...opts, source: { file } }); } +export function checkFile(file: string, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(PROJECT, FILES).validate({ ...opts, source: { file } }); } /** * checkFile for a document read elsewhere (`bpmn validate -`); without a file * the .bpmnlintrc is looked up from `opts.lintFrom`. */ -export function checkLoadedDoc(doc: Doc, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(FILES, PROJECT).validate({ ...opts, source: { doc } }); } +export function checkLoadedDoc(doc: Doc, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(PROJECT, FILES).validate({ ...opts, source: { doc } }); } export { type PngImage, type PngRenderResult } from './adapters/out/files/PngRenderer.js'; export { BPMNLINT_CONFIG_FILE } from './adapters/out/files/ConfigLookup.js'; export type { BpmnlintChain, BpmnlintTier } from './adapters/out/files/ChainResolver.js'; diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts index bb1425c6..01416b36 100644 --- a/test/helpers/helpers.ts +++ b/test/helpers/helpers.ts @@ -18,10 +18,10 @@ const project = new BpmnlintProjectSettings(); /** The use cases on the file system, as src/cli.ts wires them. */ export const FILE_USE_CASES: CliUseCases = { - editDocument: new EditDocumentService(files, files, project), - createDocument: new CreateDocumentService(files, project), - relayoutDiagram: new RelayoutDiagramService(files, files, project), - validateDocument: new ValidateDocumentService(files, project), + editDocument: new EditDocumentService(project, files, files), + createDocument: new CreateDocumentService(project, files), + relayoutDiagram: new RelayoutDiagramService(project, files, files), + validateDocument: new ValidateDocumentService(project, files), readDocument: new ReadDocumentService(files), showModel: new ShowModelService(), showConventions: new ShowConventionsService(project),