@teamkeel/functions-runtime 0.390.0 → 0.391.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 +3 -1
- package/src/InlineFile.js +127 -12
- package/src/ModelAPI.js +49 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamkeel/functions-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.391.1",
|
|
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(
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
this.key =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|