@knime/hub-features 1.26.2 → 1.28.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/CHANGELOG.md +18 -0
- package/package.json +5 -3
- package/src/analytics/README.md +19 -73
- package/src/analytics/analytics.ts +97 -57
- package/src/analytics/index.ts +1 -1
- package/src/analytics/schema/cloudhome-event-functions.ts +247 -0
- package/src/analytics/schema/code-generator.js +158 -0
- package/src/analytics/schema/editor-event-functions.ts +1121 -0
- package/src/analytics/schema/schema.d.ts +765 -122
- package/src/analytics/schema/schema.json +3916 -269
- package/src/analytics/types.ts +22 -46
- package/src/analytics/utils/logger.ts +1 -0
- package/src/analytics/utils/eventIds.ts +0 -35
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const schemaPath = path.join(__dirname, "schema.json");
|
|
7
|
+
const editorOutputPath = path.join(__dirname, "editor-event-functions.ts");
|
|
8
|
+
const cloudHomeOutputPath = path.join(
|
|
9
|
+
__dirname,
|
|
10
|
+
"cloudhome-event-functions.ts",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
14
|
+
const events = schema.properties.events.properties;
|
|
15
|
+
|
|
16
|
+
const TYPE_HELPERS = {
|
|
17
|
+
analyticsEvent: "AnalyticsEvent",
|
|
18
|
+
defaultContext: "DefaultContext",
|
|
19
|
+
fnParams: "EventFunctionArgs",
|
|
20
|
+
generatedStaticFields: "GeneratedStaticFields",
|
|
21
|
+
schemaStaticFields: "SchemaStaticFields",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const HEADER_LINES = [
|
|
25
|
+
"/* eslint-disable max-lines */",
|
|
26
|
+
"/* eslint-disable camelcase */",
|
|
27
|
+
"/* eslint-disable @typescript-eslint/no-explicit-any */",
|
|
28
|
+
"// AUTO-GENERATED FILE. DO NOT EDIT.",
|
|
29
|
+
`import type { ${Object.values(TYPE_HELPERS).join(", ")} } from "../types";`,
|
|
30
|
+
'import { toSnakeCaseDeep } from "../utils/toSnakeCaseDeep";',
|
|
31
|
+
];
|
|
32
|
+
const editorResult = [];
|
|
33
|
+
const cloudHomeResult = [];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generic event definition, read from the schema.json.
|
|
37
|
+
* @typedef {{
|
|
38
|
+
* properties: {
|
|
39
|
+
* data: { $ref?: string };
|
|
40
|
+
* [key: string]: any;
|
|
41
|
+
* };
|
|
42
|
+
* $comment?: string;
|
|
43
|
+
* }} EventDefinition
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate the JSDoc string for the generated event function, if it has a `$comment` field
|
|
48
|
+
* in the schema
|
|
49
|
+
* @param {EventDefinition} eventDef
|
|
50
|
+
* @returns {String}
|
|
51
|
+
*/
|
|
52
|
+
const generateJSDocComment = (eventDef) => {
|
|
53
|
+
// eslint-disable-next-line no-useless-concat
|
|
54
|
+
const jsdoc = "/**\n" + ` * ${eventDef.$comment}\n` + " */";
|
|
55
|
+
return jsdoc;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolves the const value from a property, handling both direct `const` and
|
|
60
|
+
* `allOf`-based patterns like `{ allOf: [{ $ref: "..." }, { const: "value" }] }`
|
|
61
|
+
* @param {{ const?: any; allOf?: Array<{ const?: any }> }} prop
|
|
62
|
+
* @returns {any | undefined}
|
|
63
|
+
*/
|
|
64
|
+
const resolveConst = (prop) => {
|
|
65
|
+
if (prop.const !== undefined) {
|
|
66
|
+
return prop.const;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(prop.allOf)) {
|
|
69
|
+
return prop.allOf.find((s) => s.const !== undefined)?.const;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generates an event function for an event based on its name and definition
|
|
76
|
+
* @param {String} fnName
|
|
77
|
+
* @param {EventDefinition} eventDef
|
|
78
|
+
* @param {Boolean} isPayloadRequired whether the function requires a data payload
|
|
79
|
+
* @returns {String} function as a string to be added to the generated file
|
|
80
|
+
*/
|
|
81
|
+
const generateEventFunction = (fnName, eventDef, isPayloadRequired = true) => {
|
|
82
|
+
const params = isPayloadRequired
|
|
83
|
+
? `eventData: ${TYPE_HELPERS.fnParams}<"${fnName}">`
|
|
84
|
+
: "";
|
|
85
|
+
|
|
86
|
+
let fn =
|
|
87
|
+
// function header
|
|
88
|
+
`export const ${fnName} = <T extends ${TYPE_HELPERS.defaultContext}>(ctx: T) => (${params}): ${TYPE_HELPERS.analyticsEvent} => {\n` +
|
|
89
|
+
// open object for static data and add properties that are static but generated at runtime
|
|
90
|
+
" const staticData = {\n" +
|
|
91
|
+
" ...toSnakeCaseDeep(ctx),\n" +
|
|
92
|
+
" unique_event_id: crypto.randomUUID(),\n" +
|
|
93
|
+
" timestamp: new Date().toISOString(),\n";
|
|
94
|
+
|
|
95
|
+
for (const [key, prop] of Object.entries(eventDef.properties)) {
|
|
96
|
+
// add properties that are static and whose values come from the schema
|
|
97
|
+
const constValue = resolveConst(prop);
|
|
98
|
+
if (constValue !== undefined) {
|
|
99
|
+
fn += ` ${key}: "${constValue}",\n`;
|
|
100
|
+
} else if (prop.type === "object" && prop.properties) {
|
|
101
|
+
// nested object (e.g. action): emit only sub-properties that have const values
|
|
102
|
+
fn += ` ${key}: {\n`;
|
|
103
|
+
for (const [subKey, subProp] of Object.entries(prop.properties)) {
|
|
104
|
+
const subConstValue = resolveConst(subProp);
|
|
105
|
+
if (subConstValue !== undefined) {
|
|
106
|
+
fn += ` ${subKey}: "${subConstValue}",\n`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
fn += " },\n";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// close object for staticData
|
|
114
|
+
fn += ` } satisfies ${TYPE_HELPERS.schemaStaticFields} & ${TYPE_HELPERS.generatedStaticFields};\n\n`;
|
|
115
|
+
|
|
116
|
+
const payload = isPayloadRequired
|
|
117
|
+
? "{ ...staticData, payload: { ...toSnakeCaseDeep(eventData as Record<string, any>) } }"
|
|
118
|
+
: "{ ...staticData }";
|
|
119
|
+
|
|
120
|
+
fn += ` const event = { id: staticData.event_name, data: ${payload} };\n\n`;
|
|
121
|
+
fn += " return event;\n";
|
|
122
|
+
fn += "};\n\n";
|
|
123
|
+
|
|
124
|
+
return fn;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks whether the given event definition has an empty payload or requires data
|
|
129
|
+
* @param {EventDefinition} eventDef
|
|
130
|
+
* @returns
|
|
131
|
+
*/
|
|
132
|
+
const hasEmptyPayload = (eventDef) => {
|
|
133
|
+
return Boolean(eventDef.properties.payload?.$ref?.includes("EmptyPayload"));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
for (const [fnName, eventDef] of Object.entries(events)) {
|
|
137
|
+
const isPayloadRequired = !hasEmptyPayload(eventDef);
|
|
138
|
+
let fn = generateEventFunction(fnName, eventDef, isPayloadRequired);
|
|
139
|
+
|
|
140
|
+
if (eventDef.$comment) {
|
|
141
|
+
const jsdoc = generateJSDocComment(eventDef);
|
|
142
|
+
fn = `${jsdoc}\n${fn}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (eventDef.properties.event_source.const === "editor") {
|
|
146
|
+
editorResult.push(fn);
|
|
147
|
+
} else if (eventDef.properties.event_source.const === "cloudhome") {
|
|
148
|
+
cloudHomeResult.push(fn);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const editorFileContent = `${HEADER_LINES.join("\n")}\n\n${editorResult.join("")}`;
|
|
153
|
+
const cloudHomeFileContent = `${HEADER_LINES.join("\n")}\n\n${cloudHomeResult.join("")}`;
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(editorOutputPath, editorFileContent, "utf8");
|
|
156
|
+
fs.writeFileSync(cloudHomeOutputPath, cloudHomeFileContent, "utf8");
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.log("Generated static data based on the Events JSON Schema.");
|