@dbos-inc/aws-s3-workflows 3.0.29-preview

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/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # DBOS AWS Simple Storage Service (S3) Workflows
2
+
3
+ This is a [DBOS](https://docs.dbos.dev/) workflow library for working with [Amazon Web Services Simple Storage Service (S3)](https://aws.amazon.com/s3/). The primary feature of this library is workflows that keep a database table in sync with the S3 contents, regardless of failures. These workflows can be used as-is, or as an example for your own code.
4
+
5
+ ## Keeping S3 And Database In Sync
6
+
7
+ AWS applications often store large / unstructured data as S3 objects. S3 is, as the name implies, simple storage, and it doesn't comprehensively track file attributes, permissions, fine-grained ownership, dependencies, etc., and listing the objects can be inefficient and expensive.
8
+
9
+ Keeping an indexed set of file metadata records, including referential links to their owners, is a "database problem". And, while keeping the database in sync with the contents of S3 sounds like it may be tricky, [DBOS Workflows](https://docs.dbos.dev/typescript/tutorials/workflow-tutorial) provide the perfect tool for accomplishing this, even in the face of client or server failures.
10
+
11
+ When keeping a database table in sync with S3 contents, several failures have to be anticipated:
12
+
13
+ 1. The S3 object could get added without a database record getting added, perhaps due to a machine failure during processing. This would create an orphaned S3 object.
14
+ 2. Likewise, the a database table entry could get removed, without the S3 object getting cleaned up.
15
+ 3. An outside process responsible for putting an object into S3 could fail, leaving the S3 object state in an unknown condition.
16
+
17
+ This library solves all three of these problems via lightweight, durable DBOS workflows. DBOS workflows ensure that execution resumes, finishing the job of adding or removing the database table entries, or performing cleanup if an outside process does not indicate success within a timeout period.
18
+
19
+ ## Getting Started
20
+
21
+ To use AWS S3:
22
+
23
+ - Register with AWS, create an S3 bucket, and create access credentials. (See [Getting started with Amazon S3](https://aws.amazon.com/s3/getting-started/) in AWS documentation.)
24
+ - If browser-based clients will read from S3 directly, set up S3 CORS. (See [Using cross-origin resource sharing (CORS)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html) in AWS documentation.)
25
+
26
+ ## Using AWS S3 From a DBOS Application
27
+
28
+ First, ensure that the AWS S3 SDK and DBOS S3 workflows are installed into the application:
29
+
30
+ ```
31
+ npm install --save @dbos-inc/aws-s3-workflows aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
32
+ ```
33
+
34
+ Second, ensure that workflow registration functions are imported:
35
+
36
+ ```typescript
37
+ import {
38
+ FileRecord,
39
+ S3WorkflowCallbacks,
40
+ registerS3UploadWorkflow,
41
+ registerS3PresignedUploadWorkflow,
42
+ registerS3DeleteWorkflow,
43
+ } from '@dbos-inc/aws-s3-workflows';
44
+ ```
45
+
46
+ Third, create your database transactions, and which will be used in the callback for the S3 workflows. The transactions below are just examples, you can use your own table schema, data source, and migrations.
47
+
48
+ ```typescript
49
+ // See the test for schema, migrations, file lookup SQL, etc...
50
+ class TestUserFileTable {
51
+ // File table DML operations
52
+ @DBOS.transaction()
53
+ static async insertFileRecord(rec: UserFile) {
54
+ await DBOS.knexClient<FileDetails>('user_files').insert(TestUserFileTable.toFileDetails(rec));
55
+ }
56
+ @DBOS.transaction()
57
+ static async updateFileRecord(rec: UserFile) {
58
+ await DBOS.knexClient<FileDetails>('user_files')
59
+ .update(TestUserFileTable.toFileDetails(rec))
60
+ .where({ file_id: rec.file_id });
61
+ }
62
+ @DBOS.transaction()
63
+ static async deleteFileRecordById(file_id: string) {
64
+ await DBOS.knexClient<FileDetails>('user_files').delete().where({ file_id });
65
+ }
66
+ // Lookups, etc. ...
67
+ }
68
+ ```
69
+
70
+ Fourth, create an callback for the workflows to use. The callback will specify the transactions and exact S3 calls to make, as these can be customized based on the method used for obtaining an S3 client.
71
+
72
+ ```typescript
73
+ const s3callback: S3WorkflowCallbacks<UserFile, Opts> = {
74
+ // Database operations (these should be transactions)
75
+ newActiveFile: async (rec: UserFile) => {
76
+ rec.file_status = FileStatus.ACTIVE;
77
+ return await TestUserFileTable.insertFileRecord(rec);
78
+ },
79
+ newPendingFile: async (rec: UserFile) => {
80
+ rec.file_status = FileStatus.PENDING;
81
+ return await TestUserFileTable.insertFileRecord(rec);
82
+ },
83
+ fileActivated: async (rec: UserFile) => {
84
+ rec.file_status = FileStatus.ACTIVE;
85
+ return await TestUserFileTable.updateFileRecord(rec);
86
+ },
87
+ fileDeleted: async (rec: UserFile) => {
88
+ return await TestUserFileTable.deleteFileRecordById(rec.file_id);
89
+ },
90
+
91
+ // S3 interaction options, these will be run as steps
92
+ putS3Contents: async (rec: UserFile, content: string, options?: Opts) => {
93
+ return await s3client?.send(
94
+ new PutObjectCommand({
95
+ Bucket: s3bucket,
96
+ Key: rec.key,
97
+ ContentType: options?.contentType ?? 'text/plain',
98
+ Body: content,
99
+ }),
100
+ );
101
+ },
102
+ createPresignedPost: async (rec: UserFile, timeout?: number, opts?: Opts) => {
103
+ const postPresigned = await createPresignedPost(s3client!, {
104
+ Conditions: [
105
+ ['content-length-range', 1, 10000000], // 10MB
106
+ ],
107
+ Bucket: s3bucket!,
108
+ Key: rec.key,
109
+ Expires: timeout || 60,
110
+ Fields: {
111
+ 'Content-Type': opts?.contentType || '*',
112
+ },
113
+ });
114
+ return { url: postPresigned.url, fields: postPresigned.fields };
115
+ },
116
+ deleteS3Object: async (rec: UserFile) => {
117
+ return await s3client?.send(
118
+ new DeleteObjectCommand({
119
+ Bucket: s3bucket,
120
+ Key: rec.key,
121
+ }),
122
+ );
123
+ },
124
+ };
125
+ ```
126
+
127
+ Finally, register and use the S3 workflows:
128
+
129
+ ```typescript
130
+ export const uploadWF = registerS3UploadWorkflow({ className: 'UserFile', name: 'uploadWF' }, s3callback);
131
+ ...
132
+ await uploadWF(myFileRec, 'This is my file');
133
+ ```
134
+
135
+ ### Workflow to Upload a String to S3
136
+
137
+ The `registerS3UploadWorkflow` function registers a workflow that stores a string to an S3 key, and runs the callback function to update the database. If anything goes wrong during the workflow, S3 will be cleaned up and the database will be unchanged by the workflow.
138
+
139
+ ```typescript
140
+ export const uploadWF = registerS3UploadWorkflow({ className: 'UserFile', name: 'uploadWF' }, s3callback);
141
+ ...
142
+ await uploadWF(myFileRec, 'These contents should be saved to S3');
143
+ ```
144
+
145
+ This workflow performs the following actions:
146
+
147
+ - Puts the string in S3
148
+ - If there is difficulty with S3, ensures that no entry is left there and throws an error
149
+ - Invokes the transaction for a new active file record
150
+
151
+ ### Workflow to Delete a File
152
+
153
+ The `registerS3DeleteWorkflow` function creates a workflow that removes a file from both S3 and the database.
154
+
155
+ ```typescript
156
+ export const deleteWF = registerS3DeleteWorkflow({ className: 'UserFile', name: 'deleteWF' }, s3callback);
157
+
158
+ await deleteWF(fileDBRecord);
159
+ ```
160
+
161
+ This workflow performs the following actions:
162
+
163
+ - Invokes the callback transaction for a deleting the file record
164
+ - Removes the key from S3
165
+
166
+ ### Workflow to Allow Client File Upload via Presigned URLs
167
+
168
+ It is often not convenient to send or retrieve large S3 object contents through DBOS; the client should exchange data directly with S3. S3 accomodates this use case very well, using a feature called ["presigned URLs"](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html).
169
+
170
+ In these cases, the client can place a request to DBOS that produces a presigned POST URL, which the client can use for a limited time and purpose for S3 access. The DBOS workflow takes care of making sure that table entries are cleaned up if the client does not finish writing files.
171
+
172
+ The workflow interaction generally proceeds as follows:
173
+
174
+ - The client makes some request to a DBOS API handler.
175
+ - The handler decides that the client will be uploading a file, and starts the upload workflow.
176
+ - The upload workflow sends the handler a presigned post, which the handler returns to the client. The workflow continues in the background, waiting to hear that the client upload is complete.
177
+ - The client (ideally) uploads the file using the presigned post and notifies the application; if not, the workflow times out and cleans up. (The workflow timeout should be set to occur after the presigned post expires.)
178
+ - If successful, the database is updated to reflect the new file, otherwise S3 is cleaned of any partial work.
179
+
180
+ The workflow can be initiated in the following way:
181
+
182
+ ```typescript
183
+ export const uploadPWF = registerS3PresignedUploadWorkflow({ className: 'UserFile', name: 'uploadPWF' }, s3callback);
184
+
185
+ // Start the workflow
186
+ const wfHandle = await DBOS.startWorkflow(uploadPWF)(
187
+ myFileRec,
188
+ 60, // Expiration (seconds)
189
+ { contentType: 'text/plain' },
190
+ );
191
+
192
+ // Get the presigned post (to pass to client)
193
+ const ppost = await DBOS.getEvent<PresignedPost>(wfHandle.workflowID, 'uploadkey');
194
+ ```
195
+
196
+ The client will then use the presigned post to upload data to S3 directly:
197
+
198
+ ```typescript
199
+ // Upload to the URL from the presigned post
200
+ const res = await uploadToS3(ppost!, '/path/to/file');
201
+ ```
202
+
203
+ Upon a completion call from the client, the workflow should be notified so that it proceeds:
204
+
205
+ ```typescript
206
+ // Look up wfHandle by the workflow ID
207
+ const wfHandle = DBOS.retrieveWorkflow(wfid);
208
+
209
+ // Notify workflow - truish means success, any falsy value indicates failure / cancel
210
+ await DBOS.send<boolean>(wfHandle.workflowID(), true, 'uploadfinish');
211
+
212
+ // Optionally, await completion of the workflow; this ensures that the database record is written,
213
+ // or will throw an error if anything went wrong in the workflow
214
+ await wfHandle.getResult();
215
+ ```
216
+
217
+ ## Notes
218
+
219
+ _Do not reuse S3 keys._ Assigning unique identifiers to files is a much better idea, if a "name" is to be reused, it can be reused in the lookup database.
220
+
221
+ Reasons why S3 keys should not be reused:
222
+
223
+ - S3 caches the key contents. Even a response of "this key doesn't exist" can be cached. If you reuse keys, you may get a stale value.
224
+ - Workflow operations against an old use of a key may still be in process... for example a delete workflow may still be attempting to delete the old object at the same time a new file is being placed under the same key.
225
+
226
+ ## Simple Testing
227
+
228
+ The `s3_utils.test.ts` file included in the source repository can be used to upload and download files to/from S3 using various approaches. Before running, set the following environment variables:
229
+
230
+ - `S3_BUCKET`: The S3 bucket for setting / retrieving test objects
231
+ - `AWS_REGION`: AWS region to use
232
+ - `AWS_ACCESS_KEY_ID`: The access key with permission to use the S3 service
233
+ - `AWS_SECRET_ACCESS_KEY`: The secret access key corresponding to `AWS_ACCESS_KEY_ID`
234
+
235
+ The test illustrates S3 setup, use of the workflows, and some S3 helper functions.
236
+
237
+ ## Next Steps
238
+
239
+ - To start a DBOS app from a template, visit our [quickstart](https://docs.dbos.dev/quickstart).
240
+ - For DBOS programming tutorials, check out our [programming guide](https://docs.dbos.dev/typescript/programming-guide).
241
+ - To learn more about DBOS, take a look at [our documentation](https://docs.dbos.dev/) or our [source code](https://github.com/dbos-inc/dbos-transact-ts).
@@ -0,0 +1,20 @@
1
+ # To enable auto-completion and validation for this file in VSCode, install the RedHat YAML extension
2
+ # https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml
3
+
4
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/dbos-inc/dbos-transact/main/dbos-config.schema.json
5
+
6
+ # NOTE this is all stuff for the s3 test.
7
+ # While some is a useful guide, not all of it needs to be the same in a real app.
8
+
9
+ database:
10
+ hostname: 'localhost'
11
+ port: 5432
12
+ username: 'postgres'
13
+ password: ${PGPASSWORD}
14
+ app_db_name: 'dbostest'
15
+ connectionTimeoutMillis: 3000
16
+ app_db_client: 'knex'
17
+ migrate: ['npx knex migrate:latest']
18
+ rollback: ['npx knex migrate:rollback']
19
+ runtimeConfig:
20
+ port: 3300 # Optional, defaults to 3000
@@ -0,0 +1,2 @@
1
+ export { FileRecord, S3WorkflowCallbacks, registerS3UploadWorkflow, registerS3PresignedUploadWorkflow, registerS3DeleteWorkflow, } from './src/s3_utils';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,mBAAmB,EACnB,wBAAwB,EACxB,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerS3DeleteWorkflow = exports.registerS3PresignedUploadWorkflow = exports.registerS3UploadWorkflow = void 0;
4
+ var s3_utils_1 = require("./src/s3_utils");
5
+ Object.defineProperty(exports, "registerS3UploadWorkflow", { enumerable: true, get: function () { return s3_utils_1.registerS3UploadWorkflow; } });
6
+ Object.defineProperty(exports, "registerS3PresignedUploadWorkflow", { enumerable: true, get: function () { return s3_utils_1.registerS3PresignedUploadWorkflow; } });
7
+ Object.defineProperty(exports, "registerS3DeleteWorkflow", { enumerable: true, get: function () { return s3_utils_1.registerS3DeleteWorkflow; } });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AAAA,2CAMwB;AAHtB,oHAAA,wBAAwB,OAAA;AACxB,6HAAA,iCAAiC,OAAA;AACjC,oHAAA,wBAAwB,OAAA"}
@@ -0,0 +1,4 @@
1
+ import { Knex } from 'knex';
2
+ declare const config: Knex.Config;
3
+ export default config;
4
+ //# sourceMappingURL=knexfile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knexfile.d.ts","sourceRoot":"","sources":["../knexfile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM5B,QAAA,MAAM,MAAM,EAAE,IAAI,CAAC,MAYlB,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
4
+ const [dbosConfig] = (0, dbos_sdk_1.parseConfigFile)();
5
+ const config = {
6
+ client: 'pg',
7
+ connection: {
8
+ host: dbosConfig.poolConfig.host,
9
+ user: dbosConfig.poolConfig.user,
10
+ password: dbosConfig.poolConfig.password,
11
+ database: dbosConfig.poolConfig.database,
12
+ ssl: dbosConfig.poolConfig.ssl,
13
+ },
14
+ migrations: {
15
+ directory: './migrations',
16
+ },
17
+ };
18
+ exports.default = config;
19
+ //# sourceMappingURL=knexfile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knexfile.js","sourceRoot":"","sources":["../knexfile.ts"],"names":[],"mappings":";;AACA,iDAAqD;AAGrD,MAAM,CAAC,UAAU,CAAC,GAA0B,IAAA,0BAAe,GAAE,CAAC;AAE9D,MAAM,MAAM,GAAgB;IAC1B,MAAM,EAAE,IAAI;IACZ,UAAU,EAAE;QACV,IAAI,EAAE,UAAU,CAAC,UAAW,CAAC,IAAI;QACjC,IAAI,EAAE,UAAU,CAAC,UAAW,CAAC,IAAI;QACjC,QAAQ,EAAE,UAAU,CAAC,UAAW,CAAC,QAAQ;QACzC,QAAQ,EAAE,UAAU,CAAC,UAAW,CAAC,QAAQ;QACzC,GAAG,EAAE,UAAU,CAAC,UAAW,CAAC,GAAG;KAChC;IACD,UAAU,EAAE;QACV,SAAS,EAAE,cAAc;KAC1B;CACF,CAAC;AAEF,kBAAe,MAAM,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
4
+ //# sourceMappingURL=20240410.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20240410.d.ts","sourceRoot":"","sources":["../../migrations/20240410.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,wBAAsB,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CASlD;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpD"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.down = exports.up = void 0;
4
+ async function up(knex) {
5
+ await knex.schema.createTable('user_files', (table) => {
6
+ table.uuid('file_id').primary();
7
+ table.uuid('user_id'); //.index().references("cusers.user_id");
8
+ table.string('file_status', 16);
9
+ table.string('file_type', 16);
10
+ table.bigint('file_time');
11
+ table.string('file_name', 128);
12
+ });
13
+ }
14
+ exports.up = up;
15
+ async function down(knex) {
16
+ await knex.schema.dropTable('user_files');
17
+ }
18
+ exports.down = down;
19
+ //# sourceMappingURL=20240410.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20240410.js","sourceRoot":"","sources":["../../migrations/20240410.ts"],"names":[],"mappings":";;;AAEO,KAAK,UAAU,EAAE,CAAC,IAAU;IACjC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE;QACpD,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,wCAAwC;QAC/D,KAAK,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QAChC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC9B,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAC1B,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;AACL,CAAC;AATD,gBASC;AAEM,KAAK,UAAU,IAAI,CAAC,IAAU;IACnC,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;AAC5C,CAAC;AAFD,oBAEC"}
@@ -0,0 +1,57 @@
1
+ import { type PresignedPost } from '@aws-sdk/s3-presigned-post';
2
+ import { WorkflowConfig } from '@dbos-inc/dbos-sdk';
3
+ export interface FileRecord {
4
+ key: string;
5
+ }
6
+ export interface S3WorkflowCallbacks<R extends FileRecord, Options = unknown> {
7
+ /** Called back when a new, active file is created; should write the file to the database as active */
8
+ newActiveFile: (rec: R) => Promise<unknown>;
9
+ /** Called back when a file might get uploaded; this function may write the file to the database as 'pending' */
10
+ newPendingFile: (rec: R) => Promise<unknown>;
11
+ /** Called back when a pending file becomes active; should write the file to the database as active */
12
+ fileActivated: (rec: R) => Promise<unknown>;
13
+ /** Called back when a file is in the process of getting deleted; should remove the file from the database */
14
+ fileDeleted: (rec: R) => Promise<unknown>;
15
+ /** Should execute the S3 operation to write contents to rec.key; this will be run as a step */
16
+ putS3Contents: (rec: R, content: string, options?: Options) => Promise<unknown>;
17
+ /** Should execute the S3 operation to create a presigned post for external upload, this will be run as a step */
18
+ createPresignedPost: (rec: R, timeout?: number, options?: Options) => Promise<PresignedPost>;
19
+ /** Optional validation to check if a client S3 upload is valid, before activating in the datbase. Will run as a step */
20
+ validateS3Upload?: (rec: R) => Promise<void>;
21
+ /** Should execute the S3 operation to delete rec.key; this will be run as a step */
22
+ deleteS3Object: (rec: R) => Promise<unknown>;
23
+ }
24
+ /**
25
+ * Create a workflow function for deleting S3 objects and removing the DB entry
26
+ * @param options - Registration options for the workflow
27
+ * @param callbacks - S3 operation implementation and database recordkeeping transactions
28
+ */
29
+ export declare function registerS3DeleteWorkflow<R extends FileRecord, Options = unknown>(options: {
30
+ name?: string;
31
+ ctorOrProto?: object;
32
+ className?: string;
33
+ config?: WorkflowConfig;
34
+ }, callbacks: S3WorkflowCallbacks<R, Options>): (this: unknown, fileDetails: R) => Promise<unknown>;
35
+ /**
36
+ * Create a workflow function for uploading S3 contents from DBOS
37
+ * @param options - Registration options for the workflow
38
+ * @param callbacks - S3 operation implementation and database recordkeeping transactions
39
+ */
40
+ export declare function registerS3UploadWorkflow<R extends FileRecord, Options = unknown>(options: {
41
+ name?: string;
42
+ ctorOrProto?: object;
43
+ className?: string;
44
+ config?: WorkflowConfig;
45
+ }, callbacks: S3WorkflowCallbacks<R, Options>): (this: unknown, fileDetails: R, content: string, objOptions?: Options | undefined) => Promise<R>;
46
+ /**
47
+ * Create a workflow function for uploading S3 contents externally, via a presigned URL
48
+ * @param options - Registration options for the workflow
49
+ * @param callbacks - S3 operation implementation and database recordkeeping transactions
50
+ */
51
+ export declare function registerS3PresignedUploadWorkflow<R extends FileRecord, Options = unknown>(options: {
52
+ name?: string;
53
+ ctorOrProto?: object;
54
+ className?: string;
55
+ config?: WorkflowConfig;
56
+ }, callbacks: S3WorkflowCallbacks<R, Options>): (this: unknown, fileDetails: R, timeoutSeconds: number, objOptions?: Options | undefined) => Promise<R>;
57
+ //# sourceMappingURL=s3_utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3_utils.d.ts","sourceRoot":"","sources":["../../src/s3_utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,EAAQ,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,GAAG,OAAO;IAE1E,sGAAsG;IACtG,aAAa,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,gHAAgH;IAChH,cAAc,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,sGAAsG;IACtG,aAAa,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,6GAA6G;IAC7G,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAG1C,+FAA+F;IAC/F,aAAa,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChF,iHAAiH;IACjH,mBAAmB,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7F,yHAAyH;IACzH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,oFAAoF;IACpF,cAAc,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,GAAG,OAAO,EAC9E,OAAO,EAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,EACD,SAAS,EAAE,mBAAmB,CAAC,CAAC,EAAE,OAAO,CAAC,uDAW3C;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,GAAG,OAAO,EAC9E,OAAO,EAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,EACD,SAAS,EAAE,mBAAmB,CAAC,CAAC,EAAE,OAAO,CAAC,oGA2B3C;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,GAAG,OAAO,EACvF,OAAO,EAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,EACD,SAAS,EAAE,mBAAmB,CAAC,CAAC,EAAE,OAAO,CAAC,2GAgD3C"}
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerS3PresignedUploadWorkflow = exports.registerS3UploadWorkflow = exports.registerS3DeleteWorkflow = void 0;
4
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
5
+ /**
6
+ * Create a workflow function for deleting S3 objects and removing the DB entry
7
+ * @param options - Registration options for the workflow
8
+ * @param callbacks - S3 operation implementation and database recordkeeping transactions
9
+ */
10
+ function registerS3DeleteWorkflow(options, callbacks) {
11
+ return dbos_sdk_1.DBOS.registerWorkflow(async (fileDetails) => {
12
+ await callbacks.fileDeleted(fileDetails);
13
+ return await dbos_sdk_1.DBOS.runStep(async () => {
14
+ return callbacks.deleteS3Object(fileDetails);
15
+ }, { name: 'deleteS3Object' });
16
+ }, options);
17
+ }
18
+ exports.registerS3DeleteWorkflow = registerS3DeleteWorkflow;
19
+ /**
20
+ * Create a workflow function for uploading S3 contents from DBOS
21
+ * @param options - Registration options for the workflow
22
+ * @param callbacks - S3 operation implementation and database recordkeeping transactions
23
+ */
24
+ function registerS3UploadWorkflow(options, callbacks) {
25
+ return dbos_sdk_1.DBOS.registerWorkflow(async (fileDetails, content, objOptions) => {
26
+ try {
27
+ await dbos_sdk_1.DBOS.runStep(async () => {
28
+ await callbacks.putS3Contents(fileDetails, content, objOptions);
29
+ }, { name: 'putS3Contents' });
30
+ }
31
+ catch (e) {
32
+ try {
33
+ await dbos_sdk_1.DBOS.runStep(async () => {
34
+ return callbacks.deleteS3Object(fileDetails);
35
+ }, { name: 'deleteS3Object' });
36
+ }
37
+ catch (e2) {
38
+ dbos_sdk_1.DBOS.logger.debug(e2);
39
+ }
40
+ throw e;
41
+ }
42
+ await callbacks.newActiveFile(fileDetails);
43
+ return fileDetails;
44
+ }, options);
45
+ }
46
+ exports.registerS3UploadWorkflow = registerS3UploadWorkflow;
47
+ /**
48
+ * Create a workflow function for uploading S3 contents externally, via a presigned URL
49
+ * @param options - Registration options for the workflow
50
+ * @param callbacks - S3 operation implementation and database recordkeeping transactions
51
+ */
52
+ function registerS3PresignedUploadWorkflow(options, callbacks) {
53
+ return dbos_sdk_1.DBOS.registerWorkflow(async (fileDetails, timeoutSeconds, objOptions) => {
54
+ await callbacks.newPendingFile(fileDetails);
55
+ const upkey = await dbos_sdk_1.DBOS.runStep(async () => {
56
+ return await callbacks.createPresignedPost(fileDetails, timeoutSeconds, objOptions);
57
+ }, { name: 'createPresignedPost' });
58
+ await dbos_sdk_1.DBOS.setEvent('uploadkey', upkey);
59
+ try {
60
+ const res = await dbos_sdk_1.DBOS.recv('uploadfinish', timeoutSeconds + 60);
61
+ if (!res) {
62
+ throw new Error('S3 operation timed out or canceled');
63
+ }
64
+ // Validate the file, if we have code for that
65
+ if (callbacks.validateS3Upload) {
66
+ await dbos_sdk_1.DBOS.runStep(async () => {
67
+ await callbacks.validateS3Upload(fileDetails);
68
+ }, { name: 'validateFileUpload' });
69
+ }
70
+ await callbacks?.fileActivated(fileDetails);
71
+ }
72
+ catch (e) {
73
+ try {
74
+ await callbacks.fileDeleted(fileDetails);
75
+ await dbos_sdk_1.DBOS.runStep(async () => {
76
+ await callbacks.deleteS3Object(fileDetails);
77
+ }, { name: 'deleteS3Object' });
78
+ }
79
+ catch (e2) {
80
+ dbos_sdk_1.DBOS.logger.debug(e2);
81
+ }
82
+ throw e;
83
+ }
84
+ return fileDetails;
85
+ }, options);
86
+ }
87
+ exports.registerS3PresignedUploadWorkflow = registerS3PresignedUploadWorkflow;
88
+ //# sourceMappingURL=s3_utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3_utils.js","sourceRoot":"","sources":["../../src/s3_utils.ts"],"names":[],"mappings":";;;AAEA,iDAA0D;AA4B1D;;;;GAIG;AACH,SAAgB,wBAAwB,CACtC,OAKC,EACD,SAA0C;IAE1C,OAAO,eAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,WAAc,EAAE,EAAE;QACpD,MAAM,SAAS,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QACzC,OAAO,MAAM,eAAI,CAAC,OAAO,CACvB,KAAK,IAAI,EAAE;YACT,OAAO,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAC/C,CAAC,EACD,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAC3B,CAAC;IACJ,CAAC,EAAE,OAAO,CAAC,CAAC;AACd,CAAC;AAlBD,4DAkBC;AAED;;;;GAIG;AACH,SAAgB,wBAAwB,CACtC,OAKC,EACD,SAA0C;IAE1C,OAAO,eAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,WAAc,EAAE,OAAe,EAAE,UAAoB,EAAE,EAAE;QAC3F,IAAI,CAAC;YACH,MAAM,eAAI,CAAC,OAAO,CAChB,KAAK,IAAI,EAAE;gBACT,MAAM,SAAS,CAAC,aAAa,CAAC,WAAW,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YAClE,CAAC,EACD,EAAE,IAAI,EAAE,eAAe,EAAE,CAC1B,CAAC;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,eAAI,CAAC,OAAO,CAChB,KAAK,IAAI,EAAE;oBACT,OAAO,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;gBAC/C,CAAC,EACD,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAC3B,CAAC;YACJ,CAAC;YAAC,OAAO,EAAE,EAAE,CAAC;gBACZ,eAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACxB,CAAC;YACD,MAAM,CAAC,CAAC;QACV,CAAC;QAED,MAAM,SAAS,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;QAC3C,OAAO,WAAW,CAAC;IACrB,CAAC,EAAE,OAAO,CAAC,CAAC;AACd,CAAC;AAlCD,4DAkCC;AAED;;;;GAIG;AACH,SAAgB,iCAAiC,CAC/C,OAKC,EACD,SAA0C;IAE1C,OAAO,eAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,WAAc,EAAE,cAAsB,EAAE,UAAoB,EAAE,EAAE;QAClG,MAAM,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QAE5C,MAAM,KAAK,GAAG,MAAM,eAAI,CAAC,OAAO,CAC9B,KAAK,IAAI,EAAE;YACT,OAAO,MAAM,SAAS,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC;QACtF,CAAC,EACD,EAAE,IAAI,EAAE,qBAAqB,EAAE,CAChC,CAAC;QACF,MAAM,eAAI,CAAC,QAAQ,CAAgB,WAAW,EAAE,KAAK,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,eAAI,CAAC,IAAI,CAAU,cAAc,EAAE,cAAc,GAAG,EAAE,CAAC,CAAC;YAE1E,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;YACxD,CAAC;YAED,8CAA8C;YAC9C,IAAI,SAAS,CAAC,gBAAgB,EAAE,CAAC;gBAC/B,MAAM,eAAI,CAAC,OAAO,CAChB,KAAK,IAAI,EAAE;oBACT,MAAM,SAAS,CAAC,gBAAiB,CAAC,WAAW,CAAC,CAAC;gBACjD,CAAC,EACD,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAC/B,CAAC;YACJ,CAAC;YAED,MAAM,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,SAAS,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;gBACzC,MAAM,eAAI,CAAC,OAAO,CAChB,KAAK,IAAI,EAAE;oBACT,MAAM,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;gBAC9C,CAAC,EACD,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAC3B,CAAC;YACJ,CAAC;YAAC,OAAO,EAAE,EAAE,CAAC;gBACZ,eAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACxB,CAAC;YACD,MAAM,CAAC,CAAC;QACV,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC,EAAE,OAAO,CAAC,CAAC;AACd,CAAC;AAvDD,8EAuDC"}
@@ -0,0 +1,25 @@
1
+ import { FileRecord } from './s3_utils';
2
+ interface FileDetails {
3
+ user_id: string;
4
+ file_type: string;
5
+ file_name: string;
6
+ file_id?: string;
7
+ file_status?: string;
8
+ file_time?: number;
9
+ }
10
+ interface UserFile extends FileDetails, FileRecord {
11
+ user_id: string;
12
+ file_type: string;
13
+ file_name: string;
14
+ file_id: string;
15
+ file_status: string;
16
+ file_time: number;
17
+ }
18
+ interface Opts {
19
+ contentType?: string;
20
+ }
21
+ export declare const uploadWF: (this: unknown, fileDetails: UserFile, content: string, objOptions?: Opts | undefined) => Promise<UserFile>;
22
+ export declare const uploadPWF: (this: unknown, fileDetails: UserFile, timeoutSeconds: number, objOptions?: Opts | undefined) => Promise<UserFile>;
23
+ export declare const deleteWF: (this: unknown, fileDetails: UserFile) => Promise<unknown>;
24
+ export {};
25
+ //# sourceMappingURL=s3_utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3_utils.test.d.ts","sourceRoot":"","sources":["../../src/s3_utils.test.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAKX,MAAM,YAAY,CAAC;AAmBpB,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,QAAS,SAAQ,WAAW,EAAE,UAAU;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAmGD,UAAU,IAAI;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAwDD,eAAO,MAAM,QAAQ,6GAAoF,CAAC;AAC1G,eAAO,MAAM,SAAS,oHAA8F,CAAC;AACrH,eAAO,MAAM,QAAQ,4DAAoF,CAAC"}