@teamkeel/testing-runtime 0.415.4 → 0.416.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamkeel/testing-runtime",
3
- "version": "0.415.4",
3
+ "version": "0.416.0",
4
4
  "description": "Internal package used by the generated @teamkeel/testing package",
5
5
  "exports": "./src/index.mjs",
6
6
  "typings": "src/index.d.ts",
package/src/Executor.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import jwt from "jsonwebtoken";
2
- import { InlineFile, File, Duration } from "@teamkeel/functions-runtime";
2
+ import { parseInputs, parseOutputs, reviver } from "./parsing.mjs";
3
3
 
4
4
  export class Executor {
5
5
  constructor(props) {
@@ -123,118 +123,3 @@ export class Executor {
123
123
  });
124
124
  }
125
125
  }
126
-
127
- async function parseInputs(inputs) {
128
- if (inputs != null && typeof inputs === "object") {
129
- for (const keys of Object.keys(inputs)) {
130
- if (inputs[keys] !== null && typeof inputs[keys] === "object") {
131
- if (isDuration(inputs[keys])) {
132
- inputs[keys] = inputs[keys].toISOString();
133
- } else if (isInlineFileOrFile(inputs[keys])) {
134
- const contents = await inputs[keys].read();
135
- inputs[keys] = `data:${inputs[keys].contentType};name=${
136
- inputs[keys].filename
137
- };base64,${contents.toString("base64")}`;
138
- } else {
139
- inputs[keys] = await parseInputs(inputs[keys]);
140
- }
141
- }
142
- }
143
- }
144
- return inputs;
145
- }
146
-
147
- function isInlineFileOrFile(obj) {
148
- return (
149
- obj &&
150
- typeof obj === "object" &&
151
- (obj.constructor.name === "InlineFile" || obj.constructor.name === "File")
152
- );
153
- }
154
-
155
- function isDuration(obj) {
156
- return obj && typeof obj === "object" && obj.constructor.name === "Duration";
157
- }
158
-
159
- function parseOutputs(data) {
160
- if (!data) {
161
- return null;
162
- }
163
-
164
- if (!isPlainObject(data)) {
165
- return data;
166
- }
167
-
168
- const keys = data ? Object.keys(data) : [];
169
- const row = {};
170
-
171
- for (const key of keys) {
172
- const value = data[key];
173
-
174
- if (isPlainObject(value)) {
175
- if (value.key && value.size && value.filename && value.contentType) {
176
- row[key] = File.fromDbRecord(value);
177
- } else {
178
- row[key] = parseOutputs(value);
179
- }
180
- } else if (
181
- Array.isArray(value) &&
182
- value.every((item) => typeof item === "object" && item !== null)
183
- ) {
184
- const arr = [];
185
- for (let item of value) {
186
- if (item.key && item.size && item.filename && item.contentType) {
187
- arr.push(File.fromDbRecord(item));
188
- } else {
189
- arr.push(parseOutputs(item));
190
- }
191
- }
192
- row[key] = arr;
193
- } else {
194
- row[key] = value;
195
- }
196
- }
197
- return row;
198
- }
199
-
200
- function isPlainObject(obj) {
201
- return Object.prototype.toString.call(obj) === "[object Object]";
202
- }
203
-
204
- const dateFormat =
205
- /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
206
-
207
- function reviver(key, value) {
208
- // Handle date strings
209
- if (typeof value === "string") {
210
- if (dateFormat.test(value)) {
211
- return new Date(value);
212
- }
213
- }
214
-
215
- // Handle nested objects
216
- if (value !== null && typeof value === "object") {
217
- // Handle arrays
218
- if (Array.isArray(value)) {
219
- return value.map((item) => {
220
- if (typeof item === "string") {
221
- if (dateFormat.test(item)) {
222
- return new Date(item);
223
- }
224
- }
225
- return item;
226
- });
227
- }
228
-
229
- // Handle plain objects
230
- for (const k in value) {
231
- if (typeof value[k] === "string") {
232
- if (dateFormat.test(value[k])) {
233
- value[k] = new Date(value[k]);
234
- }
235
- }
236
- }
237
- }
238
-
239
- return value;
240
- }
@@ -0,0 +1,173 @@
1
+ import { parseInputs, parseOutputs, reviver } from "./parsing.mjs";
2
+
3
+ export class FlowExecutor {
4
+ constructor(props) {
5
+ this._flowUrl = process.env.KEEL_TESTING_FLOWS_API_URL + "/" + props.name;
6
+ this._name = props.name;
7
+ this._identity = props.identity || null;
8
+ this._authToken = props.authToken || null;
9
+ }
10
+
11
+ withIdentity(i) {
12
+ return new FlowExecutor({
13
+ name: this._name,
14
+ identity: i,
15
+ apiBaseUrl: this._apiBaseUrl,
16
+ parseJsonResult: this._parseJsonResult,
17
+ });
18
+ }
19
+ withAuthToken(t) {
20
+ return new FlowExecutor({
21
+ name: this._name,
22
+ authToken: t,
23
+ apiBaseUrl: this._apiBaseUrl,
24
+ parseJsonResult: this._parseJsonResult,
25
+ });
26
+ }
27
+
28
+ headers() {
29
+ const headers = { "Content-Type": "application/json" };
30
+
31
+ // An Identity instance is provided make a JWT
32
+ if (this._identity !== null) {
33
+ const base64pk = process.env.KEEL_DEFAULT_PK;
34
+ let privateKey = undefined;
35
+
36
+ if (base64pk) {
37
+ privateKey = Buffer.from(base64pk, "base64").toString("utf8");
38
+ }
39
+
40
+ headers["Authorization"] =
41
+ "Bearer " +
42
+ jwt.sign({}, privateKey, {
43
+ algorithm: privateKey ? "RS256" : "none",
44
+ expiresIn: 60 * 60 * 24,
45
+ subject: this._identity.id,
46
+ issuer: "https://keel.so",
47
+ });
48
+ }
49
+
50
+ // If an auth token is provided that can be sent as-is
51
+ if (this._authToken !== null) {
52
+ headers["Authorization"] = "Bearer " + this._authToken;
53
+ }
54
+
55
+ return headers;
56
+ }
57
+
58
+ async start(inputs) {
59
+ return parseInputs(inputs).then((parsed) => {
60
+ // Use the HTTP JSON API as that returns more friendly errors than
61
+ // the JSON-RPC API.
62
+ return fetch(this._flowUrl, {
63
+ method: "POST",
64
+ body: JSON.stringify(parsed),
65
+ headers: this.headers(),
66
+ }).then(handleResponse);
67
+ });
68
+ }
69
+
70
+ async get(id) {
71
+ return fetch(this._flowUrl + "/" + id, {
72
+ method: "GET",
73
+ headers: this.headers(),
74
+ }).then(handleResponse);
75
+ }
76
+
77
+ async cancel(id) {
78
+ return fetch(this._flowUrl + "/" + id + "/cancel", {
79
+ method: "POST",
80
+ headers: this.headers(),
81
+ }).then(handleResponse);
82
+ }
83
+
84
+ async putStepValues(id, stepId, values, action) {
85
+ let url = this._flowUrl + "/" + id + "/" + stepId;
86
+
87
+ if (action) {
88
+ // If an action is provided then we need to add it to the URL
89
+ const queryString = new URLSearchParams({ action }).toString();
90
+ url = `${url}?${queryString}`;
91
+ }
92
+
93
+ return await fetch(url, {
94
+ method: "PUT",
95
+ body: JSON.stringify(values),
96
+ headers: this.headers(),
97
+ }).then(handleResponse);
98
+ }
99
+
100
+ async untilFinished(id, timeout = 5000) {
101
+ const startTime = Date.now();
102
+
103
+ while (true) {
104
+ if (Date.now() - startTime > timeout) {
105
+ throw new Error(
106
+ `timed out waiting for flow run to reach a completed state after ${timeout}ms`
107
+ );
108
+ }
109
+
110
+ const flow = await this.get(id);
111
+
112
+ if (flow.status === "COMPLETED" || flow.status === "FAILED") {
113
+ return flow;
114
+ }
115
+
116
+ await new Promise((resolve) => setTimeout(resolve, 100));
117
+ }
118
+ }
119
+
120
+ async untilAwaitingInput(id, timeout = 5000) {
121
+ const startTime = Date.now();
122
+
123
+ while (true) {
124
+ if (Date.now() - startTime > timeout) {
125
+ throw new Error(
126
+ `timed out waiting for flow run to reach a completed state after ${timeout}ms`
127
+ );
128
+ }
129
+
130
+ const flow = await this.get(id);
131
+
132
+ if (flow.status === "AWAITING_INPUT") {
133
+ return flow;
134
+ }
135
+
136
+ await new Promise((resolve) => setTimeout(resolve, 100));
137
+ }
138
+ }
139
+ }
140
+
141
+ function handleResponse(r) {
142
+ if (r.status !== 200) {
143
+ // For non-200 first read the response as text
144
+ return r.text().then((t) => {
145
+ let d;
146
+ try {
147
+ d = JSON.parse(t);
148
+ } catch (e) {
149
+ if ("DEBUG" in process.env) {
150
+ console.log(e);
151
+ }
152
+ // If JSON parsing fails then throw an error with the
153
+ // response text as the message
154
+ throw new Error(t);
155
+ }
156
+ // Otherwise throw the parsed JSON error response
157
+ // We override toString as otherwise you get expect errors like:
158
+ // `expected to resolve but rejected with "[object Object]"`
159
+ Object.defineProperty(d, "toString", {
160
+ value: () => t,
161
+ enumerable: false,
162
+ });
163
+ throw d;
164
+ });
165
+ }
166
+
167
+ return r.text().then((t) => {
168
+ const response = JSON.parse(t, reviver);
169
+ response.input = parseOutputs(response.input);
170
+
171
+ return response;
172
+ });
173
+ }
package/src/index.d.ts CHANGED
@@ -17,3 +17,195 @@ declare module "vitest" {
17
17
  interface Assertion<T = any> extends CustomMatchers<T> {}
18
18
  interface AsymmetricMatchersContaining extends CustomMatchers {}
19
19
  }
