@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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Structural validation for Camunda element templates.
3
+ *
4
+ * The published JSON schema
5
+ * (https://github.com/camunda/element-templates-json-schema) is the reference,
6
+ * but a JSON-schema engine is a dependency this package does not otherwise
7
+ * need, and a generic validator's messages ("must match exactly one schema in
8
+ * oneOf") are worse than useless to someone who mistyped a binding. So the
9
+ * rules the schema actually enforces are checked directly here, against the
10
+ * same shapes `template-types.ts` declares and `apply.ts` consumes: a template
11
+ * that passes is one this toolkit can render and apply, which is the question
12
+ * the caller is really asking.
13
+ */
14
+ import type { ElementTemplate } from "./template-types.js";
15
+ /** One reason a template was rejected. */
16
+ export interface TemplateProblem {
17
+ /** Where in the document, e.g. `properties[3].binding.type`. */
18
+ path: string;
19
+ message: string;
20
+ }
21
+ export interface TemplateValidation {
22
+ valid: boolean;
23
+ /** Reasons the template is not usable. A template with any of these is invalid. */
24
+ problems: TemplateProblem[];
25
+ /**
26
+ * Things that are valid but will not do what the author expects here — today,
27
+ * a binding the schema defines that `applyElementTemplate` does not write.
28
+ * Warnings never make a template invalid.
29
+ */
30
+ warnings: TemplateProblem[];
31
+ }
32
+ /**
33
+ * Checks one value against the element-template shape.
34
+ *
35
+ * @param value - Parsed JSON, from a file or anywhere else.
36
+ * @returns Every problem found, not just the first — a template with three
37
+ * mistakes should take one round trip to fix, not three.
38
+ */
39
+ export declare function validateElementTemplate(value: unknown): TemplateValidation;
40
+ /** One template read out of a document, with the problems that rejected it. */
41
+ export interface TemplateDocumentResult {
42
+ /** Templates that passed. */
43
+ templates: ElementTemplate[];
44
+ /** Problems, each naming the template index within the document. */
45
+ problems: Array<TemplateProblem & {
46
+ index: number;
47
+ id?: string;
48
+ }>;
49
+ /** Warnings from the templates that passed, named the same way. */
50
+ warnings: Array<TemplateProblem & {
51
+ index: number;
52
+ id?: string;
53
+ }>;
54
+ }
55
+ /**
56
+ * Reads a parsed template document, which Camunda allows to be either one
57
+ * template or an array of them.
58
+ *
59
+ * A template that fails validation is reported and left out — never silently
60
+ * dropped, and never allowed to take the rest of the file down with it.
61
+ *
62
+ * @param value - The parsed JSON of one `.json` file.
63
+ */
64
+ export declare function readTemplateDocument(value: unknown): TemplateDocumentResult;
65
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Structural validation for Camunda element templates.
3
+ *
4
+ * The published JSON schema
5
+ * (https://github.com/camunda/element-templates-json-schema) is the reference,
6
+ * but a JSON-schema engine is a dependency this package does not otherwise
7
+ * need, and a generic validator's messages ("must match exactly one schema in
8
+ * oneOf") are worse than useless to someone who mistyped a binding. So the
9
+ * rules the schema actually enforces are checked directly here, against the
10
+ * same shapes `template-types.ts` declares and `apply.ts` consumes: a template
11
+ * that passes is one this toolkit can render and apply, which is the question
12
+ * the caller is really asking.
13
+ */
14
+ import { APPLIED_BINDING_TYPES } from "./apply.js";
15
+ /** Property `type` values the schema allows. Absent means String. */
16
+ const PROPERTY_TYPES = new Set(["String", "Text", "Hidden", "Dropdown", "Boolean", "Number"]);
17
+ /** FEEL modes the schema allows. */
18
+ const FEEL_MODES = new Set(["optional", "required", "static"]);
19
+ /**
20
+ * Binding types, mapped to the extra field each one requires. An empty list
21
+ * means the binding needs nothing beyond its `type`.
22
+ */
23
+ const BINDING_FIELDS = {
24
+ property: ["name"],
25
+ "zeebe:taskDefinition:type": [],
26
+ "zeebe:taskDefinition": ["property"],
27
+ "zeebe:input": ["name"],
28
+ "zeebe:output": ["source"],
29
+ "zeebe:taskHeader": ["key"],
30
+ "zeebe:property": ["name"],
31
+ "zeebe:adHoc": ["property"],
32
+ "bpmn:Message#property": ["name"],
33
+ "bpmn:Message#zeebe:subscription#property": ["name"],
34
+ "zeebe:linkedResource": ["property", "linkName"],
35
+ };
36
+ const TASK_DEFINITION_PROPERTIES = new Set(["type", "retries"]);
37
+ const AD_HOC_PROPERTIES = new Set(["outputCollection", "outputElement", "activeElementsCollection"]);
38
+ function isRecord(value) {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+ function isNonEmptyString(value) {
42
+ return typeof value === "string" && value.trim() !== "";
43
+ }
44
+ function checkBinding(binding, path, problems, warnings) {
45
+ if (!isRecord(binding)) {
46
+ problems.push({ path, message: "binding must be an object" });
47
+ return;
48
+ }
49
+ const type = binding.type;
50
+ if (!isNonEmptyString(type)) {
51
+ problems.push({ path: `${path}.type`, message: "binding.type is required" });
52
+ return;
53
+ }
54
+ if (!APPLIED_BINDING_TYPES.has(type) && BINDING_FIELDS[type] !== undefined) {
55
+ warnings.push({
56
+ path: `${path}.type`,
57
+ message: `binding type "${type}" is valid but is not applied by this toolkit yet — the template will load, and this property will not be written`,
58
+ });
59
+ }
60
+ const required = BINDING_FIELDS[type];
61
+ if (required === undefined) {
62
+ problems.push({
63
+ path: `${path}.type`,
64
+ message: `unknown binding type "${type}" — expected one of ${Object.keys(BINDING_FIELDS).join(", ")}`,
65
+ });
66
+ return;
67
+ }
68
+ for (const field of required) {
69
+ if (!isNonEmptyString(binding[field])) {
70
+ problems.push({
71
+ path: `${path}.${field}`,
72
+ message: `binding of type "${type}" requires a non-empty "${field}"`,
73
+ });
74
+ }
75
+ }
76
+ if (type === "zeebe:taskDefinition" && isNonEmptyString(binding.property)) {
77
+ if (!TASK_DEFINITION_PROPERTIES.has(binding.property)) {
78
+ problems.push({
79
+ path: `${path}.property`,
80
+ message: `expected one of ${[...TASK_DEFINITION_PROPERTIES].join(", ")}`,
81
+ });
82
+ }
83
+ }
84
+ if (type === "zeebe:adHoc" && isNonEmptyString(binding.property)) {
85
+ if (!AD_HOC_PROPERTIES.has(binding.property)) {
86
+ problems.push({
87
+ path: `${path}.property`,
88
+ message: `expected one of ${[...AD_HOC_PROPERTIES].join(", ")}`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ function checkProperty(property, path, problems, warnings) {
94
+ if (!isRecord(property)) {
95
+ problems.push({ path, message: "property must be an object" });
96
+ return;
97
+ }
98
+ const type = property.type;
99
+ if (type !== undefined && (typeof type !== "string" || !PROPERTY_TYPES.has(type))) {
100
+ problems.push({
101
+ path: `${path}.type`,
102
+ message: `unknown property type ${JSON.stringify(type)} — expected one of ${[...PROPERTY_TYPES].join(", ")}`,
103
+ });
104
+ }
105
+ if (property.feel !== undefined) {
106
+ if (typeof property.feel !== "string" || !FEEL_MODES.has(property.feel)) {
107
+ problems.push({
108
+ path: `${path}.feel`,
109
+ message: `expected one of ${[...FEEL_MODES].join(", ")}`,
110
+ });
111
+ }
112
+ }
113
+ if (type === "Dropdown") {
114
+ const choices = property.choices;
115
+ if (!Array.isArray(choices) || choices.length === 0) {
116
+ problems.push({ path: `${path}.choices`, message: "a Dropdown property needs choices" });
117
+ }
118
+ else {
119
+ choices.forEach((choice, index) => {
120
+ if (!isRecord(choice) || !isNonEmptyString(choice.value)) {
121
+ problems.push({
122
+ path: `${path}.choices[${index}]`,
123
+ message: "each choice needs a name and a value",
124
+ });
125
+ }
126
+ });
127
+ }
128
+ }
129
+ if (property.binding === undefined) {
130
+ problems.push({ path: `${path}.binding`, message: "binding is required" });
131
+ return;
132
+ }
133
+ checkBinding(property.binding, `${path}.binding`, problems, warnings);
134
+ }
135
+ /**
136
+ * Checks one value against the element-template shape.
137
+ *
138
+ * @param value - Parsed JSON, from a file or anywhere else.
139
+ * @returns Every problem found, not just the first — a template with three
140
+ * mistakes should take one round trip to fix, not three.
141
+ */
142
+ export function validateElementTemplate(value) {
143
+ const problems = [];
144
+ const warnings = [];
145
+ if (!isRecord(value)) {
146
+ return {
147
+ valid: false,
148
+ problems: [{ path: "", message: "template must be a JSON object" }],
149
+ warnings,
150
+ };
151
+ }
152
+ if (!isNonEmptyString(value.id)) {
153
+ problems.push({ path: "id", message: "id is required and must be a non-empty string" });
154
+ }
155
+ if (!isNonEmptyString(value.name)) {
156
+ problems.push({ path: "name", message: "name is required and must be a non-empty string" });
157
+ }
158
+ if (value.version !== undefined && !Number.isInteger(value.version)) {
159
+ problems.push({ path: "version", message: "version must be an integer" });
160
+ }
161
+ const appliesTo = value.appliesTo;
162
+ if (!Array.isArray(appliesTo) || appliesTo.length === 0) {
163
+ problems.push({ path: "appliesTo", message: "appliesTo is required and must be non-empty" });
164
+ }
165
+ else {
166
+ appliesTo.forEach((entry, index) => {
167
+ if (!isNonEmptyString(entry)) {
168
+ problems.push({ path: `appliesTo[${index}]`, message: "must be a BPMN type name" });
169
+ }
170
+ else if (!entry.startsWith("bpmn:")) {
171
+ problems.push({
172
+ path: `appliesTo[${index}]`,
173
+ message: `expected a "bpmn:" type, got "${entry}"`,
174
+ });
175
+ }
176
+ });
177
+ }
178
+ if (value.elementType !== undefined) {
179
+ if (!isRecord(value.elementType) || !isNonEmptyString(value.elementType.value)) {
180
+ problems.push({ path: "elementType.value", message: "elementType needs a value" });
181
+ }
182
+ }
183
+ if (value.groups !== undefined) {
184
+ if (!Array.isArray(value.groups)) {
185
+ problems.push({ path: "groups", message: "groups must be an array" });
186
+ }
187
+ else {
188
+ value.groups.forEach((group, index) => {
189
+ if (!isRecord(group) || !isNonEmptyString(group.id)) {
190
+ problems.push({ path: `groups[${index}].id`, message: "each group needs an id" });
191
+ }
192
+ });
193
+ }
194
+ }
195
+ if (value.icon !== undefined) {
196
+ if (!isRecord(value.icon) || !isNonEmptyString(value.icon.contents)) {
197
+ problems.push({ path: "icon.contents", message: "icon needs contents" });
198
+ }
199
+ }
200
+ const properties = value.properties;
201
+ if (!Array.isArray(properties)) {
202
+ problems.push({ path: "properties", message: "properties is required and must be an array" });
203
+ }
204
+ else {
205
+ properties.forEach((property, index) => {
206
+ checkProperty(property, `properties[${index}]`, problems, warnings);
207
+ });
208
+ }
209
+ return { valid: problems.length === 0, problems, warnings };
210
+ }
211
+ /**
212
+ * Reads a parsed template document, which Camunda allows to be either one
213
+ * template or an array of them.
214
+ *
215
+ * A template that fails validation is reported and left out — never silently
216
+ * dropped, and never allowed to take the rest of the file down with it.
217
+ *
218
+ * @param value - The parsed JSON of one `.json` file.
219
+ */
220
+ export function readTemplateDocument(value) {
221
+ const entries = Array.isArray(value) ? value : [value];
222
+ const templates = [];
223
+ const problems = [];
224
+ const warnings = [];
225
+ entries.forEach((entry, index) => {
226
+ const result = validateElementTemplate(entry);
227
+ const id = isRecord(entry) && typeof entry.id === "string" ? entry.id : undefined;
228
+ const locate = (problem) => id === undefined ? { ...problem, index } : { ...problem, index, id };
229
+ if (result.valid) {
230
+ templates.push(entry);
231
+ warnings.push(...result.warnings.map(locate));
232
+ return;
233
+ }
234
+ problems.push(...result.problems.map(locate));
235
+ });
236
+ return { templates, problems, warnings };
237
+ }
238
+ //# sourceMappingURL=validate.js.map
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@bpmnkit/connectors",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
+ },
11
+ "./node": {
12
+ "import": "./dist/node/index.js",
13
+ "types": "./dist/node/index.d.ts"
14
+ }
15
+ },
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "LICENSE",
20
+ "README.md",
21
+ "dist/**/*.js",
22
+ "dist/**/*.d.ts"
23
+ ],
24
+ "dependencies": {
25
+ "@bpmnkit/core": "0.3.0",
26
+ "@bpmnkit/feel": "0.0.20"
27
+ },
28
+ "description": "Camunda 8 out-of-the-box connector catalog and deterministic element-template application for @bpmnkit/core",
29
+ "keywords": [
30
+ "camunda",
31
+ "connector",
32
+ "element-template",
33
+ "bpmn",
34
+ "zeebe",
35
+ "typescript"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/bpmnkit/monorepo"
41
+ },
42
+ "homepage": "https://bpmnkit.com",
43
+ "bugs": {
44
+ "url": "https://github.com/bpmnkit/monorepo/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc",
51
+ "typecheck": "tsc --noEmit",
52
+ "check": "biome check .",
53
+ "test": "vitest run"
54
+ }
55
+ }