@teamkeel/functions-runtime 0.459.0 → 0.460.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/dist/index.js CHANGED
@@ -1,10 +1,336 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
3
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, { get: all[name], enumerable: true });
6
10
  };
7
11
 
12
+ // src/File.ts
13
+ var File_exports = {};
14
+ __export(File_exports, {
15
+ File: () => File,
16
+ InlineFile: () => InlineFile,
17
+ buildContentDisposition: () => buildContentDisposition,
18
+ deleteStoredFile: () => deleteStoredFile,
19
+ rewriteFilesDomain: () => rewriteFilesDomain
20
+ });
21
+ import {
22
+ S3Client,
23
+ PutObjectCommand,
24
+ GetObjectCommand,
25
+ DeleteObjectCommand
26
+ } from "@aws-sdk/client-s3";
27
+ import { fromEnv } from "@aws-sdk/credential-providers";
28
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
29
+ import KSUID from "ksuid";
30
+ function rewriteFilesDomain(url) {
31
+ const domain = process.env.KEEL_FILES_DOMAIN;
32
+ if (domain) {
33
+ const override = new URL(domain);
34
+ url.protocol = override.protocol;
35
+ url.hostname = override.hostname;
36
+ url.port = override.port;
37
+ const overridePath = override.pathname.replace(/\/+$/, "");
38
+ if (overridePath && overridePath !== "/") {
39
+ url.pathname = overridePath + url.pathname;
40
+ }
41
+ }
42
+ return url;
43
+ }
44
+ function encodeRFC5987(value) {
45
+ return encodeURIComponent(value).replace(
46
+ /['()*]/g,
47
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
48
+ );
49
+ }
50
+ function buildContentDisposition(disposition, filename) {
51
+ const type = disposition === "attachment" ? "attachment" : "inline";
52
+ if (!filename || filename.trim() === "") {
53
+ return type;
54
+ }
55
+ const asciiFallback = filename.replace(/[^\x20-\x7e]|["\\]/g, "_");
56
+ return `${type}; filename="${asciiFallback}"; filename*=UTF-8''${encodeRFC5987(
57
+ filename
58
+ )}`;
59
+ }
60
+ async function storeFile(contents, key, filename, contentType, size, expires) {
61
+ if (!s3Client) {
62
+ throw new Error("S3 client is required");
63
+ }
64
+ const params = {
65
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
66
+ Key: "files/" + key,
67
+ Body: contents,
68
+ ContentType: contentType,
69
+ ContentDisposition: `attachment; filename="${encodeURIComponent(
70
+ filename
71
+ )}"`,
72
+ Metadata: {
73
+ filename
74
+ },
75
+ ACL: "private"
76
+ };
77
+ if (expires) {
78
+ if (expires instanceof Date) {
79
+ params.Expires = expires;
80
+ } else {
81
+ console.warn("Invalid expires value. Skipping Expires parameter.");
82
+ }
83
+ }
84
+ const command = new PutObjectCommand(params);
85
+ try {
86
+ await s3Client.send(command);
87
+ } catch (error) {
88
+ console.error("Error uploading file:", error);
89
+ throw error;
90
+ }
91
+ }
92
+ async function deleteStoredFile(key) {
93
+ if (!s3Client) {
94
+ throw new Error("S3 client is required");
95
+ }
96
+ await s3Client.send(
97
+ new DeleteObjectCommand({
98
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
99
+ Key: "files/" + key
100
+ })
101
+ );
102
+ }
103
+ var s3Client, InlineFile, File;
104
+ var init_File = __esm({
105
+ "src/File.ts"() {
106
+ "use strict";
107
+ s3Client = (() => {
108
+ if (!process.env.KEEL_FILES_BUCKET_NAME) {
109
+ return null;
110
+ }
111
+ const endpoint = process.env.KEEL_S3_ENDPOINT;
112
+ if (endpoint) {
113
+ return new S3Client({
114
+ region: process.env.KEEL_REGION,
115
+ credentials: {
116
+ accessKeyId: "keelstorage",
117
+ secretAccessKey: "keelstorage"
118
+ },
119
+ endpoint
120
+ });
121
+ }
122
+ const testEndpoint = process.env.TEST_AWS_ENDPOINT;
123
+ if (testEndpoint) {
124
+ return new S3Client({
125
+ region: process.env.KEEL_REGION,
126
+ credentials: {
127
+ accessKeyId: "test",
128
+ secretAccessKey: "test"
129
+ },
130
+ endpointProvider: /* @__PURE__ */ __name(() => {
131
+ return {
132
+ url: new URL(testEndpoint)
133
+ };
134
+ }, "endpointProvider")
135
+ });
136
+ }
137
+ return new S3Client({
138
+ region: process.env.KEEL_REGION,
139
+ credentials: fromEnv()
140
+ });
141
+ })();
142
+ __name(rewriteFilesDomain, "rewriteFilesDomain");
143
+ __name(encodeRFC5987, "encodeRFC5987");
144
+ __name(buildContentDisposition, "buildContentDisposition");
145
+ InlineFile = class _InlineFile {
146
+ static {
147
+ __name(this, "InlineFile");
148
+ }
149
+ constructor(input) {
150
+ this._filename = input.filename;
151
+ this._contentType = input.contentType;
152
+ this._contents = null;
153
+ }
154
+ static fromDataURL(dataURL) {
155
+ const info = dataURL.split(",")[0].split(":")[1];
156
+ const data = dataURL.split(",")[1];
157
+ const mimeType = info.split(";")[0];
158
+ const name = info.split(";")[1].split("=")[1] || "file";
159
+ const buffer = Buffer.from(data, "base64");
160
+ const file2 = new _InlineFile({ filename: name, contentType: mimeType });
161
+ file2.write(buffer);
162
+ return file2;
163
+ }
164
+ // Gets size of the file's contents in bytes
165
+ get size() {
166
+ if (this._contents) {
167
+ return this._contents.size;
168
+ }
169
+ return 0;
170
+ }
171
+ // Gets the media type of the file contents
172
+ get contentType() {
173
+ return this._contentType;
174
+ }
175
+ // Gets the name of the file
176
+ get filename() {
177
+ return this._filename;
178
+ }
179
+ // Write the files contents from a buffer
180
+ write(buffer) {
181
+ this._contents = new Blob([
182
+ new Uint8Array(
183
+ buffer.buffer,
184
+ buffer.byteOffset,
185
+ buffer.byteLength
186
+ )
187
+ ]);
188
+ }
189
+ // Reads the contents of the file as a buffer
190
+ async read() {
191
+ if (!this._contents) {
192
+ throw new Error("No contents to read");
193
+ }
194
+ const arrayBuffer = await this._contents.arrayBuffer();
195
+ return Buffer.from(arrayBuffer);
196
+ }
197
+ // Persists the file
198
+ async store(expires = null) {
199
+ const content = await this.read();
200
+ const key = KSUID.randomSync().string;
201
+ await storeFile(
202
+ content,
203
+ key,
204
+ this._filename,
205
+ this._contentType,
206
+ this.size,
207
+ expires
208
+ );
209
+ return new File({
210
+ key,
211
+ size: this.size,
212
+ filename: this.filename,
213
+ contentType: this.contentType
214
+ });
215
+ }
216
+ };
217
+ File = class _File extends InlineFile {
218
+ static {
219
+ __name(this, "File");
220
+ }
221
+ constructor(input) {
222
+ super({
223
+ filename: input.filename || "",
224
+ contentType: input.contentType || ""
225
+ });
226
+ this._key = input.key || "";
227
+ this._size = input.size || 0;
228
+ }
229
+ // Creates a new instance from the database record
230
+ static fromDbRecord(input) {
231
+ return new _File({
232
+ key: input.key,
233
+ filename: input.filename,
234
+ size: input.size,
235
+ contentType: input.contentType
236
+ });
237
+ }
238
+ get size() {
239
+ return this._size;
240
+ }
241
+ // Gets the stored key
242
+ get key() {
243
+ return this._key;
244
+ }
245
+ get isPublic() {
246
+ return false;
247
+ }
248
+ async read() {
249
+ if (this._contents) {
250
+ const arrayBuffer = await this._contents.arrayBuffer();
251
+ return Buffer.from(arrayBuffer);
252
+ }
253
+ if (!s3Client) {
254
+ throw new Error("S3 client is required");
255
+ }
256
+ const params = {
257
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
258
+ Key: "files/" + this.key
259
+ };
260
+ const command = new GetObjectCommand(params);
261
+ const response = await s3Client.send(command);
262
+ const blob = await response.Body.transformToByteArray();
263
+ return Buffer.from(blob);
264
+ }
265
+ async store(expires = null) {
266
+ if (this._contents) {
267
+ const contents = await this.read();
268
+ await storeFile(
269
+ contents,
270
+ this.key,
271
+ this.filename,
272
+ this.contentType,
273
+ this.size,
274
+ expires
275
+ );
276
+ }
277
+ return this;
278
+ }
279
+ // Generates a presigned download URL.
280
+ //
281
+ // By default the browser previews the file inline and, when saved, uses the
282
+ // file's own filename. Pass `contentDisposition: "attachment"` to force a
283
+ // download, or `filename` to override the suggested name.
284
+ async getPresignedUrl(options) {
285
+ if (!s3Client) {
286
+ throw new Error("S3 client is required");
287
+ }
288
+ const disposition = options?.contentDisposition ?? "inline";
289
+ const filename = options?.filename ?? this.filename;
290
+ const command = new GetObjectCommand({
291
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
292
+ Key: "files/" + this.key,
293
+ ResponseContentDisposition: buildContentDisposition(
294
+ disposition,
295
+ filename
296
+ )
297
+ });
298
+ const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
299
+ return rewriteFilesDomain(new URL(url));
300
+ }
301
+ // Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
302
+ async getPresignedUploadUrl() {
303
+ if (!s3Client) {
304
+ throw new Error("S3 client is required");
305
+ }
306
+ if (!this.key) {
307
+ this._key = KSUID.randomSync().string;
308
+ }
309
+ const command = new PutObjectCommand({
310
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
311
+ Key: "files/" + this.key
312
+ });
313
+ const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
314
+ return rewriteFilesDomain(new URL(url));
315
+ }
316
+ // Persists the file
317
+ toDbRecord() {
318
+ return {
319
+ key: this.key,
320
+ filename: this.filename,
321
+ contentType: this.contentType,
322
+ size: this.size
323
+ };
324
+ }
325
+ toJSON() {
326
+ return this.toDbRecord();
327
+ }
328
+ };
329
+ __name(storeFile, "storeFile");
330
+ __name(deleteStoredFile, "deleteStoredFile");
331
+ }
332
+ });
333
+
8
334
  // src/ModelAPI.js
9
335
  import { sql as sql3 } from "kysely";
10
336
 
@@ -72,6 +398,9 @@ var AuditContextPlugin = class {
72
398
  const rawNode = sql`set_trace_id(${audit.traceId})`.as(this.traceIdAlias).toOperationNode();
73
399
  returning.selections.push(SelectionNode.create(rawNode));
74
400
  }
401
+ if (returning.selections.length === 0) {
402
+ return { ...args.node };
403
+ }
75
404
  return {
76
405
  ...args.node,
77
406
  returning
@@ -441,20 +770,44 @@ var KEEL_INTERNAL_CHILDREN = "includeChildrenSpans";
441
770
  import WebSocket from "ws";
442
771
  import { readFileSync } from "fs";
443
772
  var dbInstance = new AsyncLocalStorage2();
773
+ var fileCleanupStore = new AsyncLocalStorage2();
774
+ function deferFileDeletion(key) {
775
+ fileCleanupStore.getStore()?.add(key);
776
+ }
777
+ __name(deferFileDeletion, "deferFileDeletion");
778
+ async function flushFileDeletions(keys) {
779
+ if (keys.size === 0) {
780
+ return;
781
+ }
782
+ const { deleteStoredFile: deleteStoredFile2 } = await Promise.resolve().then(() => (init_File(), File_exports));
783
+ for (const key of keys) {
784
+ try {
785
+ await deleteStoredFile2(key);
786
+ } catch (e) {
787
+ console.error("failed to delete orphaned file from storage", e);
788
+ }
789
+ }
790
+ }
791
+ __name(flushFileDeletions, "flushFileDeletions");
444
792
  var vitestDb = null;
445
793
  async function withDatabase(db, requiresTransaction, cb) {
794
+ const pending = /* @__PURE__ */ new Set();
446
795
  if (requiresTransaction) {
447
- return db.transaction().execute(async (transaction) => {
448
- return dbInstance.run(transaction, async () => {
449
- return cb({ transaction });
796
+ const result2 = await db.transaction().execute(async (transaction) => {
797
+ return dbInstance.run(transaction, () => {
798
+ return fileCleanupStore.run(pending, () => cb({ transaction }));
450
799
  });
451
800
  });
801
+ await flushFileDeletions(pending);
802
+ return result2;
452
803
  }
453
- return db.connection().execute(async (sDb) => {
454
- return dbInstance.run(sDb, async () => {
455
- return cb({ sDb });
804
+ const result = await db.connection().execute(async (sDb) => {
805
+ return dbInstance.run(sDb, () => {
806
+ return fileCleanupStore.run(pending, () => cb({ sDb }));
456
807
  });
457
808
  });
809
+ await flushFileDeletions(pending);
810
+ return result;
458
811
  }
459
812
  __name(withDatabase, "withDatabase");
460
813
  function useDatabase() {
@@ -629,275 +982,8 @@ function getDialect(connString) {
629
982
  }
630
983
  __name(getDialect, "getDialect");
631
984
 
632
- // src/File.ts
633
- import {
634
- S3Client,
635
- PutObjectCommand,
636
- GetObjectCommand
637
- } from "@aws-sdk/client-s3";
638
- import { fromEnv } from "@aws-sdk/credential-providers";
639
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
640
- import KSUID from "ksuid";
641
- var s3Client = (() => {
642
- if (!process.env.KEEL_FILES_BUCKET_NAME) {
643
- return null;
644
- }
645
- const endpoint = process.env.KEEL_S3_ENDPOINT;
646
- if (endpoint) {
647
- return new S3Client({
648
- region: process.env.KEEL_REGION,
649
- credentials: {
650
- accessKeyId: "keelstorage",
651
- secretAccessKey: "keelstorage"
652
- },
653
- endpoint
654
- });
655
- }
656
- const testEndpoint = process.env.TEST_AWS_ENDPOINT;
657
- if (testEndpoint) {
658
- return new S3Client({
659
- region: process.env.KEEL_REGION,
660
- credentials: {
661
- accessKeyId: "test",
662
- secretAccessKey: "test"
663
- },
664
- endpointProvider: /* @__PURE__ */ __name(() => {
665
- return {
666
- url: new URL(testEndpoint)
667
- };
668
- }, "endpointProvider")
669
- });
670
- }
671
- return new S3Client({
672
- region: process.env.KEEL_REGION,
673
- credentials: fromEnv()
674
- });
675
- })();
676
- function rewriteFilesDomain(url) {
677
- const domain = process.env.KEEL_FILES_DOMAIN;
678
- if (domain) {
679
- const override = new URL(domain);
680
- url.protocol = override.protocol;
681
- url.hostname = override.hostname;
682
- url.port = override.port;
683
- const overridePath = override.pathname.replace(/\/+$/, "");
684
- if (overridePath && overridePath !== "/") {
685
- url.pathname = overridePath + url.pathname;
686
- }
687
- }
688
- return url;
689
- }
690
- __name(rewriteFilesDomain, "rewriteFilesDomain");
691
- var InlineFile = class _InlineFile {
692
- static {
693
- __name(this, "InlineFile");
694
- }
695
- constructor(input) {
696
- this._filename = input.filename;
697
- this._contentType = input.contentType;
698
- this._contents = null;
699
- }
700
- static fromDataURL(dataURL) {
701
- const info = dataURL.split(",")[0].split(":")[1];
702
- const data = dataURL.split(",")[1];
703
- const mimeType = info.split(";")[0];
704
- const name = info.split(";")[1].split("=")[1] || "file";
705
- const buffer = Buffer.from(data, "base64");
706
- const file2 = new _InlineFile({ filename: name, contentType: mimeType });
707
- file2.write(buffer);
708
- return file2;
709
- }
710
- // Gets size of the file's contents in bytes
711
- get size() {
712
- if (this._contents) {
713
- return this._contents.size;
714
- }
715
- return 0;
716
- }
717
- // Gets the media type of the file contents
718
- get contentType() {
719
- return this._contentType;
720
- }
721
- // Gets the name of the file
722
- get filename() {
723
- return this._filename;
724
- }
725
- // Write the files contents from a buffer
726
- write(buffer) {
727
- this._contents = new Blob([
728
- new Uint8Array(
729
- buffer.buffer,
730
- buffer.byteOffset,
731
- buffer.byteLength
732
- )
733
- ]);
734
- }
735
- // Reads the contents of the file as a buffer
736
- async read() {
737
- if (!this._contents) {
738
- throw new Error("No contents to read");
739
- }
740
- const arrayBuffer = await this._contents.arrayBuffer();
741
- return Buffer.from(arrayBuffer);
742
- }
743
- // Persists the file
744
- async store(expires = null) {
745
- const content = await this.read();
746
- const key = KSUID.randomSync().string;
747
- await storeFile(
748
- content,
749
- key,
750
- this._filename,
751
- this._contentType,
752
- this.size,
753
- expires
754
- );
755
- return new File({
756
- key,
757
- size: this.size,
758
- filename: this.filename,
759
- contentType: this.contentType
760
- });
761
- }
762
- };
763
- var File = class _File extends InlineFile {
764
- static {
765
- __name(this, "File");
766
- }
767
- constructor(input) {
768
- super({
769
- filename: input.filename || "",
770
- contentType: input.contentType || ""
771
- });
772
- this._key = input.key || "";
773
- this._size = input.size || 0;
774
- }
775
- // Creates a new instance from the database record
776
- static fromDbRecord(input) {
777
- return new _File({
778
- key: input.key,
779
- filename: input.filename,
780
- size: input.size,
781
- contentType: input.contentType
782
- });
783
- }
784
- get size() {
785
- return this._size;
786
- }
787
- // Gets the stored key
788
- get key() {
789
- return this._key;
790
- }
791
- get isPublic() {
792
- return false;
793
- }
794
- async read() {
795
- if (this._contents) {
796
- const arrayBuffer = await this._contents.arrayBuffer();
797
- return Buffer.from(arrayBuffer);
798
- }
799
- if (!s3Client) {
800
- throw new Error("S3 client is required");
801
- }
802
- const params = {
803
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
804
- Key: "files/" + this.key
805
- };
806
- const command = new GetObjectCommand(params);
807
- const response = await s3Client.send(command);
808
- const blob = await response.Body.transformToByteArray();
809
- return Buffer.from(blob);
810
- }
811
- async store(expires = null) {
812
- if (this._contents) {
813
- const contents = await this.read();
814
- await storeFile(
815
- contents,
816
- this.key,
817
- this.filename,
818
- this.contentType,
819
- this.size,
820
- expires
821
- );
822
- }
823
- return this;
824
- }
825
- // Generates a presigned download URL
826
- async getPresignedUrl() {
827
- if (!s3Client) {
828
- throw new Error("S3 client is required");
829
- }
830
- const command = new GetObjectCommand({
831
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
832
- Key: "files/" + this.key,
833
- ResponseContentDisposition: "inline"
834
- });
835
- const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
836
- return rewriteFilesDomain(new URL(url));
837
- }
838
- // Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
839
- async getPresignedUploadUrl() {
840
- if (!s3Client) {
841
- throw new Error("S3 client is required");
842
- }
843
- if (!this.key) {
844
- this._key = KSUID.randomSync().string;
845
- }
846
- const command = new PutObjectCommand({
847
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
848
- Key: "files/" + this.key
849
- });
850
- const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
851
- return rewriteFilesDomain(new URL(url));
852
- }
853
- // Persists the file
854
- toDbRecord() {
855
- return {
856
- key: this.key,
857
- filename: this.filename,
858
- contentType: this.contentType,
859
- size: this.size
860
- };
861
- }
862
- toJSON() {
863
- return this.toDbRecord();
864
- }
865
- };
866
- async function storeFile(contents, key, filename, contentType, size, expires) {
867
- if (!s3Client) {
868
- throw new Error("S3 client is required");
869
- }
870
- const params = {
871
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
872
- Key: "files/" + key,
873
- Body: contents,
874
- ContentType: contentType,
875
- ContentDisposition: `attachment; filename="${encodeURIComponent(
876
- filename
877
- )}"`,
878
- Metadata: {
879
- filename
880
- },
881
- ACL: "private"
882
- };
883
- if (expires) {
884
- if (expires instanceof Date) {
885
- params.Expires = expires;
886
- } else {
887
- console.warn("Invalid expires value. Skipping Expires parameter.");
888
- }
889
- }
890
- const command = new PutObjectCommand(params);
891
- try {
892
- await s3Client.send(command);
893
- } catch (error) {
894
- console.error("Error uploading file:", error);
895
- throw error;
896
- }
897
- }
898
- __name(storeFile, "storeFile");
899
-
900
985
  // src/parsing.js
986
+ init_File();
901
987
  var dateFormat = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
902
988
  function parseInputs(inputs) {
903
989
  if (inputs != null && typeof inputs === "object") {
@@ -1833,6 +1919,7 @@ var QueryBuilder = class _QueryBuilder {
1833
1919
  };
1834
1920
 
1835
1921
  // src/ModelAPI.js
1922
+ init_File();
1836
1923
  var ModelAPI = class {
1837
1924
  static {
1838
1925
  __name(this, "ModelAPI");
@@ -1842,9 +1929,10 @@ var ModelAPI = class {
1842
1929
  * @param {Function} _ Used to be a function that returns the default values for a row in this table. No longer used.
1843
1930
  * @param {TableConfigMap} tableConfigMap
1844
1931
  */
1845
- constructor(tableName, _, tableConfigMap = {}) {
1932
+ constructor(tableName, _, tableConfigMap = {}, fileFieldsMap = {}) {
1846
1933
  this._tableName = tableName;
1847
1934
  this._tableConfigMap = tableConfigMap;
1935
+ this._fileFields = fileFieldsMap[tableName] || {};
1848
1936
  this._modelName = upperCamelCase(this._tableName);
1849
1937
  }
1850
1938
  async create(values) {
@@ -1914,6 +2002,10 @@ var ModelAPI = class {
1914
2002
  const name = spanNameForModelAPI(this._modelName, "update");
1915
2003
  const db = useDatabase();
1916
2004
  return withSpan(name, async (span) => {
2005
+ const fileColumns = Object.keys(values || {}).filter(
2006
+ (k) => k in this._fileFields
2007
+ );
2008
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
1917
2009
  let builder = db.updateTable(this._tableName).returningAll();
1918
2010
  const keys = values ? Object.keys(values) : [];
1919
2011
  const row = {};
@@ -1952,7 +2044,9 @@ var ModelAPI = class {
1952
2044
  span.setAttribute("sql", builder.compile().sql);
1953
2045
  try {
1954
2046
  const row2 = await builder.executeTakeFirstOrThrow();
1955
- return transformRichDataTypes(camelCaseObject(row2));
2047
+ const result = transformRichDataTypes(camelCaseObject(row2));
2048
+ this._deferReplacedFiles(existingFileRows, fileColumns, result);
2049
+ return result;
1956
2050
  } catch (e) {
1957
2051
  throw new DatabaseError(e);
1958
2052
  }
@@ -1962,12 +2056,15 @@ var ModelAPI = class {
1962
2056
  const name = spanNameForModelAPI(this._modelName, "delete");
1963
2057
  const db = useDatabase();
1964
2058
  return withSpan(name, async (span) => {
2059
+ const fileColumns = Object.keys(this._fileFields);
2060
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
1965
2061
  let builder = db.deleteFrom(this._tableName).returning(["id"]);
1966
2062
  const context7 = new QueryContext([this._tableName], this._tableConfigMap);
1967
2063
  builder = applyWhereConditions(context7, builder, where);
1968
2064
  span.setAttribute("sql", builder.compile().sql);
1969
2065
  try {
1970
2066
  const row = await builder.executeTakeFirstOrThrow();
2067
+ this._deferReplacedFiles(existingFileRows, fileColumns, null);
1971
2068
  return row.id;
1972
2069
  } catch (e) {
1973
2070
  throw new DatabaseError(e);
@@ -1982,6 +2079,30 @@ var ModelAPI = class {
1982
2079
  builder = applyWhereConditions(context7, builder, where);
1983
2080
  return new QueryBuilder(this._tableName, context7, builder);
1984
2081
  }
2082
+ // Reads the current file-column values for rows matched by `where`.
2083
+ async _selectExistingFileValues(where, fileColumns) {
2084
+ if (fileColumns.length === 0) {
2085
+ return [];
2086
+ }
2087
+ const db = useDatabase();
2088
+ let builder = db.selectFrom(this._tableName).selectAll(this._tableName);
2089
+ const context7 = new QueryContext([this._tableName], this._tableConfigMap);
2090
+ builder = applyWhereConditions(context7, builder, where);
2091
+ const rows = await builder.execute();
2092
+ return rows.map((x) => transformRichDataTypes(camelCaseObject(x)));
2093
+ }
2094
+ // Defers deletion of every old file key in existingRows that is not still
2095
+ // referenced by newRow.
2096
+ _deferReplacedFiles(existingRows, fileColumns, newRow) {
2097
+ const retained = new Set(
2098
+ collectFileKeys(newRow ? [newRow] : [], fileColumns)
2099
+ );
2100
+ for (const key of collectFileKeys(existingRows, fileColumns)) {
2101
+ if (!retained.has(key)) {
2102
+ deferFileDeletion(key);
2103
+ }
2104
+ }
2105
+ }
1985
2106
  };
1986
2107
  async function create(conn, tableName, tableConfigs, values) {
1987
2108
  try {
@@ -2091,6 +2212,23 @@ async function create(conn, tableName, tableConfigs, values) {
2091
2212
  }
2092
2213
  }
2093
2214
  __name(create, "create");
2215
+ function collectFileKeys(rows, fileColumns) {
2216
+ const keys = [];
2217
+ for (const row of rows || []) {
2218
+ if (!row) continue;
2219
+ for (const col of fileColumns) {
2220
+ const v = row[col];
2221
+ if (!v) continue;
2222
+ if (Array.isArray(v)) {
2223
+ for (const f of v) if (f?.key) keys.push(f.key);
2224
+ } else if (v.key) {
2225
+ keys.push(v.key);
2226
+ }
2227
+ }
2228
+ }
2229
+ return keys;
2230
+ }
2231
+ __name(collectFileKeys, "collectFileKeys");
2094
2232
 
2095
2233
  // src/TaskAPI.js
2096
2234
  import jwt from "jsonwebtoken";
@@ -2762,6 +2900,61 @@ var FlowsAPI = class _FlowsAPI {
2762
2900
  }
2763
2901
  };
2764
2902
 
2903
+ // src/integrationServer.js
2904
+ import jwt3 from "jsonwebtoken";
2905
+ function buildHeaders3(identity) {
2906
+ const headers = { "Content-Type": "application/json" };
2907
+ const base64pk = process.env.KEEL_PRIVATE_KEY;
2908
+ if (!base64pk) {
2909
+ throw new Error(
2910
+ "KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
2911
+ );
2912
+ }
2913
+ const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
2914
+ const subject = identity && identity.id ? identity.id : "integration-proxy";
2915
+ headers["Authorization"] = "Bearer " + jwt3.sign({}, privateKey, {
2916
+ algorithm: "RS256",
2917
+ expiresIn: 60 * 60 * 24,
2918
+ subject,
2919
+ // Scope the token to the integration proxy so it can't be mistaken for an ordinary user
2920
+ // access token; the runtime proxy verifies this audience before injecting credentials.
2921
+ audience: "integration-proxy",
2922
+ issuer: "https://keel.so"
2923
+ });
2924
+ return headers;
2925
+ }
2926
+ __name(buildHeaders3, "buildHeaders");
2927
+ function getApiUrl3() {
2928
+ const apiUrl = process.env.KEEL_API_URL;
2929
+ if (!apiUrl) {
2930
+ throw new Error("KEEL_API_URL environment variable is not set");
2931
+ }
2932
+ return apiUrl;
2933
+ }
2934
+ __name(getApiUrl3, "getApiUrl");
2935
+ function createIntegrationServer(name, identity) {
2936
+ return {
2937
+ do: /* @__PURE__ */ __name(async (request) => {
2938
+ const url = `${getApiUrl3()}/integrations/${encodeURIComponent(
2939
+ name
2940
+ )}/proxy`;
2941
+ const response = await fetch(url, {
2942
+ method: "POST",
2943
+ headers: buildHeaders3(identity ?? null),
2944
+ body: JSON.stringify(request ?? {})
2945
+ });
2946
+ if (!response.ok) {
2947
+ const text = await response.text();
2948
+ throw new Error(
2949
+ `integration "${name}" proxy request failed (${response.status}): ${text}`
2950
+ );
2951
+ }
2952
+ return await response.json();
2953
+ }, "do")
2954
+ };
2955
+ }
2956
+ __name(createIntegrationServer, "createIntegrationServer");
2957
+
2765
2958
  // src/RequestHeaders.ts
2766
2959
  var RequestHeaders = class {
2767
2960
  /**
@@ -3962,6 +4155,7 @@ var datePickerInput = /* @__PURE__ */ __name((name, options) => {
3962
4155
  }, "datePickerInput");
3963
4156
 
3964
4157
  // src/flows/ui/elements/input/file.ts
4158
+ init_File();
3965
4159
  var fileInput = /* @__PURE__ */ __name((name, options) => {
3966
4160
  return {
3967
4161
  __type: "input",
@@ -3987,6 +4181,7 @@ var fileInput = /* @__PURE__ */ __name((name, options) => {
3987
4181
  }, "fileInput");
3988
4182
 
3989
4183
  // src/flows/ui/elements/input/imageCapture.ts
4184
+ init_File();
3990
4185
  var isMultiOptions = /* @__PURE__ */ __name((opts) => opts && opts.mode === "multi", "isMultiOptions");
3991
4186
  function validateEntry(entry, requireCaption, context7) {
3992
4187
  if (!entry?.file?.key) {
@@ -4482,7 +4677,7 @@ var defaultOpts = {
4482
4677
  async function insertNewStep(db, runId, name, stage) {
4483
4678
  await db.transaction().execute(async (trx) => {
4484
4679
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
4485
- const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "=", "NEW" /* NEW */).executeTakeFirst();
4680
+ const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "in", ["NEW" /* NEW */, "RUNNING" /* RUNNING */]).executeTakeFirst();
4486
4681
  if (existing) {
4487
4682
  return;
4488
4683
  }
@@ -4505,6 +4700,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4505
4700
  now: ctx.now,
4506
4701
  secrets: ctx.secrets,
4507
4702
  trace: ctx.trace,
4703
+ module: ctx.module,
4508
4704
  complete: /* @__PURE__ */ __name((options) => {
4509
4705
  return {
4510
4706
  __type: "ui.complete",
@@ -4567,6 +4763,11 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4567
4763
  const raw = completedSteps[0].valueRaw;
4568
4764
  return raw == null ? void 0 : JSON.parse(raw);
4569
4765
  }
4766
+ if (runningSteps.length >= 1) {
4767
+ span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4768
+ span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4769
+ throw new StepCreatedDisrupt();
4770
+ }
4570
4771
  if (newSteps.length === 1) {
4571
4772
  let result = null;
4572
4773
  const claimed = await db.updateTable("keel.flow_step").set({
@@ -4622,11 +4823,6 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4622
4823
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4623
4824
  return result;
4624
4825
  }
4625
- if (runningSteps.length >= 1) {
4626
- span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4627
- span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4628
- throw new StepCreatedDisrupt();
4629
- }
4630
4826
  await insertNewStep(db, runId, name, options.stage);
4631
4827
  span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4632
4828
  span.setAttribute("step.status", "NEW" /* NEW */);
@@ -5006,7 +5202,183 @@ async function handleFlow(request, config) {
5006
5202
  __name(handleFlow, "handleFlow");
5007
5203
 
5008
5204
  // src/index.ts
5205
+ import KSUID4 from "ksuid";
5206
+ init_File();
5207
+
5208
+ // src/notifications/email-template.tsx
5209
+ import {
5210
+ Html,
5211
+ Head,
5212
+ Body,
5213
+ Container,
5214
+ Heading,
5215
+ Text,
5216
+ Button,
5217
+ Section
5218
+ } from "@react-email/components";
5219
+ import { render } from "@react-email/render";
5220
+ import { jsx, jsxs } from "react/jsx-runtime";
5221
+ function renderBody(text) {
5222
+ return text.split("\n").flatMap((line, i) => i === 0 ? [line] : [/* @__PURE__ */ jsx("br", {}, `br-${i}`), line]);
5223
+ }
5224
+ __name(renderBody, "renderBody");
5225
+ var StockEmailTemplate = /* @__PURE__ */ __name(({ content }) => /* @__PURE__ */ jsxs(Html, { children: [
5226
+ /* @__PURE__ */ jsx(Head, {}),
5227
+ /* @__PURE__ */ jsx(
5228
+ Body,
5229
+ {
5230
+ style: {
5231
+ backgroundColor: "#f6f6f6",
5232
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
5233
+ },
5234
+ children: /* @__PURE__ */ jsxs(
5235
+ Container,
5236
+ {
5237
+ style: {
5238
+ backgroundColor: "#ffffff",
5239
+ margin: "40px auto",
5240
+ padding: "40px",
5241
+ maxWidth: "560px",
5242
+ borderRadius: "8px"
5243
+ },
5244
+ children: [
5245
+ content.title && /* @__PURE__ */ jsx(
5246
+ Heading,
5247
+ {
5248
+ style: {
5249
+ fontSize: "22px",
5250
+ fontWeight: 600,
5251
+ color: "#111827",
5252
+ margin: "0 0 16px"
5253
+ },
5254
+ children: content.title
5255
+ }
5256
+ ),
5257
+ /* @__PURE__ */ jsx(
5258
+ Text,
5259
+ {
5260
+ style: {
5261
+ fontSize: "15px",
5262
+ lineHeight: "1.6",
5263
+ color: "#374151",
5264
+ margin: "0 0 24px"
5265
+ },
5266
+ children: renderBody(content.body)
5267
+ }
5268
+ ),
5269
+ (content.actions ?? []).length > 0 && /* @__PURE__ */ jsx(Section, { style: { margin: "0 0 12px" }, children: content.actions.map((action, i) => /* @__PURE__ */ jsx(
5270
+ Button,
5271
+ {
5272
+ href: action.url,
5273
+ style: {
5274
+ backgroundColor: "#111827",
5275
+ color: "#ffffff",
5276
+ padding: "12px 24px",
5277
+ borderRadius: "6px",
5278
+ fontSize: "14px",
5279
+ fontWeight: 600,
5280
+ textDecoration: "none",
5281
+ display: "inline-block",
5282
+ // Lay buttons out side by side; space between adjacent buttons.
5283
+ marginRight: i < content.actions.length - 1 ? "12px" : "0"
5284
+ },
5285
+ children: action.label
5286
+ },
5287
+ i
5288
+ )) })
5289
+ ]
5290
+ }
5291
+ )
5292
+ }
5293
+ )
5294
+ ] }), "StockEmailTemplate");
5295
+ async function renderEmailHtml(content) {
5296
+ return render(/* @__PURE__ */ jsx(StockEmailTemplate, { content }), { pretty: false });
5297
+ }
5298
+ __name(renderEmailHtml, "renderEmailHtml");
5299
+
5300
+ // src/notifications/notify.ts
5009
5301
  import KSUID3 from "ksuid";
5302
+ function getApiUrl4() {
5303
+ const apiUrl = process.env.KEEL_API_URL;
5304
+ if (!apiUrl) {
5305
+ throw new Error("KEEL_API_URL environment variable is not set");
5306
+ }
5307
+ return apiUrl;
5308
+ }
5309
+ __name(getApiUrl4, "getApiUrl");
5310
+ function toArray(v) {
5311
+ if (v === void 0) return [];
5312
+ return Array.isArray(v) ? v : [v];
5313
+ }
5314
+ __name(toArray, "toArray");
5315
+ function normaliseGroup(group) {
5316
+ if (!group) return [];
5317
+ const refs = [];
5318
+ for (const email of toArray(group.emails)) {
5319
+ refs.push({ kind: "email", value: email });
5320
+ }
5321
+ for (const user of toArray(group.users)) {
5322
+ refs.push({
5323
+ kind: "user",
5324
+ value: typeof user === "string" ? user : user.id
5325
+ });
5326
+ }
5327
+ for (const identity of toArray(group.identities)) {
5328
+ refs.push({
5329
+ kind: "identity",
5330
+ value: typeof identity === "string" ? identity : identity.id
5331
+ });
5332
+ }
5333
+ for (const team of toArray(group.teams)) {
5334
+ refs.push({ kind: "team", value: team });
5335
+ }
5336
+ return refs;
5337
+ }
5338
+ __name(normaliseGroup, "normaliseGroup");
5339
+ async function notifyEmail(input) {
5340
+ return withSpan("notify.email", async () => {
5341
+ const id = KSUID3.randomSync().string;
5342
+ let rendered;
5343
+ let content;
5344
+ if (typeof input.content === "string") {
5345
+ rendered = input.content;
5346
+ content = void 0;
5347
+ } else {
5348
+ rendered = await renderEmailHtml(input.content);
5349
+ content = input.content;
5350
+ }
5351
+ const body = {
5352
+ id,
5353
+ subject: input.subject,
5354
+ rendered,
5355
+ content,
5356
+ recipients: {
5357
+ to: normaliseGroup(input.recipients.to),
5358
+ cc: normaliseGroup(input.recipients.cc),
5359
+ bcc: normaliseGroup(input.recipients.bcc)
5360
+ }
5361
+ };
5362
+ const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
5363
+ method: "POST",
5364
+ headers: { "Content-Type": "application/json" },
5365
+ body: JSON.stringify(body)
5366
+ });
5367
+ if (!response.ok) {
5368
+ const errorBody = await response.json().catch(() => ({}));
5369
+ throw new Error(
5370
+ `Failed to send notification: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
5371
+ );
5372
+ }
5373
+ await response.body?.cancel();
5374
+ return id;
5375
+ });
5376
+ }
5377
+ __name(notifyEmail, "notifyEmail");
5378
+ function createNotifier() {
5379
+ return { email: notifyEmail };
5380
+ }
5381
+ __name(createNotifier, "createNotifier");
5010
5382
 
5011
5383
  // src/experimental.ts
5012
5384
  var experimental_exports = {};
@@ -5371,16 +5743,17 @@ __name(LlmFlowStep, "LlmFlowStep");
5371
5743
  import { z } from "zod";
5372
5744
  var createTraceAPI2 = createTraceAPI;
5373
5745
  function ksuid() {
5374
- return KSUID3.randomSync().string;
5746
+ return KSUID4.randomSync().string;
5375
5747
  }
5376
5748
  __name(ksuid, "ksuid");
5749
+ var notify = createNotifier();
5377
5750
  export {
5378
5751
  Duration,
5379
5752
  ErrorPresets,
5380
5753
  File,
5381
5754
  FlowsAPI,
5382
5755
  InlineFile,
5383
- KSUID3 as KSUID,
5756
+ KSUID4 as KSUID,
5384
5757
  ModelAPI,
5385
5758
  NonRetriableError,
5386
5759
  PERMISSION_STATE,
@@ -5395,6 +5768,8 @@ export {
5395
5768
  TaskAPI,
5396
5769
  checkBuiltInPermissions,
5397
5770
  createFlowContext,
5771
+ createIntegrationServer,
5772
+ createNotifier,
5398
5773
  createTraceAPI2 as createTraceAPI,
5399
5774
  experimental_exports as experimental,
5400
5775
  handleFlow,
@@ -5402,7 +5777,9 @@ export {
5402
5777
  handleRequest,
5403
5778
  handleRoute,
5404
5779
  handleSubscriber,
5780
+ insertNewStep,
5405
5781
  ksuid,
5782
+ notify,
5406
5783
  tracing_exports as tracing,
5407
5784
  useDatabase,
5408
5785
  z