20
+
21
+ // Flow Status Types
22
+ export type FlowStatus =
23
+ | "NEW"
24
+ | "RUNNING"
25
+ | "AWAITING_INPUT"
26
+ | "FAILED"
27
+ | "COMPLETED"
28
+ | "CANCELLED";
29
+
30
+ // Step Types
31
+ export type StepType = "FUNCTION" | "UI" | "COMPLETE";
32
+
33
+ // Step Status Types
34
+ export type StepStatus =
35
+ | "NEW"
36
+ | "PENDING"
37
+ | "FAILED"
38
+ | "COMPLETED"
39
+ | "CANCELLED";
40
+
41
+ // Stage Configuration
42
+ export interface FlowStage {
43
+ key: string;
44
+ name: string;
45
+ description: string;
46
+ }
47
+
48
+ // Flow Configuration
49
+ export interface FlowConfig {
50
+ title: string;
51
+ description?: string;
52
+ stages?: FlowStage[];
53
+ }
54
+
55
+ // UI Action Configuration
56
+ export interface UIAction {
57
+ label: string;
58
+ mode: "primary" | "secondary";
59
+ value: string;
60
+ }
61
+
62
+ // UI Input Elements
63
+ export interface UITextInput {
64
+ __type: "ui.input.text";
65
+ label: string;
66
+ name: string;
67
+ disabled?: boolean;
68
+ optional?: boolean;
69
+ defaultValue?: string;
70
+ placeholder?: string;
71
+ validationError?: string;
72
+ }
73
+
74
+ export interface UINumberInput {
75
+ __type: "ui.input.number";
76
+ label: string;
77
+ name: string;
78
+ disabled?: boolean;
79
+ optional?: boolean;
80
+ defaultValue?: number;
81
+ validationError?: string;
82
+ }
83
+
84
+ export interface UIBooleanInput {
85
+ __type: "ui.input.boolean";
86
+ label: string;
87
+ name: string;
88
+ disabled?: boolean;
89
+ optional?: boolean;
90
+ mode: "checkbox";
91
+ validationError?: string;
92
+ }
93
+
94
+ // UI Display Elements
95
+ export interface UIDivider {
96
+ __type: "ui.display.divider";
97
+ }
98
+
99
+ export interface UIMarkdown {
100
+ __type: "ui.display.markdown";
101
+ content: string;
102
+ }
103
+
104
+ export interface UIGrid {
105
+ __type: "ui.display.grid";
106
+ data: Array<{ title: string; [key: string]: any }>;
107
+ }
108
+
109
+ // UI Complete Element
110
+ export interface UIComplete {
111
+ __type: "ui.complete";
112
+ title: string;
113
+ description?: string;
114
+ stage?: string;
115
+ content: UIElement[];
116
+ }
117
+
118
+ // UI Page Element
119
+ export interface UIPage {
120
+ __type: "ui.page";
121
+ title: string;
122
+ description?: string;
123
+ content: UIElement[];
124
+ actions?: UIAction[];
125
+ hasValidationErrors: boolean;
126
+ validationError?: string;
127
+ }
128
+
129
+ // Union type for all UI elements
130
+ export type UIElement =
131
+ | UITextInput
132
+ | UINumberInput
133
+ | UIBooleanInput
134
+ | UIDivider
135
+ | UIMarkdown
136
+ | UIGrid;
137
+
138
+ // Union type for UI configurations
139
+ export type UIConfig = UIPage | UIComplete;
140
+
141
+ declare class FlowExecutor<Input = {}> {
142
+ withIdentity(identity: sdk.Identity): FlowExecutor<Input>;
143
+ withAuthToken(token: string): FlowExecutor<Input>;
144
+ start(inputs: Input): Promise<FlowRun<Input>>;
145
+ get(id: string): Promise<FlowRun<Input>>;
146
+ cancel(id: string): Promise<FlowRun<Input>>;
147
+ putStepValues(
148
+ id: string,
149
+ stepId: string,
150
+ values: Record<string, any>,
151
+ action?: string
152
+ ): Promise<FlowRun<Input>>;
153
+ untilAwaitingInput(id: string): Promise<FlowRun<Input>>;
154
+ untilFinished(id: string): Promise<FlowRun<Input>>;
155
+ }
156
+
157
+ // Step Definition
158
+ export interface FlowStep {
159
+ id: string;
160
+ name: string;
161
+ runId: string;
162
+ stage: string | null;
163
+ status: StepStatus;
164
+ type: StepType;
165
+ value: any;
166
+ error: string | null;
167
+ startTime: string | null;
168
+ endTime: string | null;
169
+ createdAt: string;
170
+ updatedAt: string;
171
+ ui: UIConfig | null;
172
+ }
173
+
174
+ // Flow Run Definition
175
+ export interface FlowRun<Input = {}> {
176
+ id: string;
177
+ traceId: string;
178
+ status: FlowStatus;
179
+ name: string;
180
+ startedBy: Date;
181
+ input: Input | {};
182
+ data: any;
183
+ steps: FlowStep[];
184
+ createdAt: Date;
185
+ updatedAt: Date;
186
+ config: FlowConfig;
187
+ }
188
+
189
+ // Step Values Request
190
+ export interface PutStepValuesRequest {
191
+ name: string;
192
+ runId: string;
193
+ stepId: string;
194
+ values: Record<string, any>;
195
+ token: string;
196
+ action?: string | null;
197
+ }
198
+
199
+ // Flow Start Request
200
+ export interface StartFlowRequest {
201
+ name: string;
202
+ token: string;
203
+ body: Record<string, any>;
204
+ }
205
+
206
+ // Flow Get Request
207
+ export interface GetFlowRequest {
208
+ name: string;
209
+ id: string;
210
+ token: string;
211
+ }
package/src/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  export { sql } from "kysely";
2
2
  export { ActionExecutor } from "./ActionExecutor.mjs";
