@teamkeel/functions-runtime 0.390.0 → 0.391.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.390.0",
3
+ "version": "0.391.0",
4
4
  "description": "Internal package used by @teamkeel/sdk",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -19,6 +19,8 @@
19
19
  "vitest": "^0.34.6"
20
20
  },
21
21
  "dependencies": {
22
+ "@aws-sdk/client-s3": "^3.617.0",
23
+ "@aws-sdk/credential-providers": "^3.617.0",
22
24
  "@neondatabase/serverless": "^0.9.3",
23
25
  "@opentelemetry/api": "^1.7.0",
24
26
  "@opentelemetry/exporter-trace-otlp-proto": "^0.46.0",
package/src/InlineFile.js CHANGED
@@ -1,9 +1,21 @@
1
+ const {
2
+ S3Client,
3
+ PutObjectCommand,
4
+ GetObjectCommand,
5
+ } = require("@aws-sdk/client-s3");
6
+ const { fromEnv } = require("@aws-sdk/credential-providers");
7
+ const { useDatabase } = require("./database");
8
+ const { DatabaseError } = require("./errors");
9
+ const KSUID = require("ksuid");
10
+
1
11
  class InlineFile {
2
- constructor(filename, contentType, size, url) {
12
+ constructor(filename, contentType, size, url, key, pub) {
3
13
  this.filename = filename;
4
14
  this.contentType = contentType;
5
15
  this.size = size;
6
16
  this.url = url;
17
+ this.key = key;
18
+ this.public = pub || false;
7
19
  }
8
20
 
9
21
  // Create an InlineFile instance from a given json object.
@@ -14,7 +26,14 @@ class InlineFile {
14
26
  return file;
15
27
  }
16
28
 
17
- return new InlineFile(obj.filename, obj.contentType, obj.size, obj.url);
29
+ return new InlineFile(
30
+ obj.filename,
31
+ obj.contentType,
32
+ obj.size,
33
+ obj.url,
34
+ obj.key,
35
+ obj.public
36
+ );
18
37
  }
19
38
 
20
39
  // Create an InlineFile instance from a given dataURL
@@ -34,21 +53,112 @@ class InlineFile {
34
53
 
35
54
  // Read the contents of the file. If URL is set, it will be read from the remote storage, otherwise, if dataURL is set
36
55
  // 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
-
56
+ async read() {
42
57
  if (this._dataURL) {
43
58
  var data = this._dataURL.split(",")[1];
44
- var byteString = Buffer.from(data, "base64");
45
- return new Blob([byteString], { type: this.contentType });
59
+ return Buffer.from(data, "base64");
60
+ }
61
+
62
+ // if we don't have a key nor a dataURL, this inline file has no data
63
+ if (!this.key) {
64
+ throw new Error("invalid file data");
65
+ }
66
+
67
+ if (isS3Storage()) {
68
+ const s3Client = new S3Client({
69
+ credentials: fromEnv(),
70
+ region: process.env.KEEL_REGION,
71
+ });
72
+
73
+ const params = {
74
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
75
+ Key: "files/" + this.key,
76
+ };
77
+ const command = new GetObjectCommand(params);
78
+ const response = await s3Client.send(command);
79
+ const blob = response.Body.transformToByteArray();
80
+ return Buffer.from(blob);
81
+ }
82
+
83
+ // default to db storage
84
+ const db = useDatabase();
85
+
86
+ try {
87
+ let query = db
88
+ .selectFrom("keel_storage")
89
+ .select("data")
90
+ .where("id", "=", this.key);
91
+
92
+ const row = await query.executeTakeFirstOrThrow();
93
+ return row.data;
94
+ } catch (e) {
95
+ throw new DatabaseError(e);
46
96
  }
47
97
  }
48
98
 
49
- store() {
50
- //TODO: actually store and generate a key
51
- this.key = uuidv4();
99
+ async store(expires = null) {
100
+ const content = await this.read();
101
+ this.key = KSUID.randomSync().string;
102
+
103
+ if (isS3Storage()) {
104
+ const s3Client = new S3Client({
105
+ credentials: fromEnv(),
106
+ region: process.env.KEEL_REGION,
107
+ });
108
+
109
+ const params = {
110
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
111
+ Key: "files/" + this.key,
112
+ Body: content,
113
+ ContentType: this.contentType,
114
+ Metadata: {
115
+ filename: this.filename,
116
+ },
117
+ ACL: this.public ? "public-read" : "private",
118
+ };
119
+
120
+ if (expires) {
121
+ params.Expires = expires;
122
+ }
123
+
124
+ const command = new PutObjectCommand(params);
125
+ try {
126
+ await s3Client.send(command);
127
+
128
+ return {
129
+ key: this.key,
130
+ size: this.size,
131
+ filename: this.filename,
132
+ contentType: this.contentType,
133
+ public: this.public,
134
+ };
135
+ } catch (error) {
136
+ console.error("Error uploading file:", error);
137
+ throw error;
138
+ }
139
+ }
140
+
141
+ // default to db storage
142
+ const db = useDatabase();
143
+
144
+ try {
145
+ let query = db.insertInto("keel_storage").values({
146
+ id: this.key,
147
+ filename: this.filename,
148
+ content_type: this.contentType,
149
+ data: content,
150
+ });
151
+
152
+ await query.returningAll().executeTakeFirstOrThrow();
153
+ return {
154
+ key: this.key,
155
+ size: this.size,
156
+ filename: this.filename,
157
+ contentType: this.contentType,
158
+ };
159
+ } catch (e) {
160
+ throw new DatabaseError(e);
161
+ }
52
162
  }
53
163
 
54
164
  toJSON() {
@@ -59,6 +169,7 @@ class InlineFile {
59
169
  contentType: this.contentType,
60
170
  size: this.size,
61
171
  url: this.url,
172
+ public: this.public,
62
173
  };
63
174
  }
64
175
  }
@@ -66,3 +177,7 @@ class InlineFile {
66
177
  module.exports = {
67
178
  InlineFile,
68
179
  };
180
+
181
+ function isS3Storage() {
182
+ return "KEEL_FILES_BUCKET_NAME" in process.env;
183
+ }
package/src/ModelAPI.js CHANGED
@@ -4,6 +4,7 @@ const { QueryBuilder } = require("./QueryBuilder");
4
4
  const { QueryContext } = require("./QueryContext");
5
5
  const { applyWhereConditions } = require("./applyWhereConditions");
6
6
  const { applyJoins } = require("./applyJoins");
7
+ const { InlineFile } = require("./InlineFile");
7
8
 
8
9
  const {
9
10
  applyLimit,
@@ -84,7 +85,7 @@ class ModelAPI {
84
85
  return null;
85
86
  }
86
87
 
87
- return camelCaseObject(row);
88
+ return transformRichDataTypes(camelCaseObject(row));
88
89
  });
89
90
  }
90
91
 
@@ -140,7 +141,7 @@ class ModelAPI {
140
141
 
141
142
  span.setAttribute("sql", query.compile().sql);
142
143
  const rows = await builder.execute();
143
- return rows.map((x) => camelCaseObject(x));
144
+ return rows.map((x) => transformRichDataTypes(camelCaseObject(x)));
144
145
  });
145
146
  }
