@lowdefy/plugin-aws 6.0.0 → 7.0.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.
@@ -23,7 +23,7 @@
23
23
  properties:
24
24
  s3PostPolicyRequestId: s3_upload_policy_request
25
25
  button:
26
- icon: UploadOutlined
26
+ icon: upload
27
27
  title: Choose File
28
28
  type: default
29
29
  - id: basic_custom_icon
@@ -31,7 +31,7 @@
31
31
  properties:
32
32
  s3PostPolicyRequestId: s3_upload_policy_request
33
33
  button:
34
- icon: AiOutlineCloudUpload
34
+ icon: CloudUpload
35
35
  title: Upload to Cloud
36
36
  type: default
37
37
 
@@ -43,7 +43,7 @@
43
43
  s3PostPolicyRequestId: s3_upload_policy_request
44
44
  accept: 'image/*'
45
45
  button:
46
- icon: AiOutlinePicture
46
+ icon: image
47
47
  title: Upload Image
48
48
  type: default
49
49
  - id: accept_pdf
@@ -52,7 +52,7 @@
52
52
  s3PostPolicyRequestId: s3_upload_policy_request
53
53
  accept: '.pdf'
54
54
  button:
55
- icon: AiOutlineFilePdf
55
+ icon: FileText
56
56
  title: Upload PDF
57
57
  type: default
58
58
  - id: accept_specific
@@ -61,7 +61,7 @@
61
61
  s3PostPolicyRequestId: s3_upload_policy_request
62
62
  accept: '.jpg,.png,.gif'
63
63
  button:
64
- icon: AiOutlinePicture
64
+ icon: image
65
65
  title: Upload JPG, PNG, or GIF
66
66
  type: default
67
67
 
@@ -73,7 +73,7 @@
73
73
  s3PostPolicyRequestId: s3_upload_policy_request
74
74
  singleFile: true
75
75
  button:
76
- icon: UploadOutlined
76
+ icon: upload
77
77
  title: Upload Single File
78
78
  type: default
79
79
  - id: limit_max_three
@@ -82,7 +82,7 @@
82
82
  s3PostPolicyRequestId: s3_upload_policy_request
83
83
  maxCount: 3
84
84
  button:
85
- icon: UploadOutlined
85
+ icon: upload
86
86
  title: Upload (Max 3)
87
87
  type: default
88
88
  - id: limit_single_images
@@ -92,7 +92,7 @@
92
92
  singleFile: true
93
93
  accept: 'image/*'
94
94
  button:
95
- icon: AiOutlinePicture
95
+ icon: image
96
96
  title: Upload One Image
97
97
  type: default
98
98
 
@@ -103,7 +103,7 @@
103
103
  properties:
104
104
  s3PostPolicyRequestId: s3_upload_policy_request
105
105
  button:
106
- icon: UploadOutlined
106
+ icon: upload
107
107
  title: Upload
108
108
  type: primary
109
109
  - id: button_large
@@ -111,7 +111,7 @@
111
111
  properties:
112
112
  s3PostPolicyRequestId: s3_upload_policy_request
113
113
  button:
114
- icon: UploadOutlined
114
+ icon: upload
115
115
  title: Upload Files
116
116
  type: default
117
117
  size: large
@@ -120,7 +120,7 @@
120
120
  properties:
121
121
  s3PostPolicyRequestId: s3_upload_policy_request
122
122
  button:
123
- icon: AiOutlinePaperClip
123
+ icon: attach
124
124
  title: Attach Files
125
125
  type: default
126
126
 
@@ -137,7 +137,7 @@
137
137
  s3PostPolicyRequestId: s3_upload_policy_request
138
138
  disabled: true
139
139
  button:
140
- icon: UploadOutlined
140
+ icon: upload
141
141
  title: Upload
142
142
  type: primary
143
143
  - id: disabled_hidden_list
@@ -155,7 +155,7 @@
155
155
  s3PostPolicyRequestId: s3_upload_policy_request
156
156
  showUploadList: true
157
157
  button:
158
- icon: UploadOutlined
158
+ icon: upload
159
159
  title: Upload (List Visible)
160
160
  type: default
161
161
  - id: list_hidden
@@ -164,7 +164,7 @@
164
164
  s3PostPolicyRequestId: s3_upload_policy_request
165
165
  showUploadList: false
166
166
  button:
167
- icon: UploadOutlined
167
+ icon: upload
168
168
  title: Upload (No List)
169
169
  type: default
170
170
  - id: list_single_hidden
@@ -174,7 +174,7 @@
174
174
  singleFile: true
175
175
  showUploadList: false
176
176
  button:
177
- icon: UploadOutlined
177
+ icon: upload
178
178
  title: Upload Single (No List)
179
179
  type: default
180
180
 
@@ -214,7 +214,7 @@
214
214
  properties:
215
215
  s3PostPolicyRequestId: s3_upload_policy_request
