@teamkeel/functions-runtime 0.317.0 → 0.318.1
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 +1 -1
- package/src/handleRequest.js +7 -6
- package/src/handleRequest.test.js +15 -16
- package/src/index.d.ts +5 -0
- package/src/tracing.js +33 -6
- package/src/tracing.test.js +62 -0
package/package.json
CHANGED
package/src/handleRequest.js
CHANGED
|
@@ -14,7 +14,7 @@ const { PROTO_ACTION_TYPES } = require("./consts");
|
|
|
14
14
|
|
|
15
15
|
const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
|
|
16
16
|
const opentelemetry = require("@opentelemetry/api");
|
|
17
|
-
const {
|
|
17
|
+
const { getTracer } = require("./tracing");
|
|
18
18
|
|
|
19
19
|
// Generic handler function that is agnostic to runtime environment (local or lambda)
|
|
20
20
|
// to execute a custom function based on the contents of a jsonrpc-2.0 payload object.
|
|
@@ -25,8 +25,7 @@ async function handleRequest(request, config) {
|
|
|
25
25
|
// "?." is so we don't have to provide this field on tests
|
|
26
26
|
request.meta?.tracing
|
|
27
27
|
);
|
|
28
|
-
|
|
29
|
-
let span = tracer.startSpan(
|
|
28
|
+
let span = getTracer().startSpan(
|
|
30
29
|
`Function/${request.method}`,
|
|
31
30
|
{ attributes: {} },
|
|
32
31
|
activeContext
|
|
@@ -68,17 +67,19 @@ async function handleRequest(request, config) {
|
|
|
68
67
|
// This is useful for permissions where we want to only proceed with database writes if all permission rules
|
|
69
68
|
// have been validated.
|
|
70
69
|
const result = await db.transaction().execute(async (transaction) => {
|
|
71
|
-
const ctx = createContextAPI(
|
|
70
|
+
const ctx = createContextAPI({
|
|
71
|
+
responseHeaders: headers,
|
|
72
|
+
meta: request.meta,
|
|
73
|
+
});
|
|
72
74
|
const api = createFunctionAPI({
|
|
73
75
|
meta: request.meta,
|
|
74
|
-
headers,
|
|
75
76
|
db: transaction,
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
const customFunction = functions[request.method];
|
|
79
80
|
|
|
80
81
|
// Call the user's custom function!
|
|
81
|
-
const fnResult = await customFunction(request.params, api
|
|
82
|
+
const fnResult = await customFunction(ctx, request.params, api);
|
|
82
83
|
|
|
83
84
|
// api.permissions maintains an internal state of whether the current operation has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
|
|
84
85
|
// we need to check that the final state is permitted or unpermitted. if it's not, then it means that the user has taken no explicit action to permit/deny
|
|
@@ -15,7 +15,7 @@ process.env.KEEL_DB_CONN = `postgresql://postgres:postgres@localhost:5432/functi
|
|
|
15
15
|
test("when the custom function returns expected value", async () => {
|
|
16
16
|
const config = {
|
|
17
17
|
functions: {
|
|
18
|
-
createPost: async (inputs, api
|
|
18
|
+
createPost: async (ctx, inputs, api) => {
|
|
19
19
|
api.permissions.allow();
|
|
20
20
|
return {
|
|
21
21
|
title: "a post",
|
|
@@ -26,7 +26,7 @@ test("when the custom function returns expected value", async () => {
|
|
|
26
26
|
actionTypes: {
|
|
27
27
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
28
28
|
},
|
|
29
|
-
createFunctionAPI: ({
|
|
29
|
+
createFunctionAPI: ({ db }) => {
|
|
30
30
|
return {
|
|
31
31
|
permissions: new Permissions(),
|
|
32
32
|
};
|
|
@@ -52,7 +52,7 @@ test("when the custom function returns expected value", async () => {
|
|
|
52
52
|
test("when the custom function doesnt return a value", async () => {
|
|
53
53
|
const config = {
|
|
54
54
|
functions: {
|
|
55
|
-
createPost: async (inputs, api
|
|
55
|
+
createPost: async (ctx, inputs, api) => {
|
|
56
56
|
api.permissions.allow();
|
|
57
57
|
},
|
|
58
58
|
},
|
|
@@ -60,7 +60,7 @@ test("when the custom function doesnt return a value", async () => {
|
|
|
60
60
|
actionTypes: {
|
|
61
61
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
62
62
|
},
|
|
63
|
-
createFunctionAPI: ({
|
|
63
|
+
createFunctionAPI: ({ db }) => {
|
|
64
64
|
return {
|
|
65
65
|
permissions: new Permissions(),
|
|
66
66
|
};
|
|
@@ -83,12 +83,12 @@ test("when the custom function doesnt return a value", async () => {
|
|
|
83
83
|
test("when there is no matching function for the path", async () => {
|
|
84
84
|
const config = {
|
|
85
85
|
functions: {
|
|
86
|
-
createPost: async (inputs, api
|
|
86
|
+
createPost: async (ctx, inputs, api) => {},
|
|
87
87
|
},
|
|
88
88
|
actionTypes: {
|
|
89
89
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
90
90
|
},
|
|
91
|
-
createFunctionAPI: ({
|
|
91
|
+
createFunctionAPI: ({ db }) => {
|
|
92
92
|
return {
|
|
93
93
|
permissions: new Permissions(),
|
|
94
94
|
};
|
|
@@ -111,7 +111,7 @@ test("when there is no matching function for the path", async () => {
|
|
|
111
111
|
test("when there is an unexpected error in the custom function", async () => {
|
|
112
112
|
const config = {
|
|
113
113
|
functions: {
|
|
114
|
-
createPost: async (inputs, api
|
|
114
|
+
createPost: async (ctx, inputs, api) => {
|
|
115
115
|
api.permissions.allow();
|
|
116
116
|
|
|
117
117
|
throw new Error("oopsie daisy");
|
|
@@ -120,7 +120,7 @@ test("when there is an unexpected error in the custom function", async () => {
|
|
|
120
120
|
actionTypes: {
|
|
121
121
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
122
122
|
},
|
|
123
|
-
createFunctionAPI: ({
|
|
123
|
+
createFunctionAPI: ({ db }) => {
|
|
124
124
|
return {
|
|
125
125
|
permissions: new Permissions(),
|
|
126
126
|
};
|
|
@@ -143,7 +143,7 @@ test("when there is an unexpected error in the custom function", async () => {
|
|
|
143
143
|
test("when a role based permission has already been granted by the main runtime", async () => {
|
|
144
144
|
const config = {
|
|
145
145
|
functions: {
|
|
146
|
-
createPost: async (inputs, api
|
|
146
|
+
createPost: async (ctx, inputs, api) => {
|
|
147
147
|
return {
|
|
148
148
|
title: inputs.title,
|
|
149
149
|
};
|
|
@@ -152,7 +152,7 @@ test("when a role based permission has already been granted by the main runtime"
|
|
|
152
152
|
actionTypes: {
|
|
153
153
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
154
154
|
},
|
|
155
|
-
createFunctionAPI: ({
|
|
155
|
+
createFunctionAPI: ({ db }) => {
|
|
156
156
|
return {
|
|
157
157
|
permissions: new Permissions({ status: "granted", reason: "role" }),
|
|
158
158
|
};
|
|
@@ -177,16 +177,15 @@ test("when a role based permission has already been granted by the main runtime"
|
|
|
177
177
|
test("when there is an unexpected object thrown in the custom function", async () => {
|
|
178
178
|
const config = {
|
|
179
179
|
functions: {
|
|
180
|
-
createPost: async (inputs, api
|
|
180
|
+
createPost: async (ctx, inputs, api) => {
|
|
181
181
|
api.permissions.allow();
|
|
182
|
-
|
|
183
182
|
throw { err: "oopsie daisy" };
|
|
184
183
|
},
|
|
185
184
|
},
|
|
186
185
|
actionTypes: {
|
|
187
186
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
188
187
|
},
|
|
189
|
-
createFunctionAPI: ({
|
|
188
|
+
createFunctionAPI: ({ db }) => {
|
|
190
189
|
return {
|
|
191
190
|
permissions: new Permissions(),
|
|
192
191
|
};
|
|
@@ -242,14 +241,14 @@ describe("ModelAPI error handling", () => {
|
|
|
242
241
|
functionConfig = {
|
|
243
242
|
permissions: {},
|
|
244
243
|
functions: {
|
|
245
|
-
createPost: async (inputs, api
|
|
244
|
+
createPost: async (ctx, inputs, api) => {
|
|
246
245
|
api.permissions.allow();
|
|
247
246
|
|
|
248
247
|
const post = await api.models.post.create(inputs);
|
|
249
248
|
|
|
250
249
|
return post;
|
|
251
250
|
},
|
|
252
|
-
deletePost: async (inputs, api
|
|
251
|
+
deletePost: async (ctx, inputs, api) => {
|
|
253
252
|
api.permissions.allow();
|
|
254
253
|
|
|
255
254
|
const deleted = await api.models.post.delete(inputs);
|
|
@@ -257,7 +256,7 @@ describe("ModelAPI error handling", () => {
|
|
|
257
256
|
return deleted;
|
|
258
257
|
},
|
|
259
258
|
},
|
|
260
|
-
createFunctionAPI: ({
|
|
259
|
+
createFunctionAPI: ({ db }) => ({
|
|
261
260
|
permissions: new Permissions(),
|
|
262
261
|
models: {
|
|
263
262
|
post: new ModelAPI(
|
package/src/index.d.ts
CHANGED
|
@@ -52,10 +52,15 @@ export type TimestampQueryInput = {
|
|
|
52
52
|
|
|
53
53
|
export type ContextAPI = {
|
|
54
54
|
headers: RequestHeaders;
|
|
55
|
+
response: Response;
|
|
55
56
|
isAuthenticated: boolean;
|
|
56
57
|
now(): Date;
|
|
57
58
|
};
|
|
58
59
|
|
|
60
|
+
export type Response = {
|
|
61
|
+
headers: Headers;
|
|
62
|
+
};
|
|
63
|
+
|
|
59
64
|
export type PageInfo = {
|
|
60
65
|
startCursor: string;
|
|
61
66
|
endCursor: string;
|
package/src/tracing.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
const opentelemetry = require("@opentelemetry/api");
|
|
2
2
|
|
|
3
|
-
const serviceName = "customerCustomFunctions";
|
|
4
|
-
|
|
5
3
|
function withSpan(name, fn) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return tracer.startActiveSpan(name, async (span) => {
|
|
4
|
+
return getTracer().startActiveSpan(name, async (span) => {
|
|
9
5
|
try {
|
|
10
6
|
// await the thing (this means we can use try/catch)
|
|
11
7
|
return await fn(span);
|
|
@@ -25,7 +21,38 @@ function withSpan(name, fn) {
|
|
|
25
21
|
});
|
|
26
22
|
}
|
|
27
23
|
|
|
24
|
+
function init() {
|
|
25
|
+
if (!globalThis.fetch.patched) {
|
|
26
|
+
const originalFetch = globalThis.fetch;
|
|
27
|
+
|
|
28
|
+
globalThis.fetch = async (...args) => {
|
|
29
|
+
return withSpan("fetch", async (span) => {
|
|
30
|
+
const url = new URL(
|
|
31
|
+
args[0] instanceof Request ? args[0].url : String(args[0])
|
|
32
|
+
);
|
|
33
|
+
span.setAttribute("http.url", url.toString());
|
|
34
|
+
const scheme = url.protocol.replace(":", "");
|
|
35
|
+
span.setAttribute("http.scheme", scheme);
|
|
36
|
+
|
|
37
|
+
const options = args[0] instanceof Request ? args[0] : args[1] || {};
|
|
38
|
+
const method = (options.method || "GET").toUpperCase();
|
|
39
|
+
span.setAttribute("http.method", method);
|
|
40
|
+
|
|
41
|
+
const res = await originalFetch(...args);
|
|
42
|
+
span.setAttribute("http.status", res.status);
|
|
43
|
+
span.setAttribute("http.status_text", res.statusText);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
globalThis.fetch.patched = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getTracer() {
|
|
51
|
+
return opentelemetry.trace.getTracer("functions");
|
|
52
|
+
}
|
|
53
|
+
|
|
28
54
|
module.exports = {
|
|
29
|
-
|
|
55
|
+
getTracer,
|
|
30
56
|
withSpan,
|
|
57
|
+
init,
|
|
31
58
|
};
|
package/src/tracing.test.js
CHANGED
|
@@ -21,6 +21,7 @@ provider.addSpanProcessor({
|
|
|
21
21
|
provider.register();
|
|
22
22
|
|
|
23
23
|
beforeEach(() => {
|
|
24
|
+
tracing.init();
|
|
24
25
|
spanEvents = [];
|
|
25
26
|
});
|
|
26
27
|
|
|
@@ -54,3 +55,64 @@ test("withSpan on error", async () => {
|
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
test("fetch - 200", async () => {
|
|
60
|
+
await fetch("http://example.com");
|
|
61
|
+
|
|
62
|
+
expect(spanEvents.map((e) => e.event)).toEqual(["onStart", "onEnd"]);
|
|
63
|
+
expect(spanEvents.pop().span.attributes).toEqual({
|
|
64
|
+
"http.url": "http://example.com/",
|
|
65
|
+
"http.scheme": "http",
|
|
66
|
+
"http.method": "GET",
|
|
67
|
+
"http.status": 200,
|
|
68
|
+
"http.status_text": "OK",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("fetch - 404", async () => {
|
|
73
|
+
await fetch("http://example.com/movies.json");
|
|
74
|
+
|
|
75
|
+
expect(spanEvents.map((e) => e.event)).toEqual(["onStart", "onEnd"]);
|
|
76
|
+
expect(spanEvents.pop().span.attributes).toEqual({
|
|
77
|
+
"http.url": "http://example.com/movies.json",
|
|
78
|
+
"http.scheme": "http",
|
|
79
|
+
"http.method": "GET",
|
|
80
|
+
"http.status": 404,
|
|
81
|
+
"http.status_text": "Not Found",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("fetch - invalid URL", async () => {
|
|
86
|
+
try {
|
|
87
|
+
await fetch({});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
expect(err.message).toEqual("Invalid URL");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(spanEvents.map((e) => e.event)).toEqual(["onStart", "onEnd"]);
|
|
93
|
+
|
|
94
|
+
const span = spanEvents.pop().span;
|
|
95
|
+
expect(spanEvents.pop().span.attributes).toEqual({});
|
|
96
|
+
expect(span.events[0].name).toEqual("exception");
|
|
97
|
+
expect.assertions(4);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("fetch - ENOTFOUND", async () => {
|
|
101
|
+
try {
|
|
102
|
+
await fetch("http://qpwoeuthnvksnvnsanrurvnc.com");
|
|
103
|
+
} catch (err) {
|
|
104
|
+
expect(err.message).toEqual("fetch failed");
|
|
105
|
+
expect(err.cause.code).toEqual("ENOTFOUND");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(spanEvents.map((e) => e.event)).toEqual(["onStart", "onEnd"]);
|
|
109
|
+
|
|
110
|
+
const span = spanEvents.pop().span;
|
|
111
|
+
expect(span.attributes).toEqual({
|
|
112
|
+
"http.method": "GET",
|
|
113
|
+
"http.scheme": "http",
|
|
114
|
+
"http.url": "http://qpwoeuthnvksnvnsanrurvnc.com/",
|
|
115
|
+
});
|
|
116
|
+
expect(span.events[0].name).toEqual("exception");
|
|
117
|
+
expect.assertions(5);
|
|
118
|
+
});
|