3
+ export { FlowExecutor } from "./FlowExecutor.mjs";
3
4
  export { JobExecutor } from "./JobExecutor.mjs";
4
5
  export { SubscriberExecutor } from "./SubscriberExecutor.mjs";
5
6
  export { toHaveError } from "./toHaveError.mjs";
@@ -0,0 +1,116 @@
1
+ import { File } from "@teamkeel/functions-runtime";
2
+
3
+ export async function parseInputs(inputs) {
4
+ if (inputs != null && typeof inputs === "object") {
5
+ for (const keys of Object.keys(inputs)) {
6
+ if (inputs[keys] !== null && typeof inputs[keys] === "object") {
7
+ if (isDuration(inputs[keys])) {
8
+ inputs[keys] = inputs[keys].toISOString();
9
+ } else if (isInlineFileOrFile(inputs[keys])) {
10
+ const contents = await inputs[keys].read();
11
+ inputs[keys] = `data:${inputs[keys].contentType};name=${
12
+ inputs[keys].filename
13
+ };base64,${contents.toString("base64")}`;
14
+ } else {
15
+ inputs[keys] = await parseInputs(inputs[keys]);
16
+ }
17
+ }
18
+ }
19
+ }
20
+ return inputs;
21
+ }
22
+
23
+ function isInlineFileOrFile(obj) {
24
+ return (
25
+ obj &&
26
+ typeof obj === "object" &&
27
+ (obj.constructor.name === "InlineFile" || obj.constructor.name === "File")
28
+ );
29
+ }
30
+
31
+ function isDuration(obj) {
32
+ return obj && typeof obj === "object" && obj.constructor.name === "Duration";
33
+ }
34
+
35
+ export function parseOutputs(data) {
36
+ if (!data) {
37
+ return null;
38
+ }
39
+
40
+ if (!isPlainObject(data)) {
41
+ return data;
42
+ }
43
+
44
+ const keys = data ? Object.keys(data) : [];
45
+ const row = {};
46
+
47
+ for (const key of keys) {
48
+ const value = data[key];
49
+
50
+ if (isPlainObject(value)) {
51
+ if (value.key && value.size && value.filename && value.contentType) {
52
+ row[key] = File.fromDbRecord(value);
53
+ } else {
54
+ row[key] = parseOutputs(value);
55
+ }
56
+ } else if (
57
+ Array.isArray(value) &&
58
+ value.every((item) => typeof item === "object" && item !== null)
59
+ ) {
60
+ const arr = [];
61
+ for (let item of value) {
62
+ if (item.key && item.size && item.filename && item.contentType) {
63
+ arr.push(File.fromDbRecord(item));
64
+ } else {
65
+ arr.push(parseOutputs(item));
66
+ }
67
+ }
68
+ row[key] = arr;
69
+ } else {
70
+ row[key] = value;
71
+ }
72
+ }
73
+ return row;
74
+ }
75
+
76
+ function isPlainObject(obj) {
77
+ return Object.prototype.toString.call(obj) === "[object Object]";
78
+ }
79
+
80
+ const dateFormat =
81
+ /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
82
+
83
+ export function reviver(key, value) {
84
+ // Handle date strings
85
+ if (typeof value === "string") {
86
+ if (dateFormat.test(value)) {
87
+ return new Date(value);
88
+ }
89
+ }
90
+
91
+ // Handle nested objects
92
+ if (value !== null && typeof value === "object") {
93
+ // Handle arrays
94
+ if (Array.isArray(value)) {
95
+ return value.map((item) => {
96
+ if (typeof item === "string") {
97
+ if (dateFormat.test(item)) {
98
+ return new Date(item);
99
+ }
100
+ }
101
+ return item;
102
+ });
103
+ }
104
+
105
+ // Handle plain objects
106
+ for (const k in value) {
107
+ if (typeof value[k] === "string") {
108
+ if (dateFormat.test(value[k])) {
109
+ value[k] = new Date(value[k]);
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ return value;
116
+ }