@teamkeel/functions-runtime 0.388.1 → 0.389.0-next.2

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.388.1",
3
+ "version": "0.389.0-next.2",
4
4
  "description": "Internal package used by @teamkeel/sdk",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,68 @@
1
+ class InlineFile {
2
+ constructor(filename, contentType, size, url) {
3
+ this.filename = filename;
4
+ this.contentType = contentType;
5
+ this.size = size;
6
+ this.url = url;
7
+ }
8
+
9
+ // Create an InlineFile instance from a given json object.
10
+ static fromObject(obj) {
11
+ if (obj.dataURL) {
12
+ var file = InlineFile.fromDataURL(obj.dataURL);
13
+ file._dataURL = obj.dataURL;
14
+ return file;
15
+ }
16
+
17
+ return new InlineFile(obj.filename, obj.contentType, obj.size, obj.url);
18
+ }
19
+
20
+ // Create an InlineFile instance from a given dataURL
21
+ static fromDataURL(dataURL) {
22
+ var info = dataURL.split(",")[0].split(":")[1];
23
+ var data = dataURL.split(",")[1];
24
+
25
+ var mime = info.split(";")[0];
26
+ var name = info.split(";")[1].split("=")[1];
27
+ var byteString = Buffer.from(data, "base64");
28
+ var blob = new Blob([byteString], { type: mime });
29
+
30
+ var file = new InlineFile(name, mime, blob.size);
31
+ file._dataURL = dataURL;
32
+ return file;
33
+ }
34
+
35
+ // Read the contents of the file. If URL is set, it will be read from the remote storage, otherwise, if dataURL is set
36
+ // on the instance, it will return a blob with the file contents
37
+ read() {
38
+ if (this.url) {
39
+ // TODO: read from store
40
+ }
41
+
42
+ if (this._dataURL) {
43
+ var data = this._dataURL.split(",")[1];
44
+ var byteString = Buffer.from(data, "base64");
45
+ return new Blob([byteString], { type: this.contentType });
46
+ }
47
+ }
48
+
49
+ store() {
50
+ //TODO: actually store and generate a key
51
+ this.key = uuidv4();
52
+ }
53
+
54
+ toJSON() {
55
+ return {
56
+ __typename: "InlineFile",
57
+ dataURL: this._dataURL,
58
+ filename: this.filename,
59
+ contentType: this.contentType,
60
+ size: this.size,
61
+ url: this.url,
62
+ };
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ InlineFile,
68
+ };
@@ -1,4 +1,5 @@
1
1
  import { test, expect, beforeEach, describe } from "vitest";
2
+ import { InlineFile } from "./InlineFile";
2
3
  const { ModelAPI } = require("./ModelAPI");
3
4
  const { sql } = require("kysely");
4
5
  const { useDatabase } = require("./database");
@@ -8,6 +9,8 @@ let personAPI;
8
9
  let postAPI;
9
10
  let authorAPI;
10
11
 
12
+ const imgDataURL = `data:image/png;name=my-avatar.png;base64,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`;
13
+
11
14
  beforeEach(async () => {
12
15
  const db = useDatabase();
13
16
 
@@ -21,6 +24,7 @@ beforeEach(async () => {
21
24
  name text UNIQUE,
22
25
  married boolean,
23
26
  favourite_number integer,
27
+ avatar jsonb,
24
28
  date timestamp
25
29
  );
26
30
  CREATE TABLE post(
@@ -61,15 +65,19 @@ beforeEach(async () => {
61
65
  });
62
66
 
63
67
  test("ModelAPI.create", async () => {
68
+ var file = InlineFile.fromDataURL(imgDataURL);
64
69
  const row = await personAPI.create({
65
70
  id: KSUID.randomSync().string,
66
71
  name: "Jim",
67
72
  married: false,
68
73
  favouriteNumber: 10,
74
+ avatar: file,
69
75
  });
70
76
  expect(row.name).toEqual("Jim");
71
77
  expect(row.married).toEqual(false);
72
78
  expect(row.favouriteNumber).toEqual(10);
79
+ expect(row.avatar.filename).toEqual("my-avatar.png");
80
+ expect(row.avatar.size).toEqual(2024);
73
81
  expect(KSUID.parse(row.id).string).toEqual(row.id);
74
82
  });
75
83
 
package/src/handleJob.js CHANGED
@@ -9,6 +9,7 @@ const opentelemetry = require("@opentelemetry/api");
9
9
  const { withSpan } = require("./tracing");
10
10
  const { PROTO_ACTION_TYPES } = require("./consts");
11
11
  const { tryExecuteJob } = require("./tryExecuteJob");
12
+ const { parseParams } = require("./parsing");
12
13
 
13
14
  // Generic handler function that is agnostic to runtime environment (local or lambda)
14
15
  // to execute a job function based on the contents of a jsonrpc-2.0 payload object.
@@ -57,8 +58,11 @@ async function handleJob(request, config) {
57
58
  await tryExecuteJob(
58
59
  { request, permitted, db, actionType },
59
60
  async () => {
61
+ // parse request params to convert objects into rich field types (e.g. InlineFile)
62
+ const inputs = parseParams(request.params);
63
+
60
64
  // Return the job function to the containing tryExecuteJob block
61
- return jobFunction(ctx, request.params);
65
+ return jobFunction(ctx, inputs);
62
66
  }
63
67
  );
64
68
 
@@ -8,6 +8,7 @@ const { tryExecuteFunction } = require("./tryExecuteFunction");
8
8
  const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
9
9
  const opentelemetry = require("@opentelemetry/api");
10
10
  const { withSpan } = require("./tracing");
11
+ const { parseParams } = require("./parsing");
11
12
 
12
13
  // Generic handler function that is agnostic to runtime environment (local or lambda)
13
14
  // to execute a custom function based on the contents of a jsonrpc-2.0 payload object.
@@ -64,24 +65,16 @@ async function handleRequest(request, config) {
64
65
  const result = await tryExecuteFunction(
65
66
  { request, ctx, permitted, db, permissionFns, actionType },
66
67
  async () => {
68
+ // parse request params to convert objects into rich field types (e.g. InlineFile)
69
+ const inputs = parseParams(request.params);
70
+
67
71
  // Return the custom function to the containing tryExecuteFunction block
68
72
  // Once the custom function is called, tryExecuteFunction will check the schema's permission rules to see if it can continue committing
69
73
  // the transaction to the db. If a permission rule is violated, any changes made inside the transaction are rolled back.
70
- return customFunction(ctx, request.params);
74
+ return customFunction(ctx, inputs);
71
75
  }
72
76
  );
73
77
 
74
- // Sometimes a custom function may be coded in such a way that nothing is returned from it.
75
- // We see this as an error so handle accordingly.
76
- if (result === undefined) {
77
- // no result returned from custom function
78
- return createJSONRPCErrorResponse(
79
- request.id,
80
- RuntimeErrors.NoResultError,
81
- `no result returned from function '${request.method}'`
82
- );
83
- }
84
-
85
78
  const response = createJSONRPCSuccessResponse(request.id, result);
86
79
 
87
80
  const responseHeaders = {};
@@ -47,38 +47,6 @@ test("when the custom function returns expected value", async () => {
47
47
  });
48
48
  });
49
49
 
50
- test("when the custom function doesnt return a value", async () => {
51
- const config = {
52
- functions: {
53
- createPost: async (ctx, inputs) => {
54
- new Permissions().allow();
55
- },
56
- },
57
- permissions: {},
58
- actionTypes: {
59
- createPost: PROTO_ACTION_TYPES.CREATE,
60
- },
61
- createContextAPI: () => {
62
- return {
63
- response: {
64
- headers: new Headers(),
65
- },
66
- };
67
- },
68
- };
69
-
70
- const rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
71
-
72
- expect(await handleRequest(rpcReq, config)).toEqual({
73
- id: "123",
74
- jsonrpc: "2.0",
75
- error: {
76
- code: RuntimeErrors.NoResultError,
77
- message: "no result returned from function 'createPost'",
78
- },
79
- });
80
- });
81
-
82
50
  test("when there is no matching function for the path", async () => {
83
51
  const config = {
84
52
  functions: {
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  checkBuiltInPermissions,
12
12
  } = require("./permissions");
13
13
  const tracing = require("./tracing");
14
+ const { InlineFile } = require("./InlineFile");
14
15
 
15
16
  module.exports = {
16
17
  ModelAPI,
@@ -19,6 +20,7 @@ module.exports = {
19
20
  handleJob,
20
21
  handleSubscriber,
21
22
  useDatabase,
23
+ InlineFile,
22
24
  Permissions,
23
25
  PERMISSION_STATE,
24
26
  checkBuiltInPermissions,
package/src/parsing.js ADDED
@@ -0,0 +1,28 @@
1
+ const { InlineFile } = require("./InlineFile");
2
+
3
+ // parseParams takes a set of inputs and creates objects for the ones that are of a complex type.
4
+ //
5
+ // inputs that are objects and contain a "__typename" field are resolved to instances of the complex type
6
+ // they represent. At the moment, the only supported type is `InlineFile`
7
+ function parseParams(inputs) {
8
+ if (inputs != null) {
9
+ Object.keys(inputs).forEach((i) => {
10
+ if (inputs[i] !== null && typeof inputs[i] === "object") {
11
+ if ("__typename" in inputs[i]) {
12
+ switch (inputs[i].__typename) {
13
+ case "InlineFile":
14
+ inputs[i] = InlineFile.fromObject(inputs[i]);
15
+ break;
16
+
17
+ default:
18
+ break;
19
+ }
20
+ }
21
+ }
22
+ });
23
+ }
24
+
25
+ return inputs;
26
+ }
27
+
28
+ module.exports.parseParams = parseParams;