146
147
 
@@ -151,7 +152,21 @@ class ModelAPI {
151
152
  return tracing.withSpan(name, async (span) => {
152
153
  let builder = db.updateTable(this._tableName).returningAll();
153
154
 
154
- builder = builder.set(snakeCaseObject(values));
155
+ // process input values
156
+ const keys = values ? Object.keys(values) : [];
157
+ const row = {};
158
+
159
+ for (const key of keys) {
160
+ const value = values[key];
161
+ // handle files that need uploading
162
+ if (value instanceof InlineFile) {
163
+ const dbValue = await value.store();
164
+ row[key] = dbValue;
165
+ } else {
166
+ row[key] = value;
167
+ }
168
+ }
169
+ builder = builder.set(snakeCaseObject(row));
155
170
 
156
171
  const context = new QueryContext([this._tableName], this._tableConfigMap);
157
172
 
@@ -226,7 +241,14 @@ async function create(conn, tableName, tableConfigs, values) {
226
241
  const columnConfig = tableConfig[key];
227
242
 
228
243
  if (!columnConfig) {
229
- row[key] = value;
244
+ // handle files that need uploading
245
+ if (value instanceof InlineFile) {
246
+ const dbValue = await value.store();
247
+ row[key] = dbValue;
248
+ } else {
249
+ row[key] = value;
250
+ }
251
+
230
252
  continue;
231
253
  }
232
254
 
@@ -305,6 +327,29 @@ async function create(conn, tableName, tableConfigs, values) {
305
327
  }
306
328
  }
307
329
 
330
+ // Iterate through the given object's keys and if any of the values are a rich data type, instantiate their respective class
331
+ function transformRichDataTypes(data) {
332
+ const keys = data ? Object.keys(data) : [];
333
+ const row = {};
334
+
335
+ for (const key of keys) {
336
+ const value = data[key];
337
+ if (isPlainObject(value)) {
338
+ // if we've got an InlineFile...
339
+ if (value.key && value.size && value.filename && value.contentType) {
340
+ row[key] = InlineFile.fromObject(value);
341
+ } else {
342
+ row[key] = value;
343
+ }
344
+ continue;
345
+ }
346
+
347
+ row[key] = value;
348
+ }
349
+
350
+ return row;
351
+ }
352
+
308
353
  function isPlainObject(obj) {
309
354
  return Object.prototype.toString.call(obj) === "[object Object]";
310
355
  }