216
216
  button:
217
- icon: UploadOutlined
217
+ icon: upload
218
218
  title: Upload
219
219
  type: default
220
220
  - id: style_label
@@ -232,7 +232,7 @@
232
232
  properties:
233
233
  s3PostPolicyRequestId: s3_upload_policy_request
234
234
  button:
235
- icon: UploadOutlined
235
+ icon: upload
236
236
  title: Upload
237
237
  type: default
238
238
 
@@ -274,7 +274,7 @@
274
274
  s3PostPolicyRequestId: s3_upload_policy_request
275
275
  accept: '.pdf,.docx,.xlsx'
276
276
  button:
277
- icon: AiOutlineFilePdf
277
+ icon: FileText
278
278
  title: Choose Document
279
279
  type: default
280
280
  label:
@@ -288,7 +288,7 @@
288
288
  title: Save Document
289
289
  color: primary
290
290
  variant: solid
291
- icon: AiOutlineSave
291
+ icon: save
292
292
  events:
293
293
  onClick:
294
294
  - id: save_validate
@@ -314,7 +314,7 @@
314
314
  singleFile: true
315
315
  accept: 'image/*'
316
316
  button:
317
- icon: AiOutlineCamera
317
+ icon: camera
318
318
  title: Choose Photo
319
319
  type: default
320
320
  label:
@@ -325,7 +325,7 @@
325
325
  required: true
326
326
  properties:
327
327
  title: Display Name
328
- prefixIcon: AiOutlineUser
328
+ prefixIcon: user
329
329
  placeholder: Enter your display name
330
330
  label:
331
331
  colon: false
@@ -337,7 +337,7 @@
337
337
  title: Update Profile
338
338
  color: primary
339
339
  variant: solid
340
- icon: AiOutlineCheck
340
+ icon: check
341
341
  events:
342
342
  onClick:
343
343
  - id: update_validate
