@teamkeel/functions-runtime 0.317.0 → 0.318.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/functions-runtime",
3
- "version": "0.317.0",
3
+ "version": "0.318.0",
4
4
  "description": "Internal package used by @teamkeel/sdk",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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 { serviceName } = require("./tracing");
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
- const tracer = opentelemetry.trace.getTracer(serviceName);
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(request.meta);
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, ctx);
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, ctx) => {
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: ({ headers, db }) => {
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, ctx) => {
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: ({ headers, db }) => {
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, ctx) => {},
86
+ createPost: async (ctx, inputs, api) => {},
87
87
  },
88
88
  actionTypes: {
89
89
  createPost: PROTO_ACTION_TYPES.CREATE,
90
90
  },
91
- createFunctionAPI: ({ headers, db }) => {
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, ctx) => {
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: ({ headers, db }) => {
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, ctx) => {
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: ({ headers, db }) => {
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, ctx) => {
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: ({ headers, db }) => {
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, ctx) => {
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, ctx) => {
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: ({ headers, db }) => ({
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
- const tracer = opentelemetry.trace.getTracer(serviceName);
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
- serviceName,
55
+ getTracer,
30
56
  withSpan,
57
+ init,
31
58
  };
@@ -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
+ });