@bpmnkit/connectors 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/apply.d.ts +67 -0
- package/dist/apply.js +243 -0
- package/dist/catalog.d.ts +76 -0
- package/dist/catalog.js +195 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/node/discover.d.ts +90 -0
- package/dist/node/discover.js +176 -0
- package/dist/node/index.d.ts +12 -0
- package/dist/node/index.js +11 -0
- package/dist/template-types.d.ts +143 -0
- package/dist/template-types.js +6 -0
- package/dist/templates/generated.d.ts +3 -0
- package/dist/templates/generated.js +57865 -0
- package/dist/validate.d.ts +65 -0
- package/dist/validate.js +238 -0
- package/package.json +55 -0
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { CAMUNDA_CONNECTOR_TEMPLATES } from "./templates/generated.js";
|
|
2
|
+
const SECRET_PATTERN = /token|secret|password|api.?key|apikey|credential|access.?key/i;
|
|
3
|
+
function isSecretField(prop, key) {
|
|
4
|
+
return SECRET_PATTERN.test(`${prop.label ?? ""} ${key}`);
|
|
5
|
+
}
|
|
6
|
+
/** Same key-derivation logic used at apply time — kept in sync with `apply.ts`. */
|
|
7
|
+
export function propertyKey(prop) {
|
|
8
|
+
if (prop.id)
|
|
9
|
+
return prop.id;
|
|
10
|
+
const b = prop.binding;
|
|
11
|
+
if (b.type === "zeebe:input")
|
|
12
|
+
return b.name;
|
|
13
|
+
if (b.type === "zeebe:output")
|
|
14
|
+
return b.source;
|
|
15
|
+
if (b.type === "zeebe:taskHeader")
|
|
16
|
+
return b.key;
|
|
17
|
+
if (b.type === "zeebe:taskDefinition")
|
|
18
|
+
return `taskDef.${b.property}`;
|
|
19
|
+
if (b.type === "zeebe:taskDefinition:type")
|
|
20
|
+
return "taskDef.type";
|
|
21
|
+
if (b.type === "property")
|
|
22
|
+
return b.name;
|
|
23
|
+
if (b.type === "zeebe:property")
|
|
24
|
+
return b.name;
|
|
25
|
+
if (b.type === "zeebe:adHoc")
|
|
26
|
+
return `adHoc.${b.property}`;
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
function toInputSpec(prop) {
|
|
30
|
+
const key = propertyKey(prop);
|
|
31
|
+
return {
|
|
32
|
+
key,
|
|
33
|
+
label: prop.label ?? key,
|
|
34
|
+
description: prop.description,
|
|
35
|
+
isSecret: isSecretField(prop, key),
|
|
36
|
+
isFeel: prop.feel === "required" || prop.feel === "optional",
|
|
37
|
+
default: prop.value,
|
|
38
|
+
choices: prop.choices,
|
|
39
|
+
condition: prop.condition,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function taskDefinitionType(template) {
|
|
43
|
+
for (const prop of template.properties) {
|
|
44
|
+
if (prop.binding.type === "zeebe:taskDefinition" && prop.binding.property === "type") {
|
|
45
|
+
return typeof prop.value === "string" ? prop.value : undefined;
|
|
46
|
+
}
|
|
47
|
+
if (prop.binding.type === "zeebe:taskDefinition:type") {
|
|
48
|
+
return typeof prop.value === "string" ? prop.value : undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function directionOf(template) {
|
|
54
|
+
const elementType = template.elementType?.value ?? template.appliesTo[0];
|
|
55
|
+
switch (elementType) {
|
|
56
|
+
case "bpmn:AdHocSubProcess":
|
|
57
|
+
return "agentic";
|
|
58
|
+
case "bpmn:StartEvent":
|
|
59
|
+
return "inbound-start";
|
|
60
|
+
case "bpmn:IntermediateCatchEvent":
|
|
61
|
+
case "bpmn:IntermediateThrowEvent":
|
|
62
|
+
case "bpmn:ReceiveTask":
|
|
63
|
+
return "inbound-intermediate";
|
|
64
|
+
case "bpmn:BoundaryEvent":
|
|
65
|
+
return "inbound-boundary";
|
|
66
|
+
default:
|
|
67
|
+
return "outbound";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function keywordsOf(template) {
|
|
71
|
+
const words = new Set();
|
|
72
|
+
for (const raw of `${template.name} ${template.description ?? ""}`
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.split(/[^a-z0-9]+/)) {
|
|
75
|
+
if (raw.length > 2)
|
|
76
|
+
words.add(raw);
|
|
77
|
+
}
|
|
78
|
+
return [...words];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What a template will do, without applying it.
|
|
82
|
+
*
|
|
83
|
+
* The catalogue computes this for every listing already; it is exported because
|
|
84
|
+
* anything offering a template to a person needs to be able to say what it
|
|
85
|
+
* binds and what it will ask for — a picker that applies on the first click is
|
|
86
|
+
* asking someone to choose blind.
|
|
87
|
+
*
|
|
88
|
+
* @param template - The template to describe.
|
|
89
|
+
*/
|
|
90
|
+
export function summarizeTemplate(template) {
|
|
91
|
+
const visible = template.properties.filter((p) => p.type !== "Hidden");
|
|
92
|
+
return {
|
|
93
|
+
id: template.id,
|
|
94
|
+
name: template.name,
|
|
95
|
+
description: template.description,
|
|
96
|
+
taskType: taskDefinitionType(template),
|
|
97
|
+
appliesTo: template.appliesTo,
|
|
98
|
+
direction: directionOf(template),
|
|
99
|
+
keywords: keywordsOf(template),
|
|
100
|
+
requiredInputs: visible.filter((p) => p.constraints?.notEmpty === true).map(toInputSpec),
|
|
101
|
+
optionalInputs: visible.filter((p) => p.constraints?.notEmpty !== true).map(toInputSpec),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Templates a host has registered on top of the bundled catalogue — a project's
|
|
106
|
+
* own `.camunda/element-templates/`, or anything else it has that the bundle
|
|
107
|
+
* does not.
|
|
108
|
+
*
|
|
109
|
+
* Kept separate rather than merged into one array so the bundle stays the
|
|
110
|
+
* constant it is declared to be, and so `clearRegisteredTemplates()` is exact.
|
|
111
|
+
*/
|
|
112
|
+
const registered = new Map();
|
|
113
|
+
let cachedSummaries;
|
|
114
|
+
/**
|
|
115
|
+
* Adds templates to the catalogue, replacing any bundled template with the same
|
|
116
|
+
* id.
|
|
117
|
+
*
|
|
118
|
+
* The workspace wins deliberately: a project that ships its own version of a
|
|
119
|
+
* connector means it, and the bundle is the fallback. Registering the same id
|
|
120
|
+
* twice keeps the later one, so a nearer directory can override a further one.
|
|
121
|
+
*
|
|
122
|
+
* @param templates - Validated templates. Nothing is checked here; run
|
|
123
|
+
* `validateElementTemplate` (or `readTemplateDocument`) at the boundary where
|
|
124
|
+
* the JSON was read, so a bad file is reported against its own path.
|
|
125
|
+
*/
|
|
126
|
+
export function registerElementTemplates(templates) {
|
|
127
|
+
for (const template of templates)
|
|
128
|
+
registered.set(template.id, template);
|
|
129
|
+
cachedSummaries = undefined;
|
|
130
|
+
}
|
|
131
|
+
/** Drops every registered template, leaving only the bundled catalogue. */
|
|
132
|
+
export function clearRegisteredTemplates() {
|
|
133
|
+
registered.clear();
|
|
134
|
+
cachedSummaries = undefined;
|
|
135
|
+
}
|
|
136
|
+
/** The bundled templates plus registered ones, the latter winning on id. */
|
|
137
|
+
function allTemplates() {
|
|
138
|
+
if (registered.size === 0)
|
|
139
|
+
return CAMUNDA_CONNECTOR_TEMPLATES;
|
|
140
|
+
const bundled = CAMUNDA_CONNECTOR_TEMPLATES.filter((t) => !registered.has(t.id));
|
|
141
|
+
return [...bundled, ...registered.values()];
|
|
142
|
+
}
|
|
143
|
+
/** Every connector template — bundled and registered — as a compact summary. */
|
|
144
|
+
export function listConnectors() {
|
|
145
|
+
if (!cachedSummaries) {
|
|
146
|
+
cachedSummaries = allTemplates().map(summarizeTemplate);
|
|
147
|
+
}
|
|
148
|
+
return cachedSummaries;
|
|
149
|
+
}
|
|
150
|
+
/** The full element template for a given template id, registered or bundled. */
|
|
151
|
+
export function getTemplate(id) {
|
|
152
|
+
return registered.get(id) ?? CAMUNDA_CONNECTOR_TEMPLATES.find((t) => t.id === id);
|
|
153
|
+
}
|
|
154
|
+
/** Tie-break preference when two templates score equally — outbound "do this" connectors are the common case. */
|
|
155
|
+
const DIRECTION_RANK = {
|
|
156
|
+
outbound: 0,
|
|
157
|
+
agentic: 1,
|
|
158
|
+
"inbound-start": 2,
|
|
159
|
+
"inbound-intermediate": 3,
|
|
160
|
+
"inbound-boundary": 4,
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Keyword-scored search over the bundled connector catalog — mirrors
|
|
164
|
+
* `@bpmnkit/patterns`' `findPattern()` matching style. Matches against the
|
|
165
|
+
* template name are weighted highest, then keyword-list matches, then a
|
|
166
|
+
* general substring match; ties prefer outbound connectors.
|
|
167
|
+
*/
|
|
168
|
+
export function searchConnectors(query) {
|
|
169
|
+
const terms = query
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.split(/[^a-z0-9]+/)
|
|
172
|
+
.filter((t) => t.length > 1);
|
|
173
|
+
if (terms.length === 0)
|
|
174
|
+
return [];
|
|
175
|
+
const scored = listConnectors()
|
|
176
|
+
.map((summary) => {
|
|
177
|
+
const nameWords = new Set(summary.name.toLowerCase().split(/[^a-z0-9]+/));
|
|
178
|
+
const haystack = `${summary.name} ${summary.description ?? ""} ${summary.keywords.join(" ")} ${summary.taskType ?? ""}`.toLowerCase();
|
|
179
|
+
let score = 0;
|
|
180
|
+
for (const term of terms) {
|
|
181
|
+
if (nameWords.has(term))
|
|
182
|
+
score += 4;
|
|
183
|
+
else if (summary.keywords.includes(term))
|
|
184
|
+
score += 3;
|
|
185
|
+
else if (haystack.includes(term))
|
|
186
|
+
score += 1;
|
|
187
|
+
}
|
|
188
|
+
return { summary, score };
|
|
189
|
+
})
|
|
190
|
+
.filter((s) => s.score > 0)
|
|
191
|
+
.sort((a, b) => b.score - a.score ||
|
|
192
|
+
DIRECTION_RANK[a.summary.direction] - DIRECTION_RANK[b.summary.direction]);
|
|
193
|
+
return scored.map((s) => s.summary);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=catalog.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { listConnectors, searchConnectors, getTemplate, propertyKey, registerElementTemplates, clearRegisteredTemplates, summarizeTemplate, } from "./catalog.js";
|
|
2
|
+
export type { ConnectorSummary, ConnectorInputSpec, ConnectorDirection } from "./catalog.js";
|
|
3
|
+
export { applyConnectorTemplate, applyElementTemplate } from "./apply.js";
|
|
4
|
+
export type { ApplyResult, ApplyProblem } from "./apply.js";
|
|
5
|
+
export { CAMUNDA_CONNECTOR_TEMPLATES } from "./templates/generated.js";
|
|
6
|
+
export type { ElementTemplate, TemplateGroup, TemplateProperty, TemplateBinding, TemplateCondition, } from "./template-types.js";
|
|
7
|
+
export { validateElementTemplate, readTemplateDocument } from "./validate.js";
|
|
8
|
+
export type { TemplateProblem, TemplateValidation, TemplateDocumentResult, } from "./validate.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { listConnectors, searchConnectors, getTemplate, propertyKey, registerElementTemplates, clearRegisteredTemplates, summarizeTemplate, } from "./catalog.js";
|
|
2
|
+
export { applyConnectorTemplate, applyElementTemplate } from "./apply.js";
|
|
3
|
+
export { CAMUNDA_CONNECTOR_TEMPLATES } from "./templates/generated.js";
|
|
4
|
+
export { validateElementTemplate, readTemplateDocument } from "./validate.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ElementTemplate } from "../template-types.js";
|
|
2
|
+
import { type TemplateProblem } from "../validate.js";
|
|
3
|
+
/** The folder name searched at each level, unless the caller names another. */
|
|
4
|
+
export declare const DEFAULT_CONFIG_FOLDER = ".camunda";
|
|
5
|
+
/** The sub-folder inside the config folder that holds templates. */
|
|
6
|
+
export declare const TEMPLATES_SUBFOLDER = "element-templates";
|
|
7
|
+
export interface DiscoverOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Where to start looking — a `.bpmn` file, or the directory holding it.
|
|
10
|
+
* The search walks up from here.
|
|
11
|
+
*/
|
|
12
|
+
from: string;
|
|
13
|
+
/**
|
|
14
|
+
* Where to stop. The walk includes this directory and goes no further, so a
|
|
15
|
+
* template outside the project cannot be picked up by accident. Defaults to
|
|
16
|
+
* the filesystem root, which is almost never what a caller wants.
|
|
17
|
+
*/
|
|
18
|
+
root?: string;
|
|
19
|
+
/** Config folder name. Defaults to `.camunda`. */
|
|
20
|
+
configFolder?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A template that could not be used, and where it came from. */
|
|
23
|
+
export interface DiscoveryProblem extends TemplateProblem {
|
|
24
|
+
/** Absolute path of the file the problem was found in. */
|
|
25
|
+
file: string;
|
|
26
|
+
/** The template's id, when the document got far enough to have one. */
|
|
27
|
+
id?: string;
|
|
28
|
+
/** Index within the file, for a document holding an array of templates. */
|
|
29
|
+
index?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface DiscoveryResult {
|
|
32
|
+
/** Valid templates, nearest directory last so a later register wins. */
|
|
33
|
+
templates: ElementTemplate[];
|
|
34
|
+
/** Everything rejected, each named by file — nothing is dropped silently. */
|
|
35
|
+
problems: DiscoveryProblem[];
|
|
36
|
+
/** Valid but worth saying, e.g. a binding this toolkit cannot apply yet. */
|
|
37
|
+
warnings: DiscoveryProblem[];
|
|
38
|
+
/** The template directories that were read, nearest last. */
|
|
39
|
+
directories: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Finds a project's own element templates by convention.
|
|
43
|
+
*
|
|
44
|
+
* Every directory from `root` down to the one holding `from` is checked for
|
|
45
|
+
* `<configFolder>/element-templates/*.json`. No project configuration, no
|
|
46
|
+
* registration step: drop a template next to the diagram that uses it and the
|
|
47
|
+
* tools pick it up.
|
|
48
|
+
*
|
|
49
|
+
* Order is deliberate. Directories are read root-first so the nearest one is
|
|
50
|
+
* read last, and `registerElementTemplates` keeps the last registration of an
|
|
51
|
+
* id — a template beside the diagram overrides one at the project root, which
|
|
52
|
+
* overrides the bundled catalogue.
|
|
53
|
+
*
|
|
54
|
+
* A file that is not valid JSON, or holds a template the schema rejects, is
|
|
55
|
+
* reported in `problems` against its own path and left out of `templates`. One
|
|
56
|
+
* bad file never costs the caller the good ones beside it.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const { templates, problems } = await discoverElementTemplates({
|
|
61
|
+
* from: "processes/orders/order.bpmn",
|
|
62
|
+
* root: process.cwd(),
|
|
63
|
+
* });
|
|
64
|
+
* registerElementTemplates(templates);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function discoverElementTemplates(options: DiscoverOptions): Promise<DiscoveryResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Reads every element template a project holds, wherever it sits.
|
|
70
|
+
*
|
|
71
|
+
* The sibling of {@link discoverElementTemplates}, and the opposite walk. That
|
|
72
|
+
* one answers "which templates apply to this diagram" by climbing from a file
|
|
73
|
+
* to the root; this one answers "is everything in this project valid" by
|
|
74
|
+
* descending from the root, which is the question a CI check asks. A project
|
|
75
|
+
* with templates beside a sub-folder's diagrams would otherwise pass a check
|
|
76
|
+
* that only ever looked at the root.
|
|
77
|
+
*
|
|
78
|
+
* The walk is breadth-first, so templates arrive shallowest-first and a deeper
|
|
79
|
+
* directory's version of an id registers last and wins. That is a reasonable
|
|
80
|
+
* merge for a host keeping one registry for a whole project, but it is not the
|
|
81
|
+
* same thing as per-file resolution: which template a *particular* diagram
|
|
82
|
+
* should see is {@link discoverElementTemplates}, and only that walk knows.
|
|
83
|
+
*
|
|
84
|
+
* @param options - `root` to scan, and the config folder name to look for.
|
|
85
|
+
*/
|
|
86
|
+
export declare function collectElementTemplates(options: {
|
|
87
|
+
root: string;
|
|
88
|
+
configFolder?: string;
|
|
89
|
+
}): Promise<DiscoveryResult>;
|
|
90
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { readTemplateDocument } from "../validate.js";
|
|
4
|
+
/** The folder name searched at each level, unless the caller names another. */
|
|
5
|
+
export const DEFAULT_CONFIG_FOLDER = ".camunda";
|
|
6
|
+
/** The sub-folder inside the config folder that holds templates. */
|
|
7
|
+
export const TEMPLATES_SUBFOLDER = "element-templates";
|
|
8
|
+
/** Directories from `root` down to `from`, so the nearest one is read last. */
|
|
9
|
+
function directoriesFromRootDown(from, root) {
|
|
10
|
+
const start = resolve(from);
|
|
11
|
+
if (root === undefined) {
|
|
12
|
+
// No root given: walk to the filesystem root.
|
|
13
|
+
const chain = [];
|
|
14
|
+
for (let dir = start;; dir = dirname(dir)) {
|
|
15
|
+
chain.push(dir);
|
|
16
|
+
if (dirname(dir) === dir)
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
return chain.reverse();
|
|
20
|
+
}
|
|
21
|
+
const stop = resolve(root);
|
|
22
|
+
// A start outside the root would otherwise walk past it and out of the
|
|
23
|
+
// project; search only where the caller said it was allowed to.
|
|
24
|
+
if (start !== stop && !start.startsWith(stop + sep))
|
|
25
|
+
return [start];
|
|
26
|
+
const chain = [];
|
|
27
|
+
for (let dir = start;; dir = dirname(dir)) {
|
|
28
|
+
chain.push(dir);
|
|
29
|
+
if (dir === stop || dirname(dir) === dir)
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return chain.reverse();
|
|
33
|
+
}
|
|
34
|
+
async function isDirectory(path) {
|
|
35
|
+
try {
|
|
36
|
+
return (await stat(path)).isDirectory();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Finds a project's own element templates by convention.
|
|
44
|
+
*
|
|
45
|
+
* Every directory from `root` down to the one holding `from` is checked for
|
|
46
|
+
* `<configFolder>/element-templates/*.json`. No project configuration, no
|
|
47
|
+
* registration step: drop a template next to the diagram that uses it and the
|
|
48
|
+
* tools pick it up.
|
|
49
|
+
*
|
|
50
|
+
* Order is deliberate. Directories are read root-first so the nearest one is
|
|
51
|
+
* read last, and `registerElementTemplates` keeps the last registration of an
|
|
52
|
+
* id — a template beside the diagram overrides one at the project root, which
|
|
53
|
+
* overrides the bundled catalogue.
|
|
54
|
+
*
|
|
55
|
+
* A file that is not valid JSON, or holds a template the schema rejects, is
|
|
56
|
+
* reported in `problems` against its own path and left out of `templates`. One
|
|
57
|
+
* bad file never costs the caller the good ones beside it.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const { templates, problems } = await discoverElementTemplates({
|
|
62
|
+
* from: "processes/orders/order.bpmn",
|
|
63
|
+
* root: process.cwd(),
|
|
64
|
+
* });
|
|
65
|
+
* registerElementTemplates(templates);
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export async function discoverElementTemplates(options) {
|
|
69
|
+
const { from, root, configFolder = DEFAULT_CONFIG_FOLDER } = options;
|
|
70
|
+
// `from` may name the diagram rather than its folder.
|
|
71
|
+
const startDir = (await isDirectory(from)) ? resolve(from) : dirname(resolve(from));
|
|
72
|
+
const templates = [];
|
|
73
|
+
const problems = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const directories = [];
|
|
76
|
+
for (const dir of directoriesFromRootDown(startDir, root)) {
|
|
77
|
+
const templateDir = join(dir, configFolder, TEMPLATES_SUBFOLDER);
|
|
78
|
+
if (!(await isDirectory(templateDir)))
|
|
79
|
+
continue;
|
|
80
|
+
directories.push(templateDir);
|
|
81
|
+
const found = await readTemplateDirectory(templateDir);
|
|
82
|
+
templates.push(...found.templates);
|
|
83
|
+
problems.push(...found.problems);
|
|
84
|
+
warnings.push(...found.warnings);
|
|
85
|
+
}
|
|
86
|
+
return { templates, problems, warnings, directories };
|
|
87
|
+
}
|
|
88
|
+
/** Reads one `element-templates` directory, in a stable filename order. */
|
|
89
|
+
async function readTemplateDirectory(templateDir) {
|
|
90
|
+
const templates = [];
|
|
91
|
+
const problems = [];
|
|
92
|
+
const warnings = [];
|
|
93
|
+
const entries = await readdir(templateDir, { withFileTypes: true });
|
|
94
|
+
const files = entries
|
|
95
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
|
|
96
|
+
.map((entry) => entry.name)
|
|
97
|
+
.sort();
|
|
98
|
+
for (const name of files) {
|
|
99
|
+
const file = join(templateDir, name);
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(await readFile(file, "utf-8"));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
problems.push({
|
|
106
|
+
file,
|
|
107
|
+
path: "",
|
|
108
|
+
message: `not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const result = readTemplateDocument(parsed);
|
|
113
|
+
templates.push(...result.templates);
|
|
114
|
+
for (const problem of result.problems)
|
|
115
|
+
problems.push({ ...problem, file });
|
|
116
|
+
for (const warning of result.warnings)
|
|
117
|
+
warnings.push({ ...warning, file });
|
|
118
|
+
}
|
|
119
|
+
return { templates, problems, warnings };
|
|
120
|
+
}
|
|
121
|
+
/** Directories never worth descending into when scanning a project. */
|
|
122
|
+
const SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
|
|
123
|
+
/**
|
|
124
|
+
* Reads every element template a project holds, wherever it sits.
|
|
125
|
+
*
|
|
126
|
+
* The sibling of {@link discoverElementTemplates}, and the opposite walk. That
|
|
127
|
+
* one answers "which templates apply to this diagram" by climbing from a file
|
|
128
|
+
* to the root; this one answers "is everything in this project valid" by
|
|
129
|
+
* descending from the root, which is the question a CI check asks. A project
|
|
130
|
+
* with templates beside a sub-folder's diagrams would otherwise pass a check
|
|
131
|
+
* that only ever looked at the root.
|
|
132
|
+
*
|
|
133
|
+
* The walk is breadth-first, so templates arrive shallowest-first and a deeper
|
|
134
|
+
* directory's version of an id registers last and wins. That is a reasonable
|
|
135
|
+
* merge for a host keeping one registry for a whole project, but it is not the
|
|
136
|
+
* same thing as per-file resolution: which template a *particular* diagram
|
|
137
|
+
* should see is {@link discoverElementTemplates}, and only that walk knows.
|
|
138
|
+
*
|
|
139
|
+
* @param options - `root` to scan, and the config folder name to look for.
|
|
140
|
+
*/
|
|
141
|
+
export async function collectElementTemplates(options) {
|
|
142
|
+
const { root, configFolder = DEFAULT_CONFIG_FOLDER } = options;
|
|
143
|
+
const templates = [];
|
|
144
|
+
const problems = [];
|
|
145
|
+
const warnings = [];
|
|
146
|
+
const directories = [];
|
|
147
|
+
const queue = [resolve(root)];
|
|
148
|
+
while (queue.length > 0) {
|
|
149
|
+
const dir = queue.shift();
|
|
150
|
+
if (dir === undefined)
|
|
151
|
+
continue;
|
|
152
|
+
let entries;
|
|
153
|
+
try {
|
|
154
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (!entry.isDirectory() || SKIP_DIRECTORIES.has(entry.name))
|
|
161
|
+
continue;
|
|
162
|
+
queue.push(join(dir, entry.name));
|
|
163
|
+
}
|
|
164
|
+
const templateDir = join(dir, configFolder, TEMPLATES_SUBFOLDER);
|
|
165
|
+
if (!(await isDirectory(templateDir)))
|
|
166
|
+
continue;
|
|
167
|
+
directories.push(templateDir);
|
|
168
|
+
const found = await readTemplateDirectory(templateDir);
|
|
169
|
+
templates.push(...found.templates);
|
|
170
|
+
problems.push(...found.problems);
|
|
171
|
+
warnings.push(...found.warnings);
|
|
172
|
+
}
|
|
173
|
+
directories.sort();
|
|
174
|
+
return { templates, problems, warnings, directories };
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@bpmnkit/connectors/node` — the filesystem half of element templates.
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own entry point so the main package stays importable in a
|
|
5
|
+
* browser: a studio or a viewer takes templates from its host and never reaches
|
|
6
|
+
* for `node:fs`.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export { collectElementTemplates, discoverElementTemplates, DEFAULT_CONFIG_FOLDER, TEMPLATES_SUBFOLDER, } from "./discover.js";
|
|
11
|
+
export type { DiscoverOptions, DiscoveryProblem, DiscoveryResult } from "./discover.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@bpmnkit/connectors/node` — the filesystem half of element templates.
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own entry point so the main package stays importable in a
|
|
5
|
+
* browser: a studio or a viewer takes templates from its host and never reaches
|
|
6
|
+
* for `node:fs`.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export { collectElementTemplates, discoverElementTemplates, DEFAULT_CONFIG_FOLDER, TEMPLATES_SUBFOLDER, } from "./discover.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript types matching the Camunda element template JSON schema.
|
|
3
|
+
* https://github.com/camunda/element-templates-json-schema
|
|
4
|
+
*/
|
|
5
|
+
export interface ElementTemplate {
|
|
6
|
+
/** Unique template identifier (reverse-domain, e.g. "io.camunda.connectors.HttpJson.v2"). */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Display name shown in the template catalog. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Short description. */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Integer version; templates with the same id + different version are distinct. */
|
|
13
|
+
version?: number;
|
|
14
|
+
/** BPMN element types this template applies to (e.g. "bpmn:ServiceTask", "bpmn:Task"). */
|
|
15
|
+
appliesTo: string[];
|
|
16
|
+
/** Forces the element to be converted to this type on apply. */
|
|
17
|
+
elementType?: {
|
|
18
|
+
value: string;
|
|
19
|
+
};
|
|
20
|
+
/** UI section definitions (collapsible groups). */
|
|
21
|
+
groups?: TemplateGroup[];
|
|
22
|
+
/** All property definitions. */
|
|
23
|
+
properties: TemplateProperty[];
|
|
24
|
+
/** Link to external documentation. */
|
|
25
|
+
documentationRef?: string;
|
|
26
|
+
/** Custom icon: { contents: "data:image/svg+xml;base64,..." } */
|
|
27
|
+
icon?: {
|
|
28
|
+
contents: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface TemplateGroup {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
tooltip?: string;
|
|
35
|
+
openByDefault?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface TemplateProperty {
|
|
38
|
+
/** Unique key used in condition references. Falls back to binding name if absent. */
|
|
39
|
+
id?: string;
|
|
40
|
+
/** Label shown above the input. Hidden properties have no label. */
|
|
41
|
+
label?: string;
|
|
42
|
+
/** Hint text shown below the input. */
|
|
43
|
+
description?: string;
|
|
44
|
+
/** UI control type. */
|
|
45
|
+
type: "String" | "Text" | "Hidden" | "Dropdown" | "Boolean" | "Number";
|
|
46
|
+
/** Default value applied when the template is first used. */
|
|
47
|
+
value?: string | number | boolean;
|
|
48
|
+
/** Placeholder text. */
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
/** If true, empty values are not written to the BPMN XML. */
|
|
51
|
+
optional?: boolean;
|
|
52
|
+
/** FEEL expression mode (only relevant for String/Text). */
|
|
53
|
+
feel?: "optional" | "required" | "static";
|
|
54
|
+
/** Which template group this property belongs to. */
|
|
55
|
+
group?: string;
|
|
56
|
+
/** Tooltip text. */
|
|
57
|
+
tooltip?: string;
|
|
58
|
+
/** How this property maps to the BPMN XML. */
|
|
59
|
+
binding: TemplateBinding;
|
|
60
|
+
/** Conditional visibility. */
|
|
61
|
+
condition?: TemplateCondition;
|
|
62
|
+
/** Required for Dropdown type. */
|
|
63
|
+
choices?: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
value: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** Validation constraints. */
|
|
68
|
+
constraints?: {
|
|
69
|
+
notEmpty?: boolean;
|
|
70
|
+
minLength?: number;
|
|
71
|
+
maxLength?: number;
|
|
72
|
+
pattern?: string | {
|
|
73
|
+
value: string;
|
|
74
|
+
message: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** All supported binding types. */
|
|
79
|
+
export type TemplateBinding = {
|
|
80
|
+
type: "property";
|
|
81
|
+
name: string;
|
|
82
|
+
}
|
|
83
|
+
/** @deprecated Use zeebe:taskDefinition with property field instead. */
|
|
84
|
+
| {
|
|
85
|
+
type: "zeebe:taskDefinition:type";
|
|
86
|
+
} | {
|
|
87
|
+
type: "zeebe:taskDefinition";
|
|
88
|
+
property: "type" | "retries";
|
|
89
|
+
} | {
|
|
90
|
+
type: "zeebe:input";
|
|
91
|
+
name: string;
|
|
92
|
+
} | {
|
|
93
|
+
type: "zeebe:output";
|
|
94
|
+
source: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: "zeebe:taskHeader";
|
|
97
|
+
key: string;
|
|
98
|
+
} | {
|
|
99
|
+
type: "zeebe:property";
|
|
100
|
+
name: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: "zeebe:adHoc";
|
|
103
|
+
property: "outputCollection" | "outputElement" | "activeElementsCollection";
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Inbound-connector bindings, and the linked-resource binding used by RPA
|
|
107
|
+
* templates. The bundled catalogue uses all three, so they belong in the
|
|
108
|
+
* union — but `applyElementTemplate` does not write them yet, and
|
|
109
|
+
* `validateElementTemplate` warns when a template depends on one.
|
|
110
|
+
*/
|
|
111
|
+
| {
|
|
112
|
+
type: "bpmn:Message#property";
|
|
113
|
+
name: string;
|
|
114
|
+
} | {
|
|
115
|
+
type: "bpmn:Message#zeebe:subscription#property";
|
|
116
|
+
name: string;
|
|
117
|
+
} | {
|
|
118
|
+
type: "zeebe:linkedResource";
|
|
119
|
+
property: string;
|
|
120
|
+
linkName: string;
|
|
121
|
+
};
|
|
122
|
+
/** Condition controlling whether a property is shown in the UI. */
|
|
123
|
+
export type TemplateCondition = {
|
|
124
|
+
property: string;
|
|
125
|
+
equals: string;
|
|
126
|
+
type?: string;
|
|
127
|
+
} | {
|
|
128
|
+
property: string;
|
|
129
|
+
oneOf: string[];
|
|
130
|
+
type?: string;
|
|
131
|
+
} | {
|
|
132
|
+
property: string;
|
|
133
|
+
isActive: boolean;
|
|
134
|
+
type?: string;
|
|
135
|
+
} | {
|
|
136
|
+
allMatch: Array<{
|
|
137
|
+
property: string;
|
|
138
|
+
equals?: string;
|
|
139
|
+
oneOf?: string[];
|
|
140
|
+
isActive?: boolean;
|
|
141
|
+
}>;
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=template-types.d.ts.map
|