@@ -16,7 +16,7 @@
16
16
  export default {
17
17
  category: 'input',
18
18
  icons: [
19
- 'AiOutlineUpload'
19
+ 'upload'
20
20
  ],
21
21
  valueType: 'object',
22
22
  cssKeys: {
@@ -77,7 +77,7 @@ export default {
77
77
  type: 'object',
78
78
  description: 'Button block properties. See <a href="/Button">Button</a> for all properties.',
79
79
  default: {
80
- icon: 'UploadOutlined',
80
+ icon: 'upload',
81
81
  title: 'Upload',
82
82
  type: 'default'
83
83
  },
@@ -219,7 +219,7 @@
219
219
  title: Submit
220
220
  color: primary
221
221
  variant: solid
222
- icon: AiOutlineUpload
222
+ icon: upload
223
223
  events:
224
224
  onClick:
225
225
  - id: submit_validate
@@ -265,7 +265,7 @@
265
265
  title: Upload Photos
266
266
  color: primary
267
267
  variant: solid
268
- icon: AiOutlineCloudUpload
268
+ icon: CloudUpload
269
269
  events:
270
270
  onClick:
271
271
  - id: gallery_msg
@@ -169,20 +169,20 @@
169
169
  properties:
170
170
  title: Full Name
171
171
  placeholder: Enter your name
172
- prefixIcon: AiOutlineUser
172
+ prefixIcon: user
173
173
  - id: profile_email
174
174
  type: TextInput
175
175
  properties:
176
176
  title: Email
177
177
  placeholder: you@example.com
178
- prefixIcon: AiOutlineMail
178
+ prefixIcon: mail
179
179
  - id: profile_save
180
180
  type: Button
181
181
  properties:
182
182
  title: Save Profile
183
183
  color: primary
184
184
  variant: solid
185
- icon: AiOutlineSave
185
+ icon: save
186
186
  events:
187
187
  onClick:
188
188
  - id: validate_profile
@@ -246,7 +246,7 @@
246
246
  title: List Product
247
247
  color: primary
248
248
  variant: solid
249
- icon: AiOutlineCheck
249
+ icon: check
250
250
  block: true
251
251
  events:
252
252
  onClick:
@@ -16,8 +16,8 @@
16
16
  export default {
17
17
  category: 'input',
18
18
  icons: [
19
- 'AiOutlineLoading',
20
- 'AiOutlineCamera'
19
+ 'loading',
20
+ 'camera'
21
21
  ],
22
22
  valueType: 'object',
23
23
  cssKeys: {
@@ -12,15 +12,23 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import AwsS3GetObject from './AwsS3GetObject/AwsS3GetObject.js';
15
+ */ import AwsS3DeleteObject from './AwsS3DeleteObject/AwsS3DeleteObject.js';
16
+ import AwsS3GetObject from './AwsS3GetObject/AwsS3GetObject.js';
17
+ import AwsS3HeadObject from './AwsS3HeadObject/AwsS3HeadObject.js';
16
18
  import AwsS3PresignedGetObject from './AwsS3PresignedGetObject/AwsS3PresignedGetObject.js';
17
19
  import AwsS3PresignedPostPolicy from './AwsS3PresignedPostPolicy/AwsS3PresignedPostPolicy.js';
18
20
  import AwsS3PutObject from './AwsS3PutObject/AwsS3PutObject.js';
19
21
  import schema from './schema.js';
20
22
  export default {
21
23
  schema,
24
+ // Object storage outside the app's database: a journey on a data set keeps its real target.
25
+ meta: {
26
+ dataSet: 'external'
27
+ },
22
28
  requests: {
29
+ AwsS3DeleteObject,
23
30
  AwsS3GetObject,
31
+ AwsS3HeadObject,
24
32
  AwsS3PresignedGetObject,
25
33
  AwsS3PresignedPostPolicy,
26
34
  AwsS3PutObject
@@ -0,0 +1,65 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { DeleteObjectCommand, DeleteObjectsCommand } from '@aws-sdk/client-s3';
16
+ import { type } from '@lowdefy/helpers';
17
+ import createS3Client from '../createS3Client.js';
18
+ import schema from './schema.js';
19
+ // Deletes only from the connection's bucket. S3 answers a delete of a key that does not exist as
20
+ // a success, so such a key is returned as deleted. With several keys, S3 reports each key it could
21
+ // not delete in the response instead of failing the call, so those come back as errors.
22
+ async function AwsS3DeleteObject({ request, connection }) {
23
+ const { bucket } = connection;
24
+ const { key, keys } = request;
25
+ const s3 = createS3Client({
26
+ connection
27
+ });
28
+ if (!type.isNone(key)) {
29
+ await s3.send(new DeleteObjectCommand({
30
+ Bucket: bucket,
31
+ Key: key
32
+ }));
33
+ return {
34
+ bucket,
35
+ deleted: [
36
+ key
37
+ ],
38
+ errors: []
39
+ };
40
+ }
41
+ const response = await s3.send(new DeleteObjectsCommand({
42
+ Bucket: bucket,
43
+ Delete: {
44
+ Objects: keys.map((objectKey)=>({
45
+ Key: objectKey
46
+ })),
47
+ Quiet: false
48
+ }
49
+ }));
50
+ return {
51
+ bucket,
52
+ deleted: (response.Deleted ?? []).map((deleted)=>deleted.Key),
53
+ errors: (response.Errors ?? []).map((error)=>({
54
+ key: error.Key,
55
+ code: error.Code,
56
+ message: error.Message
57
+ }))
58
+ };
59
+ }
60
+ AwsS3DeleteObject.schema = schema;
61
+ AwsS3DeleteObject.meta = {
62
+ checkRead: false,
63
+ checkWrite: true
64
+ };
65
+ export default AwsS3DeleteObject;
@@ -0,0 +1,86 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ export default {
16
+ $schema: 'http://json-schema.org/draft-07/schema#',
17
+ title: 'Lowdefy Request Schema - AwsS3DeleteObject',
18
+ type: 'object',
19
+ // An empty key makes the SDK send DELETE to the bucket itself, which S3 reads as DeleteBucket.
20
+ properties: {
21
+ key: {
22
+ type: 'string',
23
+ minLength: 1,
24
+ description: 'Key of the object to delete. Give either key or keys.',
25
+ errorMessage: {
26
+ type: 'AwsS3DeleteObject request property "key" should be a string.',
27
+ minLength: 'AwsS3DeleteObject request property "key" should not be empty.'
28
+ }
29
+ },
30
+ keys: {
31
+ type: 'array',
32
+ minItems: 1,
33
+ maxItems: 1000,
34
+ items: {
35
+ type: 'string',
36
+ minLength: 1,
37
+ errorMessage: {
38
+ type: 'AwsS3DeleteObject request property "keys" should be an array of strings.',
39
+ minLength: 'AwsS3DeleteObject request property "keys" should not hold an empty key.'
40
+ }
41
+ },
42
+ description: 'Keys of the objects to delete, 1 to 1000. Give either key or keys.',
43
+ errorMessage: {
44
+ type: 'AwsS3DeleteObject request property "keys" should be an array of strings.',
45
+ minItems: 'AwsS3DeleteObject request property "keys" should have at least 1 key.',
46
+ maxItems: 'AwsS3DeleteObject request property "keys" should have at most 1000 keys.'
47
+ }
48
+ }
49
+ },
50
+ // The bucket always comes from the connection, so a stray "bucket" is refused rather than
51
+ // ignored: ignoring it would delete the same key in the connection's bucket.
52
+ additionalProperties: false,
53
+ if: {
54
+ type: 'object'
55
+ },
56
+ then: {
57
+ oneOf: [
58
+ {
59
+ required: [
60
+ 'key'
61
+ ],
62
+ not: {
63
+ required: [
64
+ 'keys'
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ required: [
70
+ 'keys'
71
+ ],
72
+ not: {
73
+ required: [
74
+ 'key'
75
+ ]
76
+ }
77
+ }
78
+ ]
79
+ },
80
+ errorMessage: {
81
+ type: 'AwsS3DeleteObject request properties should be an object.',
82
+ additionalProperties: 'AwsS3DeleteObject request should only have "key" or "keys".',
83
+ // The `then` above is the only check without a message of its own.
84
+ _: 'AwsS3DeleteObject request should have either "key" or "keys", not both.'
85
+ }
86
+ };
@@ -0,0 +1,69 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { HeadObjectCommand } from '@aws-sdk/client-s3';
16
+ import { type } from '@lowdefy/helpers';
17
+ import createS3Client from '../createS3Client.js';
18
+ import schema from './schema.js';
19
+ // A missing object is an expected answer (e.g. checking a browser upload arrived), so it is
20
+ // returned as data for the routine to act on rather than thrown. A HEAD response has no body,
21
+ // so S3 reports a missing object only through the 404 status.
22
+ async function AwsS3HeadObject({ request, connection }) {
23
+ const { bucket } = connection;
24
+ const { key, versionId } = request;
25
+ const params = {
26
+ Bucket: bucket,
27
+ Key: key
28
+ };
29
+ if (versionId) {
30
+ params.VersionId = versionId;
31
+ }
32
+ const s3 = createS3Client({
33
+ connection
34
+ });
35
+ let response;
36
+ try {
37
+ response = await s3.send(new HeadObjectCommand(params));
38
+ } catch (error) {
39
+ if (error.$metadata?.httpStatusCode === 404) {
40
+ return {
41
+ exists: false,
42
+ bucket,
43
+ key
44
+ };
45
+ }
46
+ throw error;
47
+ }
48
+ const result = {
49
+ exists: true,
50
+ bucket,
51
+ key,
52
+ size: response.ContentLength,
53
+ etag: response.ETag,
54
+ lastModified: response.LastModified
55
+ };
56
+ if (!type.isNone(response.ContentType)) {
57
+ result.contentType = response.ContentType;
58
+ }
59
+ if (!type.isNone(response.VersionId)) {
60
+ result.versionId = response.VersionId;
61
+ }
62
+ return result;
63
+ }
64
+ AwsS3HeadObject.schema = schema;
65
+ AwsS3HeadObject.meta = {
66
+ checkRead: true,
67
+ checkWrite: false
68
+ };
69
+ export default AwsS3HeadObject;
@@ -0,0 +1,42 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ export default {
16
+ $schema: 'http://json-schema.org/draft-07/schema#',
17
+ title: 'Lowdefy Request Schema - AwsS3HeadObject',
18
+ type: 'object',
19
+ required: [
20
+ 'key'
21
+ ],
22
+ properties: {
23
+ key: {
24
+ type: 'string',
25
+ description: 'Key under which the object is stored.',
26
+ errorMessage: {
27
+ type: 'AwsS3HeadObject request property "key" should be a string.'
28
+ }
29
+ },
30
+ versionId: {
31
+ type: 'string',
32
+ description: 'VersionId used to reference a specific version of the object.',
33
+ errorMessage: {
34
+ type: 'AwsS3HeadObject request property "versionId" should be a string.'
35
+ }
36
+ }
37
+ },
38
+ errorMessage: {
39
+ type: 'AwsS3HeadObject request properties should be an object.',
40
+ required: 'AwsS3HeadObject request should have required property "key".'
41
+ }
42
+ };
@@ -13,6 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
16
+ import { ConfigError } from '@lowdefy/errors';
16
17
  import { type } from '@lowdefy/helpers';
17
18
  import createS3Client from '../createS3Client.js';
18
19
  import schema from './schema.js';
@@ -34,7 +35,7 @@ async function AwsS3PresignedPostPolicy({ request, connection }) {
34
35
  params.Fields.acl = acl;
35
36
  }
36
37
  if (type.isObject(fields) === false) {
37
- throw new Error(`properties.fields must be an object. Received ${JSON.stringify(fields)}.`);
38
+ throw new ConfigError('AwsS3PresignedPostPolicy request "fields" must be an object.');
38
39
  }
39
40
  Object.keys(fields).forEach((field)=>{
40
41
  if (fields[field]) {
@@ -13,36 +13,53 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { PutObjectCommand } from '@aws-sdk/client-s3';
16
+ import { type } from '@lowdefy/helpers';
17
+ import copyFromUrl from './copyFromUrl.js';
16
18
  import createS3Client from '../createS3Client.js';
17
19
  import schema from './schema.js';
18
- // Server-side write: stores base64 content as an object. Used by API endpoint
19
- // routines that receive files as endpoint payloads (emitFileContent + CallAPI).
20
+ // Server-side write. With `content`, stores base64 content as an object: used by API endpoint
21
+ // routines that receive files as endpoint payloads (emitFileContent + CallAPI). With `url`,
22
+ // streams what an https link answers into the bucket, so a file handed over as a presigned link
23
+ // never passes through the routine.
20
24
  async function AwsS3PutObject({ request, connection }) {
21
25
  const { bucket } = connection;
22
- const { acl, content, contentType, key } = request;
26
+ const { acl, content, contentType, key, url } = request;
23
27
  const params = {
24
28
  Bucket: bucket,
25
- Key: key,
26
- Body: Buffer.from(content, 'base64')
29
+ Key: key
27
30
  };
28
- if (contentType) {
29
- params.ContentType = contentType;
30
- }
31
31
  if (acl) {
32
32
  params.ACL = acl;
33
33
  }
34
34
  const s3 = createS3Client({
35
35
  connection
36
36
  });
37
+ if (!type.isNone(url)) {
38
+ return copyFromUrl({
39
+ s3,
40
+ params,
41
+ request
42
+ });
43
+ }
44
+ params.Body = Buffer.from(content, 'base64');
45
+ if (contentType) {
46
+ params.ContentType = contentType;
47
+ }
37
48
  await s3.send(new PutObjectCommand(params));
38
49
  return {
39
50
  bucket,
40
- key
51
+ key,
52
+ size: params.Body.length,
53
+ contentType: contentType ?? null
41
54
  };
42
55
  }
43
56
  AwsS3PutObject.schema = schema;
44
57
  AwsS3PutObject.meta = {
45
58
  checkRead: false,
46
- checkWrite: true
59
+ checkWrite: true,
60
+ // A presigned url carries its signature.
61
+ credentialProperties: [
62
+ 'url'
63
+ ]
47
64
  };
48
65
  export default AwsS3PutObject;
@@ -0,0 +1,190 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { Readable } from 'node:stream';
16
+ import { PutObjectCommand } from '@aws-sdk/client-s3';
17
+ import { type } from '@lowdefy/helpers';
18
+ import createConnectPublic from '@lowdefy/node-utils/createConnectPublic.js';
19
+ import { Agent, fetch } from 'undici';
20
+ import createCopyError from './createCopyError.js';
21
+ import limitBody from './limitBody.js';
22
+ import matchesContentType from './matchesContentType.js';
23
+ async function collect(chunks) {
24
+ const buffers = [];
25
+ for await (const chunk of chunks){
26
+ buffers.push(chunk);
27
+ }
28
+ return Buffer.concat(buffers);
29
+ }
30
+ const REDIRECT_STATUSES = new Set([
31
+ 301,
32
+ 302,
33
+ 303,
34
+ 307,
35
+ 308
36
+ ]);
37
+ const MAX_REDIRECTS = 20;
38
+ const dispatcher = new Agent({
39
+ connect: createConnectPublic({
40
+ createNotPublicError: ()=>createCopyError({
41
+ code: 'url_not_public',
42
+ message: 'AwsS3PutObject "url" leads to an address that is not public.'
43
+ })
44
+ })
45
+ });
46
+ function isHttps(link) {
47
+ return URL.parse(link)?.protocol === 'https:';
48
+ }
49
+ async function fetchOnce({ url, signal }) {
50
+ try {
51
+ // Asks for the body uncompressed: fetch decodes a compressed body, whose length then no longer
52
+ // matches the Content-Length checked against maxBytes.
53
+ return await fetch(url, {
54
+ headers: {
55
+ 'accept-encoding': 'identity'
56
+ },
57
+ dispatcher,
58
+ redirect: 'manual',
59
+ signal
60
+ });
61
+ } catch (error) {
62
+ if (signal.aborted) throw error;
63
+ if (error.cause?.code === 'url_not_public') throw error.cause;
64
+ // The url is left out of the message: a presigned link carries its signature.
65
+ throw createCopyError({
66
+ code: 'fetch_failed',
67
+ message: `AwsS3PutObject could not fetch the url: ${error.message}`,
68
+ cause: error
69
+ });
70
+ }
71
+ }
72
+ // Follows redirects by hand, so each link is checked to be https: before it is requested. The
73
+ // dispatcher checks each link's address as it connects.
74
+ async function fetchUrl({ url, signal }) {
75
+ let link = url;
76
+ for(let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1){
77
+ const response = await fetchOnce({
78
+ url: link,
79
+ signal
80
+ });
81
+ const location = response.headers.get('location');
82
+ if (!REDIRECT_STATUSES.has(response.status) || location === null) {
83
+ return response;
84
+ }
85
+ await response.body?.cancel();
86
+ link = URL.parse(location, link)?.href ?? location;
87
+ if (!isHttps(link)) {
88
+ throw createCopyError({
89
+ code: 'url_not_https',
90
+ message: 'AwsS3PutObject "url" redirected to a link that is not https:.'
91
+ });
92
+ }
93
+ }
94
+ throw createCopyError({
95
+ code: 'fetch_failed',
96
+ message: `AwsS3PutObject url redirected more than ${MAX_REDIRECTS} times.`
97
+ });
98
+ }
99
+ // Streams what an https url answers into the bucket, capped at maxBytes. A body with a
100
+ // Content-Length streams straight into one PutObject; one without is read into memory, still
101
+ // capped at maxBytes, since PutObject needs the length up front.
102
+ async function copyFromUrl({ s3, params, request }) {
103
+ const { contentType, contentTypes, maxBytes, timeout = 20000, url } = request;
104
+ if (!isHttps(url)) {
105
+ throw createCopyError({
106
+ code: 'url_not_https',
107
+ message: 'AwsS3PutObject "url" must be an https: link.'
108
+ });
109
+ }
110
+ const signal = AbortSignal.timeout(timeout);
111
+ let refusal = null;
112
+ try {
113
+ const response = await fetchUrl({
114
+ url,
115
+ signal
116
+ });
117
+ if (!response.ok) {
118
+ await response.body?.cancel();
119
+ throw createCopyError({
120
+ code: 'fetch_failed',
121
+ message: `AwsS3PutObject url answered ${response.status}.`,
122
+ status: response.status
123
+ });
124
+ }
125
+ const responseType = response.headers.get('content-type');
126
+ const mediaType = responseType?.split(';')[0].trim().toLowerCase() ?? null;
127
+ if (!type.isNone(contentTypes) && (mediaType === null || !contentTypes.some((pattern)=>matchesContentType({
128
+ pattern,
129
+ mediaType
130
+ })))) {
131
+ await response.body?.cancel();
132
+ throw createCopyError({
133
+ code: 'content_type',
134
+ message: `AwsS3PutObject url content type ${JSON.stringify(mediaType)} is not one of ${contentTypes.join(', ')}.`
135
+ });
136
+ }
137
+ const lengthHeader = response.headers.get('content-length');
138
+ const declaredLength = lengthHeader === null ? null : Number(lengthHeader);
139
+ if (declaredLength !== null && declaredLength > maxBytes) {
140
+ await response.body?.cancel();
141
+ throw createCopyError({
142
+ code: 'too_large',
143
+ message: `AwsS3PutObject url content of ${declaredLength} bytes is larger than maxBytes (${maxBytes}).`
144
+ });
145
+ }
146
+ const chunks = limitBody({
147
+ body: response.body ?? [],
148
+ declaredLength,
149
+ maxBytes,
150
+ onRefuse: (error)=>{
151
+ refusal = error;
152
+ }
153
+ });
154
+ let size;
155
+ if (declaredLength === null) {
156
+ params.Body = await collect(chunks);
157
+ size = params.Body.length;
158
+ } else {
159
+ params.Body = Readable.from(chunks, {
160
+ objectMode: false
161
+ });
162
+ params.ContentLength = declaredLength;
163
+ size = declaredLength;
164
+ }
165
+ const storedType = contentType ?? responseType ?? null;
166
+ if (!type.isNone(storedType)) {
167
+ params.ContentType = storedType;
168
+ }
169
+ await s3.send(new PutObjectCommand(params), {
170
+ abortSignal: signal
171
+ });
172
+ return {
173
+ bucket: params.Bucket,
174
+ key: params.Key,
175
+ size,
176
+ contentType: storedType
177
+ };
178
+ } catch (error) {
179
+ if (refusal !== null) throw refusal;
180
+ if (signal.aborted) {
181
+ throw createCopyError({
182
+ code: 'timeout',
183
+ message: `AwsS3PutObject did not copy the url within the ${timeout} ms timeout.`,
184
+ cause: error
185
+ });
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+ export default copyFromUrl;
@@ -0,0 +1,28 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ // A refused copy from a url. `code` names the reason, so a routine's :catch can answer each its own
17
+ // way; `status` is the HTTP status the url answered with, when there was one.
18
+ function createCopyError({ code, message, status, cause }) {
19
+ const error = new Error(message, {
20
+ cause
21
+ });
22
+ error.code = code;
23
+ if (!type.isUndefined(status)) {
24
+ error.status = status;
25
+ }
26
+ return error;
27
+ }
28
+ export default createCopyError;
@@ -0,0 +1,58 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import createCopyError from './createCopyError.js';
16
+ // Yields the response body, refusing it as soon as it passes maxBytes or the Content-Length it
17
+ // declared. The last chunk is held back until the body has ended and its length checked, so a
18
+ // body that turns out longer or shorter than declared never completes the upload it feeds, and
19
+ // S3 stores nothing. Each refusal is passed to onRefuse before it is thrown, since the S3 client
20
+ // may report a failed body stream as an error of its own.
21
+ async function* limitBody({ body, declaredLength, maxBytes, onRefuse }) {
22
+ function refuse(details) {
23
+ const error = createCopyError(details);
24
+ onRefuse(error);
25
+ return error;
26
+ }
27
+ let size = 0;
28
+ let held = null;
29
+ for await (const chunk of body){
30
+ size += chunk.byteLength;
31
+ if (size > maxBytes) {
32
+ throw refuse({
33
+ code: 'too_large',
34
+ message: `AwsS3PutObject url content is larger than maxBytes (${maxBytes}).`
35
+ });
36
+ }
37
+ if (declaredLength !== null && size > declaredLength) {
38
+ throw refuse({
39
+ code: 'fetch_failed',
40
+ message: `AwsS3PutObject url sent more than the ${declaredLength} bytes its Content-Length declared.`
41
+ });
42
+ }
43
+ if (held !== null) {
44
+ yield held;
45
+ }
46
+ held = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
47
+ }
48
+ if (declaredLength !== null && size !== declaredLength) {
49
+ throw refuse({
50
+ code: 'fetch_failed',
51
+ message: `AwsS3PutObject url sent ${size} of the ${declaredLength} bytes its Content-Length declared.`
52
+ });
53
+ }
54
+ if (held !== null) {
55
+ yield held;
56
+ }
57
+ }
58
+ export default limitBody;
@@ -0,0 +1,22 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // A pattern is a media type ("image/png"), a type with any subtype ("image/*") or "*/*".
16
+ function matchesContentType({ pattern, mediaType }) {
17
+ const wanted = pattern.trim().toLowerCase();
18
+ if (wanted === '*/*') return true;
19
+ if (wanted.endsWith('/*')) return mediaType.startsWith(wanted.slice(0, -1));
20
+ return mediaType === wanted;
21
+ }
22
+ export default matchesContentType;
@@ -17,8 +17,7 @@
17
17
  title: 'Lowdefy Request Schema - AwsS3PutObject',
18
18
  type: 'object',
19
19
  required: [
20
- 'key',
21
- 'content'
20
+ 'key'
22
21
  ],
23
22
  properties: {
24
23
  acl: {
@@ -40,7 +39,7 @@
40
39
  },
41
40
  content: {
42
41
  type: 'string',
43
- description: 'Object content as a base64 encoded string.',
42
+ description: 'Object content as a base64 encoded string. Give either content or url.',
44
43
  errorMessage: {
45
44
  type: 'AwsS3PutObject request property "content" should be a string.'
46
45
  }
@@ -52,19 +51,98 @@
52
51
  type: 'AwsS3PutObject request property "contentType" should be a string.'
53
52
  }
54
53
  },
54
+ contentTypes: {
55
+ type: 'array',
56
+ items: {
57
+ type: 'string'
58
+ },
59
+ description: 'With url, the content types the url may answer with, such as "image/png", "image/*" or "application/pdf". Any other is refused with code "content_type".',
60
+ errorMessage: {
61
+ type: 'AwsS3PutObject request property "contentTypes" should be an array of strings.'
62
+ }
63
+ },
55
64
  key: {
56
65
  type: 'string',
57
66
  description: 'Key under which the object will be stored.',
58
67
  errorMessage: {
59
68
  type: 'AwsS3PutObject request property "key" should be a string.'
60
69
  }
70
+ },
71
+ maxBytes: {
72
+ type: 'integer',
73
+ minimum: 1,
74
+ description: 'Required with url. The most bytes the url may answer with. A larger answer is refused with code "too_large" and nothing is stored.',
75
+ errorMessage: {
76
+ type: 'AwsS3PutObject request property "maxBytes" should be a positive integer.',
77
+ minimum: 'AwsS3PutObject request property "maxBytes" should be a positive integer.'
78
+ }
79
+ },
80
+ timeout: {
81
+ type: 'integer',
82
+ minimum: 1,
83
+ default: 20000,
84
+ description: 'With url, the milliseconds the whole fetch and upload may take before it is refused with code "timeout".',
85
+ errorMessage: {
86
+ type: 'AwsS3PutObject request property "timeout" should be a positive integer.',
87
+ minimum: 'AwsS3PutObject request property "timeout" should be a positive integer.'
88
+ }
89
+ },
90
+ url: {
91
+ type: 'string',
92
+ description: 'An https: link, such as a presigned link, whose answer is streamed into the bucket in place of content. Give either content or url.',
93
+ errorMessage: {
94
+ type: 'AwsS3PutObject request property "url" should be a string.'
95
+ }
61
96
  }
62
97
  },
98
+ if: {
99
+ type: 'object'
100
+ },
101
+ then: {
102
+ oneOf: [
103
+ {
104
+ required: [
105
+ 'content'
106
+ ],
107
+ not: {
108
+ required: [
109
+ 'url'
110
+ ]
111
+ }
112
+ },
113
+ {
114
+ required: [
115
+ 'url'
116
+ ],
117
+ not: {
118
+ required: [
119
+ 'content'
120
+ ]
121
+ }
122
+ }
123
+ ]
124
+ },
125
+ dependencies: {
126
+ url: [
127
+ 'maxBytes'
128
+ ],
129
+ maxBytes: [
130
+ 'url'
131
+ ],
132
+ contentTypes: [
133
+ 'url'
134
+ ],
135
+ timeout: [
136
+ 'url'
137
+ ]
138
+ },
63
139
  errorMessage: {
64
140
  type: 'AwsS3PutObject request properties should be an object.',
65
141
  required: {
66
- key: 'AwsS3PutObject request should have required property "key".',
67
- content: 'AwsS3PutObject request should have required property "content".'
68
- }
142
+ key: 'AwsS3PutObject request should have required property "key".'
143
+ },
144
+ // The `then` above is the only check without a message of its own.
145
+ _: 'AwsS3PutObject request should have either "content" or "url", not both.',
146
+ dependencies: 'AwsS3PutObject request with "url" should have "maxBytes", and "maxBytes", "contentTypes" and "timeout" apply only with "url".'
69
147
  }
70
148
  };
@@ -21,7 +21,10 @@ function createS3Client({ connection }) {
21
21
  accessKeyId,
22
22
  secretAccessKey
23
23
  },
24
- region
24
+ region,
25
+ // Streamed bodies (AwsS3PutObject "url") reach the SDK in the chunks the source sends. The SDK's
26
+ // aws-chunked upload rejects any non-final chunk under 8192 bytes unless it buffers the stream.
27
+ requestStreamBufferSize: 65536
25
28
  };
26
29
  if (!type.isNone(endpoint)) {
27
30
  config.endpoint = endpoint;
package/dist/types.js CHANGED
@@ -19,8 +19,17 @@ export default {
19
19
  connections: [
20
20
  'AwsS3Bucket'
21
21
  ],
22
+ // Non-scopable: the tenant wall does not cover object storage, so its
23
+ // connections are never scoped and accept no tenant: declaration.
24
+ connectionMetas: {
25
+ AwsS3Bucket: {
26
+ tenant: false
27
+ }
28
+ },
22
29
  requests: [
30
+ 'AwsS3DeleteObject',
23
31
  'AwsS3GetObject',
32
+ 'AwsS3HeadObject',
24
33
  'AwsS3PresignedGetObject',
25
34
  'AwsS3PresignedPostPolicy',
26
35
  'AwsS3PutObject'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/plugin-aws",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "",
6
6
  "homepage": "https://lowdefy.com",
@@ -35,6 +35,9 @@
35
35
  "url": "https://github.com/lowdefy/lowdefy.git"
36
36
  },
37
37
  "type": "module",
38
+ "sideEffects": [
39
+ "**/*.css"
40
+ ],
38
41
  "exports": {
39
42
  "./connections": "./dist/connections.js",
40
43
  "./blocks": "./dist/blocks.js",
@@ -47,14 +50,16 @@
47
50
  "dist/*"
48
51
  ],
49
52
  "dependencies": {
50
- "@lowdefy/block-utils": "6.0.0",
51
- "@lowdefy/blocks-antd": "6.0.0",
52
- "@lowdefy/blocks-files": "6.0.0",
53
- "@lowdefy/errors": "6.0.0",
54
- "@lowdefy/helpers": "6.0.0",
55
- "@aws-sdk/client-s3": "3.797.0",
56
- "@aws-sdk/s3-presigned-post": "3.797.0",
57
- "@aws-sdk/s3-request-presigner": "3.797.0"
53
+ "@lowdefy/block-utils": "7.0.0",
54
+ "@lowdefy/blocks-antd": "7.0.0",
55
+ "@lowdefy/blocks-files": "7.0.0",
56
+ "@lowdefy/errors": "7.0.0",
57
+ "@lowdefy/helpers": "7.0.0",
58
+ "@lowdefy/node-utils": "7.0.0",
59
+ "@aws-sdk/client-s3": "3.1141.0",
60
+ "@aws-sdk/s3-presigned-post": "3.1141.0",
61
+ "@aws-sdk/s3-request-presigner": "3.1141.0",
62
+ "undici": "7.30.0"
58
63
  },
59
64
  "peerDependencies": {
60
65
  "antd": ">=6",
@@ -62,9 +67,10 @@
62
67
  "react-dom": ">=18"
63
68
  },
64
69
  "devDependencies": {
65
- "@lowdefy/ajv": "6.0.0",
66
- "@lowdefy/block-dev": "6.0.0",
67
- "@lowdefy/jest-yaml-transform": "6.0.0",
70
+ "@jest/globals": "28.1.3",
71
+ "@lowdefy/ajv": "7.0.0",
72
+ "@lowdefy/block-dev": "7.0.0",
73
+ "@lowdefy/jest-yaml-transform": "7.0.0",
68
74
  "@swc/cli": "0.8.1",
69
75
  "@swc/core": "1.15.32",
70
76
  "@swc/jest": "0.2.39",