@rebasepro/server 0.23.0 → 0.24.1-canary.g56af4a4
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 +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/GCSStorageController-DivE9bAx.js +402 -0
- package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
- package/dist/S3StorageController-D_vKs4kP.js +480 -0
- package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +92 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +38 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +97 -11
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +9 -7
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +28 -1
- package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
- package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
- package/dist/auth/access.d.ts +121 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +16 -1
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +2 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +46 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +71 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +120 -59
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +54 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +54 -1
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +11 -6
- package/dist/auth/token-revocation.d.ts +50 -1
- package/dist/auth/user-picking.d.ts +35 -0
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
- package/dist/auth-OPq_FeMs.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +24 -3
- package/dist/backup/backup-routes.d.ts +46 -5
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +16 -0
- package/dist/backup/index.d.ts +5 -1
- package/dist/backup-5d7XhLX3.js +581 -0
- package/dist/backup-5d7XhLX3.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +56 -4
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/static-routing.d.ts +65 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
- package/dist/contract-routes-02lASyJI.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +94 -23
- package/dist/cron/cron-store.d.ts +31 -4
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
- package/dist/cron-loader-D7OmleAb.js.map +1 -0
- package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
- package/dist/cron-routes-CBfiaqEL.js.map +1 -0
- package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
- package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
- package/dist/cron-store-B2Ih0gCN.js +540 -0
- package/dist/cron-store-B2Ih0gCN.js.map +1 -0
- package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
- package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/types.d.ts +44 -0
- package/dist/env.d.ts +1 -39
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/filter-dialect-REl-TjZr.js +1628 -0
- package/dist/filter-dialect-REl-TjZr.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
- package/dist/function-loader-BDKKiBmy.js.map +1 -0
- package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
- package/dist/function-routes-DWB7BBsi.js.map +1 -0
- package/dist/functions/context.d.ts +26 -6
- package/dist/functions/define-function.d.ts +7 -4
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +114 -37
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
- package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
- package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.es.js +23363 -17552
- package/dist/index.es.js.map +1 -1
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +20 -0
- package/dist/init.d.ts +70 -6
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
- package/dist/jobs-BpD4Q0qR.js.map +1 -0
- package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
- package/dist/jwt-Bqcu7cGt.js.map +1 -0
- package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
- package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/load-failure-UvzZgGXT.js +48 -0
- package/dist/load-failure-UvzZgGXT.js.map +1 -0
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +7 -0
- package/dist/mcp/mcp-tools.d.ts +23 -9
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +7 -1
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
- package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
- package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
- package/dist/query-parser-DK9eMkb9.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +73 -3
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +20 -5
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
- package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +16 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/soft-delete-params-47yBGs3_.js +668 -0
- package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
- package/dist/sql-export-routes-BNNuGGLP.js +240 -0
- package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
- package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +48 -2
- package/dist/storage/LocalStorageController.d.ts +69 -2
- package/dist/storage/S3StorageController.d.ts +39 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-keys.d.ts +15 -0
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +52 -14
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +76 -7
- package/dist/storage/types.d.ts +101 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-CjrA4PMo.js +0 -242
- package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
- package/dist/S3StorageController-B6pKDNVj.js +0 -264
- package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-B-GIMpDG.js.map +0 -1
- package/dist/backup-D7YR94N3.js +0 -253
- package/dist/backup-D7YR94N3.js.map +0 -1
- package/dist/contract-routes-CbFjuBwa.js.map +0 -1
- package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
- package/dist/cron-routes-eE8nif_b.js.map +0 -1
- package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
- package/dist/cron-store-TcoGz-xS.js +0 -305
- package/dist/cron-store-TcoGz-xS.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-loader-xnbDAPfa.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/jobs-CazMYhyy.js.map +0 -1
- package/dist/jwt-DnQHNFCl.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
- package/dist/openapi-generator-O_O24MAT.js.map +0 -1
- package/dist/query-parser-DGRVFNM3.js.map +0 -1
- package/dist/request-timeout-C_4C2BeR.js.map +0 -1
- package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
- package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
- package/dist/src-pmvW7BFx.js +0 -6688
- package/dist/src-pmvW7BFx.js.map +0 -1
- package/dist/src-vkcwKXbT.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
+
import { t as ApiError } from "./errors-D6_y86c5.js";
|
|
7
|
+
import { c as listingPrefix, s as folderKey } from "./keys-GAVZqbqx.js";
|
|
8
|
+
import { o as webStreamFrom, t as validateUploadedFile } from "./upload-validation-Dd-fnsbA.js";
|
|
9
|
+
//#region src/storage/S3StorageController.ts
|
|
10
|
+
var S3StorageController_exports = /* @__PURE__ */ __exportAll({ S3StorageController: () => S3StorageController });
|
|
11
|
+
var _s3Module;
|
|
12
|
+
var _presignerModule;
|
|
13
|
+
async function loadS3() {
|
|
14
|
+
if (!_s3Module) try {
|
|
15
|
+
_s3Module = await import("@aws-sdk/client-s3");
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error("@aws-sdk/client-s3 is required for S3 storage. Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner");
|
|
18
|
+
}
|
|
19
|
+
return _s3Module;
|
|
20
|
+
}
|
|
21
|
+
async function loadPresigner() {
|
|
22
|
+
if (!_presignerModule) try {
|
|
23
|
+
_presignerModule = await import("@aws-sdk/s3-request-presigner");
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("@aws-sdk/s3-request-presigner is required for S3 storage. Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner");
|
|
26
|
+
}
|
|
27
|
+
return _presignerModule;
|
|
28
|
+
}
|
|
29
|
+
/** Files up to this size are one `PutObject`; larger ones are uploaded in parts of (at least) this size. */
|
|
30
|
+
var MULTIPART_PART_SIZE = 8 * 1024 * 1024;
|
|
31
|
+
/** S3's limit on the parts of one upload. */
|
|
32
|
+
var MAX_MULTIPART_PARTS = 1e4;
|
|
33
|
+
/**
|
|
34
|
+
* S3-compatible storage implementation
|
|
35
|
+
* Works with AWS S3 and MinIO (with forcePathStyle option)
|
|
36
|
+
*/
|
|
37
|
+
var S3StorageController = class {
|
|
38
|
+
config;
|
|
39
|
+
_client;
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Lazily create and cache the S3 client on first use
|
|
45
|
+
*/
|
|
46
|
+
async getClient() {
|
|
47
|
+
if (!this._client) {
|
|
48
|
+
const s3 = await loadS3();
|
|
49
|
+
this._client = new s3.S3Client({
|
|
50
|
+
region: this.config.region || "us-east-1",
|
|
51
|
+
endpoint: this.config.endpoint,
|
|
52
|
+
forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint,
|
|
53
|
+
credentials: {
|
|
54
|
+
accessKeyId: this.config.accessKeyId,
|
|
55
|
+
secretAccessKey: this.config.secretAccessKey
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return this._client;
|
|
60
|
+
}
|
|
61
|
+
getType() {
|
|
62
|
+
return "s3";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The configured bucket, and the logical `default` that maps to it.
|
|
66
|
+
*
|
|
67
|
+
* Any other name went straight to S3 as a bucket name, so the request
|
|
68
|
+
* parameter was a way to address *any* bucket the deployment's credentials
|
|
69
|
+
* can reach — and a mistyped one came back as "file not found".
|
|
70
|
+
*/
|
|
71
|
+
knownBuckets() {
|
|
72
|
+
return ["default", this.config.bucket];
|
|
73
|
+
}
|
|
74
|
+
/** See {@link StorageController.maxFileSize}. */
|
|
75
|
+
maxFileSize() {
|
|
76
|
+
return this.config.maxFileSize ?? 52428800;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate file before upload
|
|
80
|
+
*/
|
|
81
|
+
validateFile(file) {
|
|
82
|
+
validateUploadedFile(file, this.maxFileSize(), this.config.allowedMimeTypes);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get the bucket name - either from parameter or config
|
|
86
|
+
*/
|
|
87
|
+
getBucket(bucket) {
|
|
88
|
+
if (!bucket || bucket === "default") return this.config.bucket;
|
|
89
|
+
return bucket;
|
|
90
|
+
}
|
|
91
|
+
async putObject({ file, key, metadata, bucket }, options) {
|
|
92
|
+
if (options?.uploadLimits !== false) this.validateFile(file);
|
|
93
|
+
const usedBucket = this.getBucket(bucket);
|
|
94
|
+
const s3 = await loadS3();
|
|
95
|
+
const client = await this.getClient();
|
|
96
|
+
const target = {
|
|
97
|
+
Bucket: usedBucket,
|
|
98
|
+
Key: key,
|
|
99
|
+
ContentType: file.type,
|
|
100
|
+
Metadata: metadata ? this.flattenMetadata(metadata) : void 0
|
|
101
|
+
};
|
|
102
|
+
if (file.size <= MULTIPART_PART_SIZE) await client.send(new s3.PutObjectCommand({
|
|
103
|
+
...target,
|
|
104
|
+
Body: Buffer.from(await file.arrayBuffer())
|
|
105
|
+
})).catch((error) => {
|
|
106
|
+
throw misconfiguredOr(error, usedBucket);
|
|
107
|
+
});
|
|
108
|
+
else await this.putMultipart(target, file);
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
bucket: usedBucket,
|
|
112
|
+
storageUrl: `s3://${usedBucket}/${key}`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Upload a large file in parts, one part in memory at a time.
|
|
117
|
+
*
|
|
118
|
+
* Reading the whole file into one buffer held it twice over (the
|
|
119
|
+
* `arrayBuffer()`, then the SDK's view and the multipart parser's copy
|
|
120
|
+
* before it), and `PutObject` stops at 5 GB regardless. A slice of a
|
|
121
|
+
* `File` reads only that range — from disk, for the disk-backed files TUS
|
|
122
|
+
* and backups hand over — so memory is one part whatever the file's size.
|
|
123
|
+
* A failure aborts the upload: no partial object appears at the key, and
|
|
124
|
+
* no orphaned parts are kept (and billed).
|
|
125
|
+
*/
|
|
126
|
+
async putMultipart(target, file) {
|
|
127
|
+
const s3 = await loadS3();
|
|
128
|
+
const client = await this.getClient();
|
|
129
|
+
const partSize = Math.max(MULTIPART_PART_SIZE, Math.ceil(file.size / MAX_MULTIPART_PARTS));
|
|
130
|
+
const { UploadId } = await client.send(new s3.CreateMultipartUploadCommand(target)).catch((error) => {
|
|
131
|
+
throw misconfiguredOr(error, target.Bucket);
|
|
132
|
+
});
|
|
133
|
+
if (!UploadId) throw new Error(`S3 did not start a multipart upload for ${target.Key}.`);
|
|
134
|
+
const { Bucket, Key } = target;
|
|
135
|
+
try {
|
|
136
|
+
const parts = [];
|
|
137
|
+
for (let offset = 0, PartNumber = 1; offset < file.size; offset += partSize, PartNumber++) {
|
|
138
|
+
const Body = Buffer.from(await file.slice(offset, Math.min(offset + partSize, file.size)).arrayBuffer());
|
|
139
|
+
const { ETag } = await client.send(new s3.UploadPartCommand({
|
|
140
|
+
Bucket,
|
|
141
|
+
Key,
|
|
142
|
+
UploadId,
|
|
143
|
+
PartNumber,
|
|
144
|
+
Body
|
|
145
|
+
}));
|
|
146
|
+
parts.push({
|
|
147
|
+
ETag,
|
|
148
|
+
PartNumber
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
await client.send(new s3.CompleteMultipartUploadCommand({
|
|
152
|
+
Bucket,
|
|
153
|
+
Key,
|
|
154
|
+
UploadId,
|
|
155
|
+
MultipartUpload: { Parts: parts }
|
|
156
|
+
}));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
await client.send(new s3.AbortMultipartUploadCommand({
|
|
159
|
+
Bucket,
|
|
160
|
+
Key,
|
|
161
|
+
UploadId
|
|
162
|
+
})).catch(() => void 0);
|
|
163
|
+
throw misconfiguredOr(error, Bucket);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Flatten nested metadata to string values (S3 requirement)
|
|
168
|
+
*/
|
|
169
|
+
flattenMetadata(metadata) {
|
|
170
|
+
const flattened = {};
|
|
171
|
+
for (const [key, value] of Object.entries(metadata)) if (typeof value === "string") flattened[key] = value;
|
|
172
|
+
else if (value !== void 0 && value !== null) flattened[key] = JSON.stringify(value);
|
|
173
|
+
return flattened;
|
|
174
|
+
}
|
|
175
|
+
async getSignedUrl(key, bucket) {
|
|
176
|
+
let resolvedPath = key;
|
|
177
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
178
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
179
|
+
if (match) {
|
|
180
|
+
const protocolLength = match[0].length;
|
|
181
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
182
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
183
|
+
if (firstSlash > 0) {
|
|
184
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
185
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const s3 = await loadS3();
|
|
190
|
+
const presigner = await loadPresigner();
|
|
191
|
+
const client = await this.getClient();
|
|
192
|
+
const headCommand = new s3.HeadObjectCommand({
|
|
193
|
+
Bucket: resolvedBucket,
|
|
194
|
+
Key: resolvedPath
|
|
195
|
+
});
|
|
196
|
+
const headResult = await client.send(headCommand);
|
|
197
|
+
const getCommand = new s3.GetObjectCommand({
|
|
198
|
+
Bucket: resolvedBucket,
|
|
199
|
+
Key: resolvedPath
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
url: await presigner.getSignedUrl(client, getCommand, { expiresIn: 3600 }),
|
|
203
|
+
metadata: {
|
|
204
|
+
bucket: resolvedBucket,
|
|
205
|
+
fullPath: resolvedPath,
|
|
206
|
+
name: resolvedPath.split("/").pop() || resolvedPath,
|
|
207
|
+
size: headResult.ContentLength || 0,
|
|
208
|
+
contentType: headResult.ContentType || "application/octet-stream",
|
|
209
|
+
customMetadata: headResult.Metadata || {}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (isS3NotFound(error)) return {
|
|
214
|
+
url: null,
|
|
215
|
+
fileNotFound: true
|
|
216
|
+
};
|
|
217
|
+
throw misconfiguredOr(error, resolvedBucket);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async getObject(key, bucket) {
|
|
221
|
+
let resolvedPath = key;
|
|
222
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
223
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
224
|
+
if (match) {
|
|
225
|
+
const protocolLength = match[0].length;
|
|
226
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
227
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
228
|
+
if (firstSlash > 0) {
|
|
229
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
230
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const s3 = await loadS3();
|
|
235
|
+
const client = await this.getClient();
|
|
236
|
+
const command = new s3.GetObjectCommand({
|
|
237
|
+
Bucket: resolvedBucket,
|
|
238
|
+
Key: resolvedPath
|
|
239
|
+
});
|
|
240
|
+
const response = await client.send(command);
|
|
241
|
+
if (!response.Body) return null;
|
|
242
|
+
const chunks = [];
|
|
243
|
+
for await (const chunk of response.Body) chunks.push(chunk);
|
|
244
|
+
const buffer = Buffer.concat(chunks);
|
|
245
|
+
const contentType = response.ContentType || "application/octet-stream";
|
|
246
|
+
const fileName = resolvedPath.split("/").pop() || resolvedPath;
|
|
247
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
248
|
+
return new File([blob], fileName, {
|
|
249
|
+
type: contentType,
|
|
250
|
+
lastModified: response.LastModified?.getTime()
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (isS3NotFound(error)) return null;
|
|
254
|
+
throw misconfiguredOr(error, resolvedBucket);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The bucket and key a call addresses: an `s3://` or `gs://` URL names its
|
|
259
|
+
* own bucket, anything else is a key in the given (or configured) bucket.
|
|
260
|
+
*/
|
|
261
|
+
resolveLocation(key, bucket) {
|
|
262
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
263
|
+
if (match) {
|
|
264
|
+
const withoutProtocol = key.substring(match[0].length);
|
|
265
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
266
|
+
if (firstSlash > 0) return {
|
|
267
|
+
bucket: withoutProtocol.substring(0, firstSlash),
|
|
268
|
+
key: withoutProtocol.substring(firstSlash + 1)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
bucket: this.getBucket(bucket),
|
|
273
|
+
key
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* See {@link StorageController.getMetadata}. One `HeadObject`. Without it,
|
|
278
|
+
* `GET /metadata/*` fell back to `getSignedUrl`, which signed a URL per
|
|
279
|
+
* call — ~134 µs of CPU — only for the route to throw it away.
|
|
280
|
+
*/
|
|
281
|
+
async getMetadata(key, bucket) {
|
|
282
|
+
const location = this.resolveLocation(key, bucket);
|
|
283
|
+
const s3 = await loadS3();
|
|
284
|
+
const client = await this.getClient();
|
|
285
|
+
try {
|
|
286
|
+
const head = await client.send(new s3.HeadObjectCommand({
|
|
287
|
+
Bucket: location.bucket,
|
|
288
|
+
Key: location.key
|
|
289
|
+
}));
|
|
290
|
+
return {
|
|
291
|
+
bucket: location.bucket,
|
|
292
|
+
fullPath: location.key,
|
|
293
|
+
name: location.key.split("/").pop() || location.key,
|
|
294
|
+
size: head.ContentLength || 0,
|
|
295
|
+
contentType: head.ContentType || "application/octet-stream",
|
|
296
|
+
customMetadata: head.Metadata || {}
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (isS3NotFound(error)) return null;
|
|
300
|
+
throw misconfiguredOr(error, location.bucket);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** See {@link StorageController.statObject}. One `HeadObject`; no body is read. */
|
|
304
|
+
async statObject(key, bucket) {
|
|
305
|
+
const location = this.resolveLocation(key, bucket);
|
|
306
|
+
const s3 = await loadS3();
|
|
307
|
+
const client = await this.getClient();
|
|
308
|
+
try {
|
|
309
|
+
const head = await client.send(new s3.HeadObjectCommand({
|
|
310
|
+
Bucket: location.bucket,
|
|
311
|
+
Key: location.key
|
|
312
|
+
}));
|
|
313
|
+
return {
|
|
314
|
+
size: head.ContentLength ?? 0,
|
|
315
|
+
contentType: head.ContentType || "application/octet-stream",
|
|
316
|
+
lastModified: head.LastModified?.getTime() ?? Date.now()
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (isS3NotFound(error)) return null;
|
|
320
|
+
throw misconfiguredOr(error, location.bucket);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* See {@link StorageController.getObjectStream}. One `GetObject`, with a
|
|
325
|
+
* `Range` when one is asked for, so only those bytes leave the bucket; the
|
|
326
|
+
* body is handed on as it arrives rather than collected first.
|
|
327
|
+
*/
|
|
328
|
+
async getObjectStream(key, options) {
|
|
329
|
+
const location = this.resolveLocation(key, options?.bucket);
|
|
330
|
+
const range = options?.range;
|
|
331
|
+
const s3 = await loadS3();
|
|
332
|
+
const client = await this.getClient();
|
|
333
|
+
let response;
|
|
334
|
+
try {
|
|
335
|
+
response = await client.send(new s3.GetObjectCommand({
|
|
336
|
+
Bucket: location.bucket,
|
|
337
|
+
Key: location.key,
|
|
338
|
+
...range ? { Range: `bytes=${range.start}-${range.end}` } : {}
|
|
339
|
+
}));
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (isS3NotFound(error)) return null;
|
|
342
|
+
if (range && isS3RangeNotSatisfiable(error)) return this.getObjectStream(key, { bucket: options?.bucket });
|
|
343
|
+
throw misconfiguredOr(error, location.bucket);
|
|
344
|
+
}
|
|
345
|
+
const body = response.Body;
|
|
346
|
+
if (!body) return null;
|
|
347
|
+
const total = range ? /\/(\d+)\s*$/.exec(response.ContentRange ?? "")?.[1] : void 0;
|
|
348
|
+
const served = range && total !== void 0 ? range : void 0;
|
|
349
|
+
return {
|
|
350
|
+
stream: isByteIterable(body) ? webStreamFrom(body) : body.transformToWebStream(),
|
|
351
|
+
size: total !== void 0 ? Number(total) : response.ContentLength ?? 0,
|
|
352
|
+
contentType: response.ContentType || "application/octet-stream",
|
|
353
|
+
lastModified: response.LastModified?.getTime() ?? Date.now(),
|
|
354
|
+
...served ? { range: served } : {}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async deleteObject(key, bucket) {
|
|
358
|
+
let resolvedPath = key;
|
|
359
|
+
let resolvedBucket = this.getBucket(bucket);
|
|
360
|
+
const match = key.match(/^(s3|gs):\/\//);
|
|
361
|
+
if (match) {
|
|
362
|
+
const protocolLength = match[0].length;
|
|
363
|
+
const withoutProtocol = key.substring(protocolLength);
|
|
364
|
+
const firstSlash = withoutProtocol.indexOf("/");
|
|
365
|
+
if (firstSlash > 0) {
|
|
366
|
+
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
367
|
+
resolvedPath = withoutProtocol.substring(firstSlash + 1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const s3 = await loadS3();
|
|
371
|
+
const client = await this.getClient();
|
|
372
|
+
const command = new s3.DeleteObjectCommand({
|
|
373
|
+
Bucket: resolvedBucket,
|
|
374
|
+
Key: resolvedPath
|
|
375
|
+
});
|
|
376
|
+
try {
|
|
377
|
+
await client.send(command);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (isS3NotFound(error)) return;
|
|
380
|
+
throw misconfiguredOr(error, resolvedBucket);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async listObjects(prefix, options) {
|
|
384
|
+
const resolvedBucket = this.getBucket(options?.bucket);
|
|
385
|
+
const s3 = await loadS3();
|
|
386
|
+
const client = await this.getClient();
|
|
387
|
+
const folderPrefix = listingPrefix(prefix);
|
|
388
|
+
const command = new s3.ListObjectsV2Command({
|
|
389
|
+
Bucket: resolvedBucket,
|
|
390
|
+
Prefix: folderPrefix,
|
|
391
|
+
MaxKeys: options?.maxResults ?? 1e3,
|
|
392
|
+
ContinuationToken: options?.pageToken,
|
|
393
|
+
Delimiter: "/"
|
|
394
|
+
});
|
|
395
|
+
const response = await client.send(command).catch((error) => {
|
|
396
|
+
throw misconfiguredOr(error, resolvedBucket);
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
items: (response.Contents || []).filter((obj) => obj.Key !== folderPrefix).map((obj) => ({
|
|
400
|
+
bucket: resolvedBucket,
|
|
401
|
+
fullPath: obj.Key || "",
|
|
402
|
+
name: (obj.Key || "").split("/").pop() || "",
|
|
403
|
+
...typeof obj.Size === "number" ? { size: obj.Size } : {},
|
|
404
|
+
...obj.LastModified instanceof Date ? { updated: obj.LastModified.toISOString() } : {},
|
|
405
|
+
parent: null,
|
|
406
|
+
root: null,
|
|
407
|
+
toString: () => `s3://${resolvedBucket}/${obj.Key}`
|
|
408
|
+
})),
|
|
409
|
+
prefixes: (response.CommonPrefixes || []).map((common) => {
|
|
410
|
+
const folder = folderKey(common.Prefix || "");
|
|
411
|
+
return {
|
|
412
|
+
bucket: resolvedBucket,
|
|
413
|
+
fullPath: folder,
|
|
414
|
+
name: folder.split("/").pop() || "",
|
|
415
|
+
parent: null,
|
|
416
|
+
root: null,
|
|
417
|
+
toString: () => `s3://${resolvedBucket}/${folder}`
|
|
418
|
+
};
|
|
419
|
+
}),
|
|
420
|
+
nextPageToken: response.NextContinuationToken
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
/** The error name and HTTP status an SDK error carries, read without assuming its shape. */
|
|
425
|
+
function s3ErrorFacts(error) {
|
|
426
|
+
if (typeof error !== "object" || error === null) return {};
|
|
427
|
+
const name = "name" in error && typeof error.name === "string" ? error.name : void 0;
|
|
428
|
+
const meta = "$metadata" in error ? error.$metadata : void 0;
|
|
429
|
+
return {
|
|
430
|
+
name,
|
|
431
|
+
status: typeof meta === "object" && meta !== null && "httpStatusCode" in meta && typeof meta.httpStatusCode === "number" ? meta.httpStatusCode : void 0
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* A missing object, as `GetObject` (`NoSuchKey`) and `HeadObject` (`NotFound`, no body) report it.
|
|
436
|
+
*
|
|
437
|
+
* Not a missing bucket, which is a 404 too: read as a missing object, a typo in
|
|
438
|
+
* `S3_BUCKET` made every file of the deployment "not found", and a delete
|
|
439
|
+
* report success. (A `HeadObject` on a missing bucket carries no error name, so
|
|
440
|
+
* that one call still cannot tell the two apart.)
|
|
441
|
+
*/
|
|
442
|
+
function isS3NotFound(error) {
|
|
443
|
+
const { name, status } = s3ErrorFacts(error);
|
|
444
|
+
if (name === "NoSuchBucket") return false;
|
|
445
|
+
return name === "NoSuchKey" || name === "NotFound" || status === 404;
|
|
446
|
+
}
|
|
447
|
+
/** What S3 answers when the deployment's configuration, not the request, is wrong. */
|
|
448
|
+
var MISCONFIGURATION_ERRORS = /* @__PURE__ */ new Set([
|
|
449
|
+
"NoSuchBucket",
|
|
450
|
+
"AccessDenied",
|
|
451
|
+
"InvalidAccessKeyId",
|
|
452
|
+
"SignatureDoesNotMatch"
|
|
453
|
+
]);
|
|
454
|
+
/**
|
|
455
|
+
* A provider error that says this deployment's storage is misconfigured, as a
|
|
456
|
+
* `502 STORAGE_MISCONFIGURED` naming the bucket — or the error as it was.
|
|
457
|
+
*
|
|
458
|
+
* These used to reach the caller as a bare 500 "Internal Server Error", with
|
|
459
|
+
* nothing to say the bucket or its credentials were the problem.
|
|
460
|
+
*/
|
|
461
|
+
function misconfiguredOr(error, bucket) {
|
|
462
|
+
const { name } = s3ErrorFacts(error);
|
|
463
|
+
if (!name || !MISCONFIGURATION_ERRORS.has(name)) return error;
|
|
464
|
+
return new ApiError(502, "STORAGE_MISCONFIGURED", name === "NoSuchBucket" ? `The storage bucket "${bucket}" does not exist. Check the bucket name this source is configured with (S3_BUCKET, or S3_BUCKET__<KEY> for a named source).` : `The storage provider refused this deployment's credentials for bucket "${bucket}" (${name}). Check the access key and its permissions on the bucket.`, {
|
|
465
|
+
bucket,
|
|
466
|
+
reason: name
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function isS3RangeNotSatisfiable(error) {
|
|
470
|
+
const { name, status } = s3ErrorFacts(error);
|
|
471
|
+
return name === "InvalidRange" || status === 416;
|
|
472
|
+
}
|
|
473
|
+
/** A Node response body — what the SDK hands over on Node — rather than a browser one. */
|
|
474
|
+
function isByteIterable(value) {
|
|
475
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
export { S3StorageController_exports as n, S3StorageController as t };
|
|
479
|
+
|
|
480
|
+
//# sourceMappingURL=S3StorageController-D_vKs4kP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"S3StorageController-D_vKs4kP.js","names":[],"sources":["../src/storage/S3StorageController.ts"],"sourcesContent":["/**\n * S3-compatible storage controller (works with AWS S3 and MinIO)\n */\n\nimport type { S3Client as S3ClientType, } from \"@aws-sdk/client-s3\";\nimport {\n DEFAULT_MAX_FILE_SIZE,\n S3StorageConfig,\n StorageController,\n type StoredObjectInfo,\n type StoredObjectRange,\n type PutObjectOptions,\n type StoredObjectStream\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport { webStreamFrom } from \"./streams\";\nimport { ApiError } from \"../api/errors\";\nimport { validateUploadedFile } from \"./upload-validation\";\nimport {\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference,\n UploadFileProps,\n UploadFileResult\n} from \"@rebasepro/types\";\n\nlet _s3Module: typeof import(\"@aws-sdk/client-s3\") | undefined;\nlet _presignerModule: typeof import(\"@aws-sdk/s3-request-presigner\") | undefined;\n\nasync function loadS3() {\n if (!_s3Module) {\n try {\n _s3Module = await import(\"@aws-sdk/client-s3\");\n } catch {\n throw new Error(\n \"@aws-sdk/client-s3 is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _s3Module;\n}\n\nasync function loadPresigner() {\n if (!_presignerModule) {\n try {\n _presignerModule = await import(\"@aws-sdk/s3-request-presigner\");\n } catch {\n throw new Error(\n \"@aws-sdk/s3-request-presigner is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _presignerModule;\n}\n\n/** Files up to this size are one `PutObject`; larger ones are uploaded in parts of (at least) this size. */\nconst MULTIPART_PART_SIZE = 8 * 1024 * 1024;\n/** S3's limit on the parts of one upload. */\nconst MAX_MULTIPART_PARTS = 10_000;\n\n/**\n * S3-compatible storage implementation\n * Works with AWS S3 and MinIO (with forcePathStyle option)\n */\nexport class S3StorageController implements StorageController {\n private config: S3StorageConfig;\n private _client: S3ClientType | undefined;\n\n constructor(config: S3StorageConfig) {\n this.config = config;\n }\n\n /**\n * Lazily create and cache the S3 client on first use\n */\n private async getClient(): Promise<S3ClientType> {\n if (!this._client) {\n const s3 = await loadS3();\n this._client = new s3.S3Client({\n region: this.config.region || \"us-east-1\",\n endpoint: this.config.endpoint,\n forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint, // Auto-enable for custom endpoints (MinIO)\n credentials: {\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey\n }\n });\n }\n return this._client;\n }\n\n getType(): \"s3\" {\n return \"s3\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n *\n * Any other name went straight to S3 as a bucket name, so the request\n * parameter was a way to address *any* bucket the deployment's credentials\n * can reach — and a mistyped one came back as \"file not found\".\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n /** See {@link StorageController.maxFileSize}. */\n maxFileSize(): number {\n return this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n validateUploadedFile(file, this.maxFileSize(), this.config.allowedMimeTypes);\n }\n\n /**\n * Get the bucket name - either from parameter or config\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for S3 it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult> {\n if (options?.uploadLimits !== false) this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n const target = {\n Bucket: usedBucket,\n Key: key,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n };\n\n if (file.size <= MULTIPART_PART_SIZE) {\n // One request from one buffer, as always: a small file is no bigger\n // than a part. `Buffer.from(arrayBuffer)` is a view, not a copy.\n await client.send(new s3.PutObjectCommand({ ...target, Body: Buffer.from(await file.arrayBuffer()) }))\n .catch((error: unknown) => { throw misconfiguredOr(error, usedBucket); });\n } else {\n await this.putMultipart(target, file);\n }\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\n };\n }\n\n /**\n * Upload a large file in parts, one part in memory at a time.\n *\n * Reading the whole file into one buffer held it twice over (the\n * `arrayBuffer()`, then the SDK's view and the multipart parser's copy\n * before it), and `PutObject` stops at 5 GB regardless. A slice of a\n * `File` reads only that range — from disk, for the disk-backed files TUS\n * and backups hand over — so memory is one part whatever the file's size.\n * A failure aborts the upload: no partial object appears at the key, and\n * no orphaned parts are kept (and billed).\n */\n private async putMultipart(\n target: { Bucket: string; Key: string; ContentType: string; Metadata?: Record<string, string> },\n file: Blob\n ): Promise<void> {\n const s3 = await loadS3();\n const client = await this.getClient();\n // At most 10,000 parts: a part grows past 8 MiB only beyond 80 GB.\n const partSize = Math.max(MULTIPART_PART_SIZE, Math.ceil(file.size / MAX_MULTIPART_PARTS));\n const { UploadId } = await client.send(new s3.CreateMultipartUploadCommand(target))\n .catch((error: unknown) => { throw misconfiguredOr(error, target.Bucket); });\n if (!UploadId) throw new Error(`S3 did not start a multipart upload for ${target.Key}.`);\n const { Bucket, Key } = target;\n try {\n const parts: { ETag?: string; PartNumber: number }[] = [];\n for (let offset = 0, PartNumber = 1; offset < file.size; offset += partSize, PartNumber++) {\n const Body = Buffer.from(await file.slice(offset, Math.min(offset + partSize, file.size)).arrayBuffer());\n const { ETag } = await client.send(new s3.UploadPartCommand({ Bucket, Key, UploadId, PartNumber, Body }));\n parts.push({ ETag, PartNumber });\n }\n await client.send(new s3.CompleteMultipartUploadCommand({ Bucket, Key, UploadId, MultipartUpload: { Parts: parts } }));\n } catch (error: unknown) {\n await client.send(new s3.AbortMultipartUploadCommand({ Bucket, Key, UploadId })).catch(() => undefined);\n throw misconfiguredOr(error, Bucket);\n }\n }\n\n /**\n * Flatten nested metadata to string values (S3 requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle s3:// and gs:// URLs for backward compatibility\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const presigner = await loadPresigner();\n const client = await this.getClient();\n\n // First check if the object exists and get metadata\n const headCommand = new s3.HeadObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const headResult = await client.send(headCommand);\n\n // Generate a signed URL\n const getCommand = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Good for an hour. Server-side callers only: no route hands it out.\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn: 3600 });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: headResult.ContentLength || 0,\n contentType: headResult.ContentType || \"application/octet-stream\",\n customMetadata: headResult.Metadata || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const response = await client.send(command);\n\n if (!response.Body) {\n return null;\n }\n\n // Convert stream to buffer\n const chunks: Uint8Array[] = [];\n for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n const contentType = response.ContentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([buffer], { type: contentType });\n // The object's own version. Left out, `File` stamps `Date.now()`,\n // and the routes build the `ETag`, `Last-Modified` and the\n // transform cache key from this — so every read was a new version:\n // no 304, no cache hit, and a fresh rendition written per request.\n return new File([blob], fileName, {\n type: contentType,\n lastModified: response.LastModified?.getTime()\n });\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return null;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n /**\n * The bucket and key a call addresses: an `s3://` or `gs://` URL names its\n * own bucket, anything else is a key in the given (or configured) bucket.\n */\n private resolveLocation(key: string, bucket?: string): { bucket: string; key: string } {\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const withoutProtocol = key.substring(match[0].length);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n return { bucket: withoutProtocol.substring(0, firstSlash), key: withoutProtocol.substring(firstSlash + 1) };\n }\n }\n return { bucket: this.getBucket(bucket), key };\n }\n\n /**\n * See {@link StorageController.getMetadata}. One `HeadObject`. Without it,\n * `GET /metadata/*` fell back to `getSignedUrl`, which signed a URL per\n * call — ~134 µs of CPU — only for the route to throw it away.\n */\n async getMetadata(key: string, bucket?: string): Promise<DownloadMetadata | null> {\n const location = this.resolveLocation(key, bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n try {\n const head = await client.send(new s3.HeadObjectCommand({ Bucket: location.bucket, Key: location.key }));\n return {\n bucket: location.bucket,\n fullPath: location.key,\n name: location.key.split(\"/\").pop() || location.key,\n size: head.ContentLength || 0,\n contentType: head.ContentType || \"application/octet-stream\",\n customMetadata: head.Metadata || {}\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /** See {@link StorageController.statObject}. One `HeadObject`; no body is read. */\n async statObject(key: string, bucket?: string): Promise<StoredObjectInfo | null> {\n const location = this.resolveLocation(key, bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n try {\n const head = await client.send(new s3.HeadObjectCommand({ Bucket: location.bucket, Key: location.key }));\n return {\n size: head.ContentLength ?? 0,\n contentType: head.ContentType || \"application/octet-stream\",\n // The object's version — see `getObject` on why a missing one\n // must not become a stable value.\n lastModified: head.LastModified?.getTime() ?? Date.now()\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /**\n * See {@link StorageController.getObjectStream}. One `GetObject`, with a\n * `Range` when one is asked for, so only those bytes leave the bucket; the\n * body is handed on as it arrives rather than collected first.\n */\n async getObjectStream(\n key: string,\n options?: { bucket?: string; range?: StoredObjectRange }\n ): Promise<StoredObjectStream | null> {\n const location = this.resolveLocation(key, options?.bucket);\n const range = options?.range;\n const s3 = await loadS3();\n const client = await this.getClient();\n let response;\n try {\n response = await client.send(new s3.GetObjectCommand({\n Bucket: location.bucket,\n Key: location.key,\n ...(range ? { Range: `bytes=${range.start}-${range.end}` } : {})\n }));\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n // The object shrank below the range since it was described. Read\n // it whole: the caller serves a whole object as a 200, which HTTP\n // allows for any range request.\n if (range && isS3RangeNotSatisfiable(error)) return this.getObjectStream(key, { bucket: options?.bucket });\n throw misconfiguredOr(error, location.bucket);\n }\n const body = response.Body;\n if (!body) return null;\n\n // A ranged answer says the whole object's size after the slash of its\n // `Content-Range`. A store that ignored the `Range` sent no\n // `Content-Range` and the whole object, which is what this then says.\n const total = range ? /\\/(\\d+)\\s*$/.exec(response.ContentRange ?? \"\")?.[1] : undefined;\n const served = range && total !== undefined ? range : undefined;\n return {\n stream: isByteIterable(body) ? webStreamFrom(body) : body.transformToWebStream(),\n size: total !== undefined ? Number(total) : response.ContentLength ?? 0,\n contentType: response.ContentType || \"application/octet-stream\",\n lastModified: response.LastModified?.getTime() ?? Date.now(),\n ...(served ? { range: served } : {})\n };\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.DeleteObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Deleting something that is already gone is the outcome the caller\n // wanted, so it is not an error. AWS itself answers DeleteObject with\n // 204 for a key that never existed, but S3-compatible backends (MinIO,\n // Ceph, R2 in some configurations) do raise NoSuchKey — and the local\n // controller has always returned quietly in this case. Without this,\n // which of those a deployment happens to run decides whether\n // `DELETE /api/storage/:path` on a stale reference is a 200 or a 500.\n try {\n await client.send(command);\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const folderPrefix = listingPrefix(prefix);\n const command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: folderPrefix,\n MaxKeys: options?.maxResults ?? 1000,\n ContinuationToken: options?.pageToken,\n Delimiter: \"/\" // This gives us folder-like behavior\n });\n\n const response = await client.send(command)\n .catch((error: unknown) => { throw misconfiguredOr(error, resolvedBucket); });\n\n // The folder's own marker — the zero-byte `folder/` object that\n // `POST /folder` writes — is in `Contents` when the listed prefix is\n // that folder. It is how the folder exists, not something in it, and\n // listed it is an entry with an empty name. Local storage has no such\n // object to list.\n const objects = (response.Contents || []).filter(obj => obj.Key !== folderPrefix);\n const items: StorageReference[] = objects.map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\n // What the listing already carries, so a folder can be shown\n // without a HEAD per file. ListObjectsV2 has no content type.\n ...(typeof obj.Size === \"number\" ? { size: obj.Size } : {}),\n ...(obj.LastModified instanceof Date ? { updated: obj.LastModified.toISOString() } : {}),\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${obj.Key}`\n }));\n\n const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(common => {\n // Without the trailing slash S3 puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(common.Prefix || \"\");\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${folder}`\n };\n });\n\n return {\n items,\n prefixes,\n nextPageToken: response.NextContinuationToken\n };\n }\n}\n\n/** The error name and HTTP status an SDK error carries, read without assuming its shape. */\nfunction s3ErrorFacts(error: unknown): { name?: string; status?: number } {\n if (typeof error !== \"object\" || error === null) return {};\n const name = \"name\" in error && typeof error.name === \"string\" ? error.name : undefined;\n const meta = \"$metadata\" in error ? error.$metadata : undefined;\n const status = typeof meta === \"object\" && meta !== null && \"httpStatusCode\" in meta && typeof meta.httpStatusCode === \"number\"\n ? meta.httpStatusCode\n : undefined;\n return { name, status };\n}\n\n/**\n * A missing object, as `GetObject` (`NoSuchKey`) and `HeadObject` (`NotFound`, no body) report it.\n *\n * Not a missing bucket, which is a 404 too: read as a missing object, a typo in\n * `S3_BUCKET` made every file of the deployment \"not found\", and a delete\n * report success. (A `HeadObject` on a missing bucket carries no error name, so\n * that one call still cannot tell the two apart.)\n */\nfunction isS3NotFound(error: unknown): boolean {\n const { name, status } = s3ErrorFacts(error);\n if (name === \"NoSuchBucket\") return false;\n return name === \"NoSuchKey\" || name === \"NotFound\" || status === 404;\n}\n\n/** What S3 answers when the deployment's configuration, not the request, is wrong. */\nconst MISCONFIGURATION_ERRORS = new Set([\"NoSuchBucket\", \"AccessDenied\", \"InvalidAccessKeyId\", \"SignatureDoesNotMatch\"]);\n\n/**\n * A provider error that says this deployment's storage is misconfigured, as a\n * `502 STORAGE_MISCONFIGURED` naming the bucket — or the error as it was.\n *\n * These used to reach the caller as a bare 500 \"Internal Server Error\", with\n * nothing to say the bucket or its credentials were the problem.\n */\nfunction misconfiguredOr(error: unknown, bucket: string): unknown {\n const { name } = s3ErrorFacts(error);\n if (!name || !MISCONFIGURATION_ERRORS.has(name)) return error;\n return new ApiError(\n 502,\n \"STORAGE_MISCONFIGURED\",\n name === \"NoSuchBucket\"\n ? `The storage bucket \"${bucket}\" does not exist. Check the bucket name this source is configured with (S3_BUCKET, or S3_BUCKET__<KEY> for a named source).`\n : `The storage provider refused this deployment's credentials for bucket \"${bucket}\" (${name}). Check the access key and its permissions on the bucket.`,\n { bucket, reason: name }\n );\n}\n\nfunction isS3RangeNotSatisfiable(error: unknown): boolean {\n const { name, status } = s3ErrorFacts(error);\n return name === \"InvalidRange\" || status === 416;\n}\n\n/** A Node response body — what the SDK hands over on Node — rather than a browser one. */\nfunction isByteIterable(value: unknown): value is AsyncIterable<Uint8Array> {\n return typeof value === \"object\" && value !== null && Symbol.asyncIterator in value;\n}\n"],"mappings":";;;;;;;;;;AA2BA,IAAI;AACJ,IAAI;AAEJ,eAAe,SAAS;CACpB,IAAI,CAAC,WACD,IAAI;EACA,YAAY,MAAM,OAAO;CAC7B,QAAQ;EACJ,MAAM,IAAI,MACN,sHAEJ;CACJ;CAEJ,OAAO;AACX;AAEA,eAAe,gBAAgB;CAC3B,IAAI,CAAC,kBACD,IAAI;EACA,mBAAmB,MAAM,OAAO;CACpC,QAAQ;EACJ,MAAM,IAAI,MACN,iIAEJ;CACJ;CAEJ,OAAO;AACX;;AAGA,IAAM,sBAAsB,IAAI,OAAO;;AAEvC,IAAM,sBAAsB;;;;;AAM5B,IAAa,sBAAb,MAA8D;CAC1D;CACA;CAEA,YAAY,QAAyB;EACjC,KAAK,SAAS;CAClB;;;;CAKA,MAAc,YAAmC;EAC7C,IAAI,CAAC,KAAK,SAAS;GACf,MAAM,KAAK,MAAM,OAAO;GACxB,KAAK,UAAU,IAAI,GAAG,SAAS;IAC3B,QAAQ,KAAK,OAAO,UAAU;IAC9B,UAAU,KAAK,OAAO;IACtB,gBAAgB,KAAK,OAAO,kBAAkB,CAAC,CAAC,KAAK,OAAO;IAC5D,aAAa;KACT,aAAa,KAAK,OAAO;KACzB,iBAAiB,KAAK,OAAO;IACjC;GACJ,CAAC;EACL;EACA,OAAO,KAAK;CAChB;CAEA,UAAgB;EACZ,OAAO;CACX;;;;;;;;CASA,eAAyB;EACrB,OAAO,CAAC,WAAW,KAAK,OAAO,MAAM;CACzC;;CAGA,cAAsB;EAClB,OAAO,KAAK,OAAO,eAAA;CACvB;;;;CAKA,aAAqB,MAAkB;EACnC,qBAAqB,MAAM,KAAK,YAAY,GAAG,KAAK,OAAO,gBAAgB;CAC/E;;;;CAKA,UAAkB,QAAyB;EAGvC,IAAI,CAAC,UAAU,WAAW,WAAW,OAAO,KAAK,OAAO;EACxD,OAAO;CACX;CAEA,MAAM,UAAU,EACZ,MACA,KACA,UACA,UACgB,SAAuD;EACvE,IAAI,SAAS,iBAAiB,OAAO,KAAK,aAAa,IAAI;EAE3D,MAAM,aAAa,KAAK,UAAU,MAAM;EACxC,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EACpC,MAAM,SAAS;GACX,QAAQ;GACR,KAAK;GACL,aAAa,KAAK;GAClB,UAAU,WAAW,KAAK,gBAAgB,QAAQ,IAAI,KAAA;EAC1D;EAEA,IAAI,KAAK,QAAQ,qBAGb,MAAM,OAAO,KAAK,IAAI,GAAG,iBAAiB;GAAE,GAAG;GAAQ,MAAM,OAAO,KAAK,MAAM,KAAK,YAAY,CAAC;EAAE,CAAC,CAAC,CAAC,CACjG,OAAO,UAAmB;GAAE,MAAM,gBAAgB,OAAO,UAAU;EAAG,CAAC;OAE5E,MAAM,KAAK,aAAa,QAAQ,IAAI;EAGxC,OAAO;GACH;GACA,QAAQ;GACR,YAAY,QAAQ,WAAW,GAAG;EACtC;CACJ;;;;;;;;;;;;CAaA,MAAc,aACV,QACA,MACa;EACb,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EAEpC,MAAM,WAAW,KAAK,IAAI,qBAAqB,KAAK,KAAK,KAAK,OAAO,mBAAmB,CAAC;EACzF,MAAM,EAAE,aAAa,MAAM,OAAO,KAAK,IAAI,GAAG,6BAA6B,MAAM,CAAC,CAAC,CAC9E,OAAO,UAAmB;GAAE,MAAM,gBAAgB,OAAO,OAAO,MAAM;EAAG,CAAC;EAC/E,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,2CAA2C,OAAO,IAAI,EAAE;EACvF,MAAM,EAAE,QAAQ,QAAQ;EACxB,IAAI;GACA,MAAM,QAAiD,CAAC;GACxD,KAAK,IAAI,SAAS,GAAG,aAAa,GAAG,SAAS,KAAK,MAAM,UAAU,UAAU,cAAc;IACvF,MAAM,OAAO,OAAO,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,IAAI,SAAS,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC;IACvG,MAAM,EAAE,SAAS,MAAM,OAAO,KAAK,IAAI,GAAG,kBAAkB;KAAE;KAAQ;KAAK;KAAU;KAAY;IAAK,CAAC,CAAC;IACxG,MAAM,KAAK;KAAE;KAAM;IAAW,CAAC;GACnC;GACA,MAAM,OAAO,KAAK,IAAI,GAAG,+BAA+B;IAAE;IAAQ;IAAK;IAAU,iBAAiB,EAAE,OAAO,MAAM;GAAE,CAAC,CAAC;EACzH,SAAS,OAAgB;GACrB,MAAM,OAAO,KAAK,IAAI,GAAG,4BAA4B;IAAE;IAAQ;IAAK;GAAS,CAAC,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;GACtG,MAAM,gBAAgB,OAAO,MAAM;EACvC;CACJ;;;;CAKA,gBAAwB,UAA2D;EAC/E,MAAM,YAAoC,CAAC;EAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,GAC9C,IAAI,OAAO,UAAU,UACjB,UAAU,OAAO;OACd,IAAI,UAAU,KAAA,KAAa,UAAU,MACxC,UAAU,OAAO,KAAK,UAAU,KAAK;EAG7C,OAAO;CACX;CAEA,MAAM,aAAa,KAAa,QAA0C;EAEtE,IAAI,eAAe;EACnB,IAAI,iBAAiB,KAAK,UAAU,MAAM;EAE1C,MAAM,QAAQ,IAAI,MAAM,eAAe;EACvC,IAAI,OAAO;GACP,MAAM,iBAAiB,MAAM,EAAE,CAAC;GAChC,MAAM,kBAAkB,IAAI,UAAU,cAAc;GACpD,MAAM,aAAa,gBAAgB,QAAQ,GAAG;GAC9C,IAAI,aAAa,GAAG;IAChB,iBAAiB,gBAAgB,UAAU,GAAG,UAAU;IACxD,eAAe,gBAAgB,UAAU,aAAa,CAAC;GAC3D;EACJ;EAEA,IAAI;GACA,MAAM,KAAK,MAAM,OAAO;GACxB,MAAM,YAAY,MAAM,cAAc;GACtC,MAAM,SAAS,MAAM,KAAK,UAAU;GAGpC,MAAM,cAAc,IAAI,GAAG,kBAAkB;IACzC,QAAQ;IACR,KAAK;GACT,CAAC;GAED,MAAM,aAAa,MAAM,OAAO,KAAK,WAAW;GAGhD,MAAM,aAAa,IAAI,GAAG,iBAAiB;IACvC,QAAQ;IACR,KAAK;GACT,CAAC;GAcD,OAAO;IACH,KAAA,MAZc,UAAU,aAAa,QAAQ,YAAY,EAAE,WAAW,KAAK,CAAC;IAa5E,UAAA;KAVA,QAAQ;KACR,UAAU;KACV,MAAM,aAAa,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;KACvC,MAAM,WAAW,iBAAiB;KAClC,aAAa,WAAW,eAAe;KACvC,gBAAgB,WAAW,YAAY,CAAC;IAKxC;GACJ;EACJ,SAAS,OAAgB;GACrB,IAAI,aAAa,KAAK,GAClB,OAAO;IACH,KAAK;IACL,cAAc;GAClB;GAEJ,MAAM,gBAAgB,OAAO,cAAc;EAC/C;CACJ;CAEA,MAAM,UAAU,KAAa,QAAuC;EAEhE,IAAI,eAAe;EACnB,IAAI,iBAAiB,KAAK,UAAU,MAAM;EAE1C,MAAM,QAAQ,IAAI,MAAM,eAAe;EACvC,IAAI,OAAO;GACP,MAAM,iBAAiB,MAAM,EAAE,CAAC;GAChC,MAAM,kBAAkB,IAAI,UAAU,cAAc;GACpD,MAAM,aAAa,gBAAgB,QAAQ,GAAG;GAC9C,IAAI,aAAa,GAAG;IAChB,iBAAiB,gBAAgB,UAAU,GAAG,UAAU;IACxD,eAAe,gBAAgB,UAAU,aAAa,CAAC;GAC3D;EACJ;EAEA,IAAI;GACA,MAAM,KAAK,MAAM,OAAO;GACxB,MAAM,SAAS,MAAM,KAAK,UAAU;GAEpC,MAAM,UAAU,IAAI,GAAG,iBAAiB;IACpC,QAAQ;IACR,KAAK;GACT,CAAC;GAED,MAAM,WAAW,MAAM,OAAO,KAAK,OAAO;GAE1C,IAAI,CAAC,SAAS,MACV,OAAO;GAIX,MAAM,SAAuB,CAAC;GAC9B,WAAW,MAAM,SAAS,SAAS,MAC/B,OAAO,KAAK,KAAK;GAErB,MAAM,SAAS,OAAO,OAAO,MAAM;GAEnC,MAAM,cAAc,SAAS,eAAe;GAC5C,MAAM,WAAW,aAAa,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GAElD,MAAM,OAAO,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,MAAM,YAAY,CAAC;GAKrD,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,UAAU;IAC9B,MAAM;IACN,cAAc,SAAS,cAAc,QAAQ;GACjD,CAAC;EACL,SAAS,OAAgB;GACrB,IAAI,aAAa,KAAK,GAClB,OAAO;GAEX,MAAM,gBAAgB,OAAO,cAAc;EAC/C;CACJ;;;;;CAMA,gBAAwB,KAAa,QAAkD;EACnF,MAAM,QAAQ,IAAI,MAAM,eAAe;EACvC,IAAI,OAAO;GACP,MAAM,kBAAkB,IAAI,UAAU,MAAM,EAAE,CAAC,MAAM;GACrD,MAAM,aAAa,gBAAgB,QAAQ,GAAG;GAC9C,IAAI,aAAa,GACb,OAAO;IAAE,QAAQ,gBAAgB,UAAU,GAAG,UAAU;IAAG,KAAK,gBAAgB,UAAU,aAAa,CAAC;GAAE;EAElH;EACA,OAAO;GAAE,QAAQ,KAAK,UAAU,MAAM;GAAG;EAAI;CACjD;;;;;;CAOA,MAAM,YAAY,KAAa,QAAmD;EAC9E,MAAM,WAAW,KAAK,gBAAgB,KAAK,MAAM;EACjD,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EACpC,IAAI;GACA,MAAM,OAAO,MAAM,OAAO,KAAK,IAAI,GAAG,kBAAkB;IAAE,QAAQ,SAAS;IAAQ,KAAK,SAAS;GAAI,CAAC,CAAC;GACvG,OAAO;IACH,QAAQ,SAAS;IACjB,UAAU,SAAS;IACnB,MAAM,SAAS,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS;IAChD,MAAM,KAAK,iBAAiB;IAC5B,aAAa,KAAK,eAAe;IACjC,gBAAgB,KAAK,YAAY,CAAC;GACtC;EACJ,SAAS,OAAgB;GACrB,IAAI,aAAa,KAAK,GAAG,OAAO;GAChC,MAAM,gBAAgB,OAAO,SAAS,MAAM;EAChD;CACJ;;CAGA,MAAM,WAAW,KAAa,QAAmD;EAC7E,MAAM,WAAW,KAAK,gBAAgB,KAAK,MAAM;EACjD,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EACpC,IAAI;GACA,MAAM,OAAO,MAAM,OAAO,KAAK,IAAI,GAAG,kBAAkB;IAAE,QAAQ,SAAS;IAAQ,KAAK,SAAS;GAAI,CAAC,CAAC;GACvG,OAAO;IACH,MAAM,KAAK,iBAAiB;IAC5B,aAAa,KAAK,eAAe;IAGjC,cAAc,KAAK,cAAc,QAAQ,KAAK,KAAK,IAAI;GAC3D;EACJ,SAAS,OAAgB;GACrB,IAAI,aAAa,KAAK,GAAG,OAAO;GAChC,MAAM,gBAAgB,OAAO,SAAS,MAAM;EAChD;CACJ;;;;;;CAOA,MAAM,gBACF,KACA,SACkC;EAClC,MAAM,WAAW,KAAK,gBAAgB,KAAK,SAAS,MAAM;EAC1D,MAAM,QAAQ,SAAS;EACvB,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EACpC,IAAI;EACJ,IAAI;GACA,WAAW,MAAM,OAAO,KAAK,IAAI,GAAG,iBAAiB;IACjD,QAAQ,SAAS;IACjB,KAAK,SAAS;IACd,GAAI,QAAQ,EAAE,OAAO,SAAS,MAAM,MAAM,GAAG,MAAM,MAAM,IAAI,CAAC;GAClE,CAAC,CAAC;EACN,SAAS,OAAgB;GACrB,IAAI,aAAa,KAAK,GAAG,OAAO;GAIhC,IAAI,SAAS,wBAAwB,KAAK,GAAG,OAAO,KAAK,gBAAgB,KAAK,EAAE,QAAQ,SAAS,OAAO,CAAC;GACzG,MAAM,gBAAgB,OAAO,SAAS,MAAM;EAChD;EACA,MAAM,OAAO,SAAS;EACtB,IAAI,CAAC,MAAM,OAAO;EAKlB,MAAM,QAAQ,QAAQ,cAAc,KAAK,SAAS,gBAAgB,EAAE,CAAC,GAAG,KAAK,KAAA;EAC7E,MAAM,SAAS,SAAS,UAAU,KAAA,IAAY,QAAQ,KAAA;EACtD,OAAO;GACH,QAAQ,eAAe,IAAI,IAAI,cAAc,IAAI,IAAI,KAAK,qBAAqB;GAC/E,MAAM,UAAU,KAAA,IAAY,OAAO,KAAK,IAAI,SAAS,iBAAiB;GACtE,aAAa,SAAS,eAAe;GACrC,cAAc,SAAS,cAAc,QAAQ,KAAK,KAAK,IAAI;GAC3D,GAAI,SAAS,EAAE,OAAO,OAAO,IAAI,CAAC;EACtC;CACJ;CAEA,MAAM,aAAa,KAAa,QAAgC;EAE5D,IAAI,eAAe;EACnB,IAAI,iBAAiB,KAAK,UAAU,MAAM;EAE1C,MAAM,QAAQ,IAAI,MAAM,eAAe;EACvC,IAAI,OAAO;GACP,MAAM,iBAAiB,MAAM,EAAE,CAAC;GAChC,MAAM,kBAAkB,IAAI,UAAU,cAAc;GACpD,MAAM,aAAa,gBAAgB,QAAQ,GAAG;GAC9C,IAAI,aAAa,GAAG;IAChB,iBAAiB,gBAAgB,UAAU,GAAG,UAAU;IACxD,eAAe,gBAAgB,UAAU,aAAa,CAAC;GAC3D;EACJ;EAEA,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EAEpC,MAAM,UAAU,IAAI,GAAG,oBAAoB;GACvC,QAAQ;GACR,KAAK;EACT,CAAC;EASD,IAAI;GACA,MAAM,OAAO,KAAK,OAAO;EAC7B,SAAS,OAAgB;GACrB,IAAI,aAAa,KAAK,GAClB;GAEJ,MAAM,gBAAgB,OAAO,cAAc;EAC/C;CACJ;CAEA,MAAM,YAAY,QAAgB,SAIH;EAC3B,MAAM,iBAAiB,KAAK,UAAU,SAAS,MAAM;EAErD,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,SAAS,MAAM,KAAK,UAAU;EAEpC,MAAM,eAAe,cAAc,MAAM;EACzC,MAAM,UAAU,IAAI,GAAG,qBAAqB;GACxC,QAAQ;GACR,QAAQ;GACR,SAAS,SAAS,cAAc;GAChC,mBAAmB,SAAS;GAC5B,WAAW;EACf,CAAC;EAED,MAAM,WAAW,MAAM,OAAO,KAAK,OAAO,CAAC,CACtC,OAAO,UAAmB;GAAE,MAAM,gBAAgB,OAAO,cAAc;EAAG,CAAC;EAmChF,OAAO;GACH,QA7Ba,SAAS,YAAY,CAAC,EAAA,CAAG,QAAO,QAAO,IAAI,QAAQ,YAClC,CAAA,CAAQ,KAAI,SAAQ;IAClD,QAAQ;IACR,UAAU,IAAI,OAAO;IACrB,OAAO,IAAI,OAAO,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;IAG1C,GAAI,OAAO,IAAI,SAAS,WAAW,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACzD,GAAI,IAAI,wBAAwB,OAAO,EAAE,SAAS,IAAI,aAAa,YAAY,EAAE,IAAI,CAAC;IACtF,QAAQ;IACR,MAAM;IACN,gBAAgB,QAAQ,eAAe,GAAG,IAAI;GAClD,EAiBI;GACA,WAhBkC,SAAS,kBAAkB,CAAC,EAAA,CAAG,KAAI,WAAU;IAG/E,MAAM,SAAS,UAAU,OAAO,UAAU,EAAE;IAC5C,OAAO;KACH,QAAQ;KACR,UAAU;KACV,MAAM,OAAO,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;KACjC,QAAQ;KACR,MAAM;KACN,gBAAgB,QAAQ,eAAe,GAAG;IAC9C;GACJ,CAII;GACA,eAAe,SAAS;EAC5B;CACJ;AACJ;;AAGA,SAAS,aAAa,OAAoD;CACtE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,CAAC;CACzD,MAAM,OAAO,UAAU,SAAS,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,KAAA;CAC9E,MAAM,OAAO,eAAe,QAAQ,MAAM,YAAY,KAAA;CAItD,OAAO;EAAE;EAAM,QAHA,OAAO,SAAS,YAAY,SAAS,QAAQ,oBAAoB,QAAQ,OAAO,KAAK,mBAAmB,WACjH,KAAK,iBACL,KAAA;CACgB;AAC1B;;;;;;;;;AAUA,SAAS,aAAa,OAAyB;CAC3C,MAAM,EAAE,MAAM,WAAW,aAAa,KAAK;CAC3C,IAAI,SAAS,gBAAgB,OAAO;CACpC,OAAO,SAAS,eAAe,SAAS,cAAc,WAAW;AACrE;;AAGA,IAAM,0CAA0B,IAAI,IAAI;CAAC;CAAgB;CAAgB;CAAsB;AAAuB,CAAC;;;;;;;;AASvH,SAAS,gBAAgB,OAAgB,QAAyB;CAC9D,MAAM,EAAE,SAAS,aAAa,KAAK;CACnC,IAAI,CAAC,QAAQ,CAAC,wBAAwB,IAAI,IAAI,GAAG,OAAO;CACxD,OAAO,IAAI,SACP,KACA,yBACA,SAAS,iBACH,uBAAuB,OAAO,+HAC9B,0EAA0E,OAAO,KAAK,KAAK,6DACjG;EAAE;EAAQ,QAAQ;CAAK,CAC3B;AACJ;AAEA,SAAS,wBAAwB,OAAyB;CACtD,MAAM,EAAE,MAAM,WAAW,aAAa,KAAK;CAC3C,OAAO,SAAS,kBAAkB,WAAW;AACjD;;AAGA,SAAS,eAAe,OAAoD;CACxE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAO,iBAAiB;AAClF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CollectionPatch } from "@rebasepro/types";
|
|
1
2
|
/**
|
|
2
3
|
* Move presentation keys into the `admin` block.
|
|
3
4
|
*
|
|
@@ -59,6 +60,22 @@ export declare class AstSchemaEditor {
|
|
|
59
60
|
* Resolve a file path and ensure it falls within the collectionsDir.
|
|
60
61
|
*/
|
|
61
62
|
private safePath;
|
|
63
|
+
/**
|
|
64
|
+
* Bring the in-memory project back in line with the disk.
|
|
65
|
+
*
|
|
66
|
+
* The source-only editor builds ONE of these at boot and serves every
|
|
67
|
+
* request with it, and ts-morph reads a file once and then trusts its
|
|
68
|
+
* copy. `project.save()` writes that copy back whole, so anything that
|
|
69
|
+
* reached the disk in between — the live editor's committed change (it
|
|
70
|
+
* builds a fresh editor per request), or the developer's own edit in the
|
|
71
|
+
* IDE — was overwritten by the boot-time copy plus this edit. In `rebase
|
|
72
|
+
* dev` the tsx-watch restart usually closed the window; outside watch mode
|
|
73
|
+
* the loss was permanent.
|
|
74
|
+
*
|
|
75
|
+
* Called at the top of every public method, so each edit applies to what
|
|
76
|
+
* is on disk now.
|
|
77
|
+
*/
|
|
78
|
+
private syncWithDisk;
|
|
62
79
|
private getCollectionFile;
|
|
63
80
|
/**
|
|
64
81
|
* Find the object literal a collection is declared with, through whatever
|
|
@@ -85,6 +102,18 @@ export declare class AstSchemaEditor {
|
|
|
85
102
|
private findProperty;
|
|
86
103
|
private static quoteKey;
|
|
87
104
|
private static isPlainObject;
|
|
105
|
+
/**
|
|
106
|
+
* The field that identifies each element of an array of objects, so an
|
|
107
|
+
* old element can be found again in the new array.
|
|
108
|
+
*
|
|
109
|
+
* Arrays travel whole (see `collection_patch.ts`), and an element's
|
|
110
|
+
* position is not its identity — adding an entity action at the front
|
|
111
|
+
* shifts every other one. `key`, `id`, `relationName`, `name`: the first
|
|
112
|
+
* one every new element carries, distinct, wins.
|
|
113
|
+
*/
|
|
114
|
+
private static identityField;
|
|
115
|
+
/** The old array's object elements, keyed by `field`'s literal value. */
|
|
116
|
+
private oldElementsBy;
|
|
88
117
|
private convertJsonToAstString;
|
|
89
118
|
/**
|
|
90
119
|
* Write only the keys the patch names, leaving every sibling alone.
|
|
@@ -122,6 +151,45 @@ export declare class AstSchemaEditor {
|
|
|
122
151
|
saveCollection(collectionId: string, collectionData: Record<string, unknown>, options?: {
|
|
123
152
|
partial?: boolean;
|
|
124
153
|
}): Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* Write what changed about a collection, and nothing else.
|
|
156
|
+
*
|
|
157
|
+
* The patch is the difference between the collection the panel loaded and
|
|
158
|
+
* the one it is saving (`diffCollections` in `@rebasepro/types`), so it
|
|
159
|
+
* names only keys the person changed. Each is written where it is; every
|
|
160
|
+
* other byte of the file — imports, comments, formatting, handlers,
|
|
161
|
+
* properties shared from another module — is left as it was.
|
|
162
|
+
*
|
|
163
|
+
* A key whose value in the file is code — `status: statusProperty`,
|
|
164
|
+
* `enum: LOCALE_ENUM`, a spread — is not overwritten with the JSON the
|
|
165
|
+
* panel saw of it, which is what the whole-collection save did. The edit
|
|
166
|
+
* is refused, naming the code, so it can be made where that code lives.
|
|
167
|
+
*
|
|
168
|
+
* All or nothing: a refused operation leaves the file as it was, including
|
|
169
|
+
* the operations before it.
|
|
170
|
+
*/
|
|
171
|
+
applyPatch(collectionId: string, patch: CollectionPatch): Promise<void>;
|
|
172
|
+
private writePatch;
|
|
173
|
+
private applyPatchOp;
|
|
174
|
+
/**
|
|
175
|
+
* A patched value with every relation target it carries turned back into
|
|
176
|
+
* source — `() => xCollection` plus its import — according to where the
|
|
177
|
+
* path lands: a relation's `target`, a relation, a property, or a map of
|
|
178
|
+
* properties.
|
|
179
|
+
*/
|
|
180
|
+
private patchValueAsSource;
|
|
181
|
+
/** A member of an object literal by name: an assignment, a shorthand, a method. */
|
|
182
|
+
private findMember;
|
|
183
|
+
private spreadIn;
|
|
184
|
+
/** `x as const`, `x satisfies T`, `(x)` — the value inside. */
|
|
185
|
+
private static unwrapExpression;
|
|
186
|
+
/**
|
|
187
|
+
* Whether a value in the file is data the panel's JSON fully describes —
|
|
188
|
+
* a literal — rather than code it saw only the result of.
|
|
189
|
+
*/
|
|
190
|
+
private static isLiteralValue;
|
|
191
|
+
/** "`properties.status` is `statusProperty` in posts.ts…" */
|
|
192
|
+
private codeRefusal;
|
|
125
193
|
private writeCollection;
|
|
126
194
|
/**
|
|
127
195
|
* Every property relation's `target`, as source.
|
|
@@ -168,5 +236,28 @@ export declare class AstSchemaEditor {
|
|
|
168
236
|
private targetThunk;
|
|
169
237
|
private hasDefaultImport;
|
|
170
238
|
private getDefaultExportName;
|
|
171
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Delete a collection's file, and take it out of `index.ts`.
|
|
241
|
+
*
|
|
242
|
+
* This used to unlink `<slug>.ts` and stop. Every scaffold's `index.ts`
|
|
243
|
+
* imports every collection, so the next boot failed for the whole project
|
|
244
|
+
* ("Cannot find module …/tags"), and a collection another one linked to
|
|
245
|
+
* took that one down with it. Now:
|
|
246
|
+
*
|
|
247
|
+
* - a file any other module imports is refused, naming the importers — a
|
|
248
|
+
* relation to it has to go first, and that is a change the person makes;
|
|
249
|
+
* - `index.ts`'s import of it is removed, and so is each place it is listed
|
|
250
|
+
* in an array; any other use there is refused, naming the line;
|
|
251
|
+
* - the file is deleted.
|
|
252
|
+
*
|
|
253
|
+
* Only the source. The table and its rows are not touched.
|
|
254
|
+
*
|
|
255
|
+
* Returns what changed, as absolute paths, so the caller can commit it.
|
|
256
|
+
*/
|
|
257
|
+
deleteCollection(collectionId: string): Promise<{
|
|
258
|
+
deleted: string;
|
|
259
|
+
changed: string[];
|
|
260
|
+
}>;
|
|
261
|
+
/** The import and export declarations in `file` whose relative specifier resolves to `targetPath`. */
|
|
262
|
+
private declarationsOf;
|
|
172
263
|
}
|