@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc
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 +27 -2
- package/dist/GCSStorageController-fv6JAZ87.js +402 -0
- package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
- package/dist/S3StorageController-BNg-pXiY.js +480 -0
- package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
- package/dist/api/mount-path.d.ts +30 -0
- package/dist/api/mount.d.ts +6 -13
- package/dist/api/rest/api-generator.d.ts +53 -1
- 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 +7 -5
- 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 +35 -0
- package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
- package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
- package/dist/auth/access.d.ts +57 -8
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
- package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
- package/dist/auth/builtin-auth-adapter.d.ts +13 -0
- package/dist/auth/email-change-routes.d.ts +3 -0
- package/dist/auth/expired-token-sweep.d.ts +5 -1
- package/dist/auth/interfaces.d.ts +10 -0
- package/dist/auth/jwks-routes.d.ts +5 -1
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +282 -166
- package/dist/auth/magic-link-routes.d.ts +3 -0
- package/dist/auth/mfa-crypto.d.ts +9 -3
- package/dist/auth/mfa-gate.d.ts +4 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/otp-routes.d.ts +3 -0
- package/dist/auth/rate-limiter.d.ts +61 -34
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +6 -0
- package/dist/auth/user-picking.d.ts +37 -0
- package/dist/auth/verify-credential.d.ts +25 -4
- package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
- package/dist/auth-GnONzteu.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend/create-backend.d.ts +281 -0
- package/dist/backend/default-backend.d.ts +35 -0
- package/dist/backend/options.d.ts +912 -0
- package/dist/backend/scope.d.ts +23 -0
- package/dist/backend/services.d.ts +54 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +14 -3
- package/dist/backup/backup-routes.d.ts +23 -2
- package/dist/backup/backup-storage.d.ts +3 -1
- package/dist/backup/index.d.ts +3 -1
- package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
- package/dist/backup-BnvkSLHi.js.map +1 -0
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +9 -2
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/provision.d.ts +4 -2
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/sources.d.ts +2 -0
- package/dist/boot/static-routing.d.ts +9 -0
- package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
- package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
- package/dist/cron/cron-loader.d.ts +77 -18
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +69 -3
- package/dist/cron/cron-store.d.ts +33 -5
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +3 -3
- package/dist/cron-loader-B-u0snqq.js +202 -0
- package/dist/cron-loader-B-u0snqq.js.map +1 -0
- package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
- package/dist/cron-routes-OD6BstUj.js.map +1 -0
- package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
- package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
- package/dist/cron-store-CZ30ZBi-.js +577 -0
- package/dist/cron-store-CZ30ZBi-.js.map +1 -0
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/env.d.ts +0 -37
- package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
- package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
- package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
- package/dist/function-loader-iKhOZatS.js.map +1 -0
- package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
- package/dist/function-routes-j9TwpvQe.js.map +1 -0
- package/dist/functions/context.d.ts +9 -0
- package/dist/functions/define-function.d.ts +10 -6
- package/dist/functions/function-loader.d.ts +75 -18
- package/dist/functions/function-routes.d.ts +2 -1
- package/dist/functions/index.d.ts +5 -4
- package/dist/functions/index.js +42 -13
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/internal.d.ts +2 -2
- package/dist/functions/proxy.d.ts +6 -2
- 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-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
- package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
- package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.es.js +24423 -20038
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +27 -10
- package/dist/init/process-safety.d.ts +7 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +49 -865
- 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-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
- package/dist/jobs-BCbflZj2.js.map +1 -0
- package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
- package/dist/jwt-BlvuKoR6.js.map +1 -0
- package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
- package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
- package/dist/load-failure-L5hvfDoG.js +80 -0
- package/dist/load-failure-L5hvfDoG.js.map +1 -0
- package/dist/mcp/mcp-routes.d.ts +6 -0
- package/dist/mcp/mcp-tools.d.ts +8 -0
- package/dist/mcp/oauth-routes.d.ts +7 -0
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
- package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
- package/dist/openapi-generator-D9zUQmEp.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-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
- package/dist/proxy-CkTlMGmh.js.map +1 -0
- package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
- package/dist/query-parser-4WkMoA_K.js.map +1 -0
- package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
- 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 +10 -0
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +13 -3
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
- package/dist/schema-editor-routes-C11Tumyd.js.map +1 -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 +5 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/singleton.d.ts +9 -5
- package/dist/soft-delete-params-DfV3vVN5.js +668 -0
- package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
- package/dist/sql-export-routes-ClRwzIsC.js +240 -0
- package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
- package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
- package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +46 -2
- package/dist/storage/LocalStorageController.d.ts +67 -2
- package/dist/storage/S3StorageController.d.ts +37 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/routes.d.ts +20 -0
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +46 -2
- package/dist/storage/types.d.ts +82 -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/package.json +5 -5
- package/dist/GCSStorageController-BSiP1c-f.js +0 -256
- package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
- package/dist/S3StorageController-CAwFRgjV.js +0 -276
- package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
- package/dist/auth-DMLngxn_.js.map +0 -1
- package/dist/backup-CN0s50D2.js.map +0 -1
- package/dist/contract-routes-fz8i4pxs.js.map +0 -1
- package/dist/cron-loader-CwaANlOG.js +0 -118
- package/dist/cron-loader-CwaANlOG.js.map +0 -1
- package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
- package/dist/cron-scheduler-CYQgco86.js.map +0 -1
- package/dist/cron-store-D2Q9-Aco.js +0 -300
- package/dist/cron-store-D2Q9-Aco.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/function-loader-D7o5Epjj.js.map +0 -1
- package/dist/function-routes-CaNG4waN.js.map +0 -1
- package/dist/jobs-DqYNfquG.js.map +0 -1
- package/dist/jwt-R6bSPMjk.js.map +0 -1
- package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
- package/dist/proxy-qRlqeUmO.js.map +0 -1
- package/dist/query-parser-BgiKJKvc.js.map +0 -1
- package/dist/request-timeout-DgH7j8qO.js.map +0 -1
- package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
- package/dist/soft-delete-params-BWPilMPF.js +0 -59
- package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
- package/dist/src-I3aG1PcY.js +0 -6870
- package/dist/src-I3aG1PcY.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
package/dist/storage/types.d.ts
CHANGED
|
@@ -53,8 +53,6 @@ export interface S3StorageConfig {
|
|
|
53
53
|
maxFileSize?: number;
|
|
54
54
|
/** Allowed MIME types (if not set, all types allowed) */
|
|
55
55
|
allowedMimeTypes?: string[];
|
|
56
|
-
/** URL expiration time in seconds for signed URLs (default: 3600) */
|
|
57
|
-
signedUrlExpiration?: number;
|
|
58
56
|
}
|
|
59
57
|
/**
|
|
60
58
|
* Google Cloud Storage configuration (works with GCS and Firebase Storage)
|
|
@@ -77,8 +75,6 @@ export interface GCSStorageConfig {
|
|
|
77
75
|
maxFileSize?: number;
|
|
78
76
|
/** Allowed MIME types */
|
|
79
77
|
allowedMimeTypes?: string[];
|
|
80
|
-
/** Signed URL expiration in seconds (default: 3600) */
|
|
81
|
-
signedUrlExpiration?: number;
|
|
82
78
|
}
|
|
83
79
|
/**
|
|
84
80
|
* Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.
|
|
@@ -96,16 +92,40 @@ export interface GCSStorageConfig {
|
|
|
96
92
|
export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;
|
|
97
93
|
/** What a storage request is trying to do to an object. */
|
|
98
94
|
export type { StorageOperation, StorageAuthorizeUser, StorageAuthorizeContext, StorageAuthorizeData, StorageAuthorize } from "@rebasepro/types";
|
|
95
|
+
/**
|
|
96
|
+
* How the server writes an object. See {@link StorageController.putObject}.
|
|
97
|
+
*/
|
|
98
|
+
export interface PutObjectOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Hold this write to the controller's upload limits: `maxFileSize` and
|
|
101
|
+
* `allowedMimeTypes`. Defaults to `true`.
|
|
102
|
+
*
|
|
103
|
+
* Those limits say what a user may upload. A write the server makes for
|
|
104
|
+
* itself, such as a database backup, passes `false`. A dump is neither of
|
|
105
|
+
* an allowed type nor, past a small database, under 50 MB. The upload
|
|
106
|
+
* routes never pass it.
|
|
107
|
+
*/
|
|
108
|
+
uploadLimits?: boolean;
|
|
109
|
+
}
|
|
99
110
|
/**
|
|
100
111
|
* Storage controller interface for backend implementations
|
|
101
112
|
*/
|
|
102
113
|
export interface StorageController {
|
|
103
114
|
/**
|
|
104
|
-
* Upload an object
|
|
115
|
+
* Upload an object.
|
|
116
|
+
*
|
|
117
|
+
* A write is held to the controller's upload limits — `maxFileSize` and
|
|
118
|
+
* `allowedMimeTypes` — unless `options.uploadLimits` is `false`. Only the
|
|
119
|
+
* server's own writes pass that; nothing a request sends reaches it.
|
|
105
120
|
*/
|
|
106
|
-
putObject(props: UploadFileProps): Promise<UploadFileResult>;
|
|
121
|
+
putObject(props: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;
|
|
107
122
|
/**
|
|
108
|
-
* Get a download URL (signed URL equivalent) for an object
|
|
123
|
+
* Get a download URL (signed URL equivalent) for an object.
|
|
124
|
+
*
|
|
125
|
+
* For server-side code only: no route hands this URL to a client. A
|
|
126
|
+
* client reads a private object through `/file/*` with the download
|
|
127
|
+
* token `GET /metadata/*` mints. The built-in S3 and GCS controllers sign
|
|
128
|
+
* it for an hour.
|
|
109
129
|
*/
|
|
110
130
|
getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;
|
|
111
131
|
/**
|
|
@@ -128,6 +148,29 @@ export interface StorageController {
|
|
|
128
148
|
* Get object as a File
|
|
129
149
|
*/
|
|
130
150
|
getObject(key: string, bucket?: string): Promise<File | null>;
|
|
151
|
+
/**
|
|
152
|
+
* Describe an object for serving it — its size, stored type and version —
|
|
153
|
+
* without reading its bytes, or `null` when there is no such object.
|
|
154
|
+
*
|
|
155
|
+
* Optional, together with {@link getObjectStream}. A controller that has
|
|
156
|
+
* both is served streamed: `GET /file/*` answers a `304`, a `416`, a `HEAD`
|
|
157
|
+
* and a cached image rendition from this alone, and otherwise reads only
|
|
158
|
+
* the bytes it sends — the requested range, a chunk at a time. One without
|
|
159
|
+
* them is served through {@link getObject}, which holds the whole object in
|
|
160
|
+
* memory for every request, a `Range` request for one megabyte included.
|
|
161
|
+
*/
|
|
162
|
+
statObject?(key: string, bucket?: string): Promise<StoredObjectInfo | null>;
|
|
163
|
+
/**
|
|
164
|
+
* Read an object, or one byte range of it, as a stream, or `null` when
|
|
165
|
+
* there is no such object. See {@link statObject}.
|
|
166
|
+
*
|
|
167
|
+
* The stream holds a file or a connection until it is read to the end or
|
|
168
|
+
* cancelled, so a caller that decides not to read it must cancel it.
|
|
169
|
+
*/
|
|
170
|
+
getObjectStream?(key: string, options?: {
|
|
171
|
+
bucket?: string;
|
|
172
|
+
range?: StoredObjectRange;
|
|
173
|
+
}): Promise<StoredObjectStream | null>;
|
|
131
174
|
/**
|
|
132
175
|
* Delete an object
|
|
133
176
|
*/
|
|
@@ -179,6 +222,38 @@ export interface StorageController {
|
|
|
179
222
|
*/
|
|
180
223
|
maxFileSize?(): number;
|
|
181
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* What serving an object needs to know before deciding whether to read it.
|
|
227
|
+
* See {@link StorageController.statObject}.
|
|
228
|
+
*/
|
|
229
|
+
export interface StoredObjectInfo {
|
|
230
|
+
/** The whole object's size in bytes. */
|
|
231
|
+
size: number;
|
|
232
|
+
/** The stored content type — the uploader's claim, not a sniffed one. */
|
|
233
|
+
contentType: string;
|
|
234
|
+
/**
|
|
235
|
+
* The object's own modification time, in milliseconds since the epoch.
|
|
236
|
+
* The routes build `ETag`, `Last-Modified` and the transform cache key from
|
|
237
|
+
* it, so it must be the object's version, not the time of the read.
|
|
238
|
+
*/
|
|
239
|
+
lastModified: number;
|
|
240
|
+
}
|
|
241
|
+
/** An inclusive byte range, as `Range: bytes=start-end` names one. */
|
|
242
|
+
export interface StoredObjectRange {
|
|
243
|
+
start: number;
|
|
244
|
+
end: number;
|
|
245
|
+
}
|
|
246
|
+
/** An object's bytes as a stream. See {@link StorageController.getObjectStream}. */
|
|
247
|
+
export interface StoredObjectStream extends StoredObjectInfo {
|
|
248
|
+
/** The bytes: the range when {@link range} is set, the whole object otherwise. */
|
|
249
|
+
stream: ReadableStream<Uint8Array>;
|
|
250
|
+
/**
|
|
251
|
+
* The range {@link stream} holds — the one asked for — or absent when it
|
|
252
|
+
* holds the whole object. A controller may answer a range with the whole
|
|
253
|
+
* object, as HTTP lets a server do; the caller then serves it whole.
|
|
254
|
+
*/
|
|
255
|
+
range?: StoredObjectRange;
|
|
256
|
+
}
|
|
182
257
|
/**
|
|
183
258
|
* Default maximum file size (50MB)
|
|
184
259
|
*/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every storage controller checks about a file before it stores it: the
|
|
3
|
+
* source's own size cap and, when configured, its allowed content types.
|
|
4
|
+
*
|
|
5
|
+
* One function for the three controllers, which carried a copy each. Both
|
|
6
|
+
* refusals are `ApiError`s, so they reach the caller as what they are. A plain
|
|
7
|
+
* `Error` was a 500 "Internal Server Error" on `/upload` and a 502 on TUS —
|
|
8
|
+
* after the client had sent every byte.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateUploadedFile(file: {
|
|
11
|
+
size: number;
|
|
12
|
+
type: string;
|
|
13
|
+
}, maxSize: number, allowedMimeTypes: string[] | undefined): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import "./resumable-upload-threshold-CKv-Ry0l.js";
|
|
6
|
+
//#region ../common/src/util/resumable-upload.ts
|
|
7
|
+
/** What `create` answers in `Rebase-Upload-Result` when the last `PATCH` will carry the stored object there. */
|
|
8
|
+
var RESULT_ON_COMPLETE = "on-complete";
|
|
9
|
+
var DEFAULT_RETRY_DELAYS_MS = [
|
|
10
|
+
250,
|
|
11
|
+
1e3,
|
|
12
|
+
3e3
|
|
13
|
+
];
|
|
14
|
+
/** `value` as UTF-8, base64-encoded — what `Upload-Metadata` carries. */
|
|
15
|
+
function base64Utf8(value) {
|
|
16
|
+
const bytes = new TextEncoder().encode(value);
|
|
17
|
+
let binary = "";
|
|
18
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
/** Worth asking for the offset and sending again: an offset conflict, a lock, a rate limit, a server fault. */
|
|
22
|
+
var isRetryable = (status) => status === 409 || status === 423 || status === 429 || status >= 500;
|
|
23
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
/** The stored object a finished upload reports, or `null` when the header says nothing readable. */
|
|
25
|
+
function storedObjectOf(header) {
|
|
26
|
+
if (!header) return null;
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(decodeURIComponent(header));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "object" || value === null) return null;
|
|
34
|
+
const key = "key" in value ? value.key : void 0;
|
|
35
|
+
const bucket = "bucket" in value ? value.bucket : void 0;
|
|
36
|
+
const storageUrl = "storageUrl" in value ? value.storageUrl : void 0;
|
|
37
|
+
if (typeof key !== "string" || typeof bucket !== "string" || typeof storageUrl !== "string") return null;
|
|
38
|
+
return {
|
|
39
|
+
key,
|
|
40
|
+
bucket,
|
|
41
|
+
storageUrl
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Upload `file` through a Rebase server's TUS endpoint, one chunk at a time.
|
|
46
|
+
*
|
|
47
|
+
* The server holds every upload to what `POST /storage/upload` holds a file
|
|
48
|
+
* to — the source's and the property's limits, the authorize hook, the bucket
|
|
49
|
+
* — and stores it with the same key, type and custom metadata, and fires the
|
|
50
|
+
* same triggers. So a refusal is thrown as the multipart route would have
|
|
51
|
+
* thrown it. A chunk that fails on the way — a dropped connection, a 5xx, a
|
|
52
|
+
* conflict over the offset — is retried from the offset the server reports.
|
|
53
|
+
*
|
|
54
|
+
* Answers `unavailable` when the resumable path cannot carry this file and
|
|
55
|
+
* the multipart one can: a server without the endpoint (or one too old to
|
|
56
|
+
* report the stored object), an upload another instance cannot see (several
|
|
57
|
+
* instances, no shared volume, no sticky sessions), or a chunk over the
|
|
58
|
+
* server's body limit.
|
|
59
|
+
*/
|
|
60
|
+
async function uploadResumable(options) {
|
|
61
|
+
const { file, send } = options;
|
|
62
|
+
const chunkSize = Math.max(1, options.chunkSize ?? 8388608);
|
|
63
|
+
const retryDelays = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
64
|
+
const tus = { "Tus-Resumable": "1.0.0" };
|
|
65
|
+
const created = await send("POST", "", {
|
|
66
|
+
...tus,
|
|
67
|
+
"Upload-Length": String(file.size),
|
|
68
|
+
"Upload-Metadata": Object.entries(options.metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`).join(",")
|
|
69
|
+
});
|
|
70
|
+
if (created.status === 404 || created.status === 405 || created.status === 501) return { unavailable: `the server has no resumable upload endpoint (${created.status})` };
|
|
71
|
+
if (created.status < 200 || created.status >= 300) throw created.error ?? /* @__PURE__ */ new Error(`The upload was refused (${created.status}).`);
|
|
72
|
+
const id = created.header("Location")?.split("?")[0].split("/").filter(Boolean).pop();
|
|
73
|
+
if (!id) return { unavailable: "the server did not say where the upload lives" };
|
|
74
|
+
const path = `/${encodeURIComponent(id)}`;
|
|
75
|
+
if (created.header("Rebase-Upload-Result") !== RESULT_ON_COMPLETE) {
|
|
76
|
+
await send("DELETE", path, tus).catch(() => void 0);
|
|
77
|
+
return { unavailable: "the server does not report the object a resumable upload stores" };
|
|
78
|
+
}
|
|
79
|
+
let offset = 0;
|
|
80
|
+
let lastFailure;
|
|
81
|
+
for (let attempt = 0;;) {
|
|
82
|
+
const end = Math.min(file.size, offset + chunkSize);
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await send("PATCH", path, {
|
|
86
|
+
...tus,
|
|
87
|
+
"Upload-Offset": String(offset),
|
|
88
|
+
"Content-Type": "application/offset+octet-stream"
|
|
89
|
+
}, file.slice(offset, end));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
lastFailure = error instanceof Error ? error : new Error(String(error));
|
|
92
|
+
}
|
|
93
|
+
if (response && response.status >= 200 && response.status < 300) {
|
|
94
|
+
attempt = 0;
|
|
95
|
+
const reported = Number(response.header("Upload-Offset"));
|
|
96
|
+
offset = Number.isFinite(reported) && reported > offset ? reported : end;
|
|
97
|
+
if (offset < file.size) continue;
|
|
98
|
+
const stored = storedObjectOf(response.header("Rebase-Upload-Result"));
|
|
99
|
+
if (!stored) throw new Error("The upload finished, but the server did not say where it stored it.");
|
|
100
|
+
return { stored };
|
|
101
|
+
}
|
|
102
|
+
if (response?.status === 404) return { unavailable: "the server no longer knows this upload — another instance may have answered" };
|
|
103
|
+
if (response?.status === 413) {
|
|
104
|
+
await send("DELETE", path, tus).catch(() => void 0);
|
|
105
|
+
return { unavailable: "a chunk is over the server's request body limit" };
|
|
106
|
+
}
|
|
107
|
+
if (response && !isRetryable(response.status)) {
|
|
108
|
+
await send("DELETE", path, tus).catch(() => void 0);
|
|
109
|
+
throw response.error ?? /* @__PURE__ */ new Error(`The upload was refused (${response.status}).`);
|
|
110
|
+
}
|
|
111
|
+
if (response) lastFailure = response.error ?? /* @__PURE__ */ new Error(`The upload failed (${response.status}).`);
|
|
112
|
+
if (attempt >= retryDelays.length) throw lastFailure ?? /* @__PURE__ */ new Error("The upload failed.");
|
|
113
|
+
await sleep(retryDelays[attempt++]);
|
|
114
|
+
try {
|
|
115
|
+
const progress = await send("HEAD", path, tus);
|
|
116
|
+
if (progress.status === 404) return { unavailable: "the server no longer knows this upload — another instance may have answered" };
|
|
117
|
+
const held = Number(progress.header("Upload-Offset"));
|
|
118
|
+
if (progress.status >= 200 && progress.status < 300 && Number.isFinite(held)) offset = held;
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { uploadResumable };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=storage-resumable-VO-cMpse-C_YX71pO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-resumable-VO-cMpse-C_YX71pO.js","names":[],"sources":["../../common/src/util/resumable-upload.ts"],"sourcesContent":["import type { UploadFileResult } from \"@rebasepro/types\";\nimport { RESUMABLE_UPLOAD_THRESHOLD } from \"./resumable-upload-threshold\";\n\n/** One response, as the resumable upload reads it. */\nexport interface ResumableUploadResponse {\n status: number;\n header(name: string): string | null;\n /** The error a non-2xx response stands for, as the transport reports it. */\n error?: Error;\n}\n\n/**\n * Send one TUS request. `path` is `\"\"` for the creation request and\n * `/<upload id>` after it, relative to the source's `…/storage/tus` endpoint.\n * Answers every HTTP status; throws only when no response arrived.\n */\nexport type ResumableUploadSend = (\n method: \"POST\" | \"PATCH\" | \"HEAD\" | \"DELETE\",\n path: string,\n headers: Record<string, string>,\n body?: Blob\n) => Promise<ResumableUploadResponse>;\n\nexport interface ResumableUploadOptions {\n file: Blob;\n /** `Upload-Metadata` entries: `key`, `filename`, `filetype`, `bucket`, `storageId`, `collection`, `property`, `metadata_<name>`. */\n metadata: Record<string, string>;\n send: ResumableUploadSend;\n /** Bytes per `PATCH`. Default {@link RESUMABLE_UPLOAD_THRESHOLD}. */\n chunkSize?: number;\n /** How long to wait before each retry of a chunk; its length is the number of retries. */\n retryDelaysMs?: readonly number[];\n}\n\n/**\n * What a resumable upload came to: the stored object, or — for a reason the\n * multipart route does not share — nothing, and the caller sends the file\n * through `POST /storage/upload` instead.\n */\nexport type ResumableUploadOutcome =\n | { stored: UploadFileResult }\n | { unavailable: string };\n\n/** What `create` answers in `Rebase-Upload-Result` when the last `PATCH` will carry the stored object there. */\nconst RESULT_ON_COMPLETE = \"on-complete\";\nconst DEFAULT_RETRY_DELAYS_MS = [250, 1_000, 3_000];\n\n/** `value` as UTF-8, base64-encoded — what `Upload-Metadata` carries. */\nfunction base64Utf8(value: string): string {\n const bytes = new TextEncoder().encode(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\n/** Worth asking for the offset and sending again: an offset conflict, a lock, a rate limit, a server fault. */\nconst isRetryable = (status: number) => status === 409 || status === 423 || status === 429 || status >= 500;\n\nconst sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));\n\n/** The stored object a finished upload reports, or `null` when the header says nothing readable. */\nfunction storedObjectOf(header: string | null): UploadFileResult | null {\n if (!header) return null;\n let value: unknown;\n try {\n value = JSON.parse(decodeURIComponent(header));\n } catch {\n return null;\n }\n if (typeof value !== \"object\" || value === null) return null;\n const key = \"key\" in value ? value.key : undefined;\n const bucket = \"bucket\" in value ? value.bucket : undefined;\n const storageUrl = \"storageUrl\" in value ? value.storageUrl : undefined;\n if (typeof key !== \"string\" || typeof bucket !== \"string\" || typeof storageUrl !== \"string\") return null;\n return { key, bucket, storageUrl };\n}\n\n/**\n * Upload `file` through a Rebase server's TUS endpoint, one chunk at a time.\n *\n * The server holds every upload to what `POST /storage/upload` holds a file\n * to — the source's and the property's limits, the authorize hook, the bucket\n * — and stores it with the same key, type and custom metadata, and fires the\n * same triggers. So a refusal is thrown as the multipart route would have\n * thrown it. A chunk that fails on the way — a dropped connection, a 5xx, a\n * conflict over the offset — is retried from the offset the server reports.\n *\n * Answers `unavailable` when the resumable path cannot carry this file and\n * the multipart one can: a server without the endpoint (or one too old to\n * report the stored object), an upload another instance cannot see (several\n * instances, no shared volume, no sticky sessions), or a chunk over the\n * server's body limit.\n */\nexport async function uploadResumable(options: ResumableUploadOptions): Promise<ResumableUploadOutcome> {\n const { file, send } = options;\n const chunkSize = Math.max(1, options.chunkSize ?? RESUMABLE_UPLOAD_THRESHOLD);\n const retryDelays = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;\n const tus = { \"Tus-Resumable\": \"1.0.0\" };\n\n const created = await send(\"POST\", \"\", {\n ...tus,\n \"Upload-Length\": String(file.size),\n \"Upload-Metadata\": Object.entries(options.metadata)\n .map(([name, value]) => `${name} ${base64Utf8(value)}`)\n .join(\",\")\n });\n if (created.status === 404 || created.status === 405 || created.status === 501) {\n return { unavailable: `the server has no resumable upload endpoint (${created.status})` };\n }\n if (created.status < 200 || created.status >= 300) {\n throw created.error ?? new Error(`The upload was refused (${created.status}).`);\n }\n const id = created.header(\"Location\")?.split(\"?\")[0].split(\"/\").filter(Boolean).pop();\n if (!id) return { unavailable: \"the server did not say where the upload lives\" };\n const path = `/${encodeURIComponent(id)}`;\n if (created.header(\"Rebase-Upload-Result\") !== RESULT_ON_COMPLETE) {\n await send(\"DELETE\", path, tus).catch(() => undefined);\n return { unavailable: \"the server does not report the object a resumable upload stores\" };\n }\n\n let offset = 0;\n let lastFailure: Error | undefined;\n for (let attempt = 0; ;) {\n const end = Math.min(file.size, offset + chunkSize);\n let response: ResumableUploadResponse | undefined;\n try {\n response = await send(\"PATCH\", path, {\n ...tus,\n \"Upload-Offset\": String(offset),\n \"Content-Type\": \"application/offset+octet-stream\"\n }, file.slice(offset, end));\n } catch (error: unknown) {\n // No response: the connection dropped. Ask where the upload got to.\n lastFailure = error instanceof Error ? error : new Error(String(error));\n }\n\n if (response && response.status >= 200 && response.status < 300) {\n attempt = 0;\n const reported = Number(response.header(\"Upload-Offset\"));\n offset = Number.isFinite(reported) && reported > offset ? reported : end;\n if (offset < file.size) continue;\n const stored = storedObjectOf(response.header(\"Rebase-Upload-Result\"));\n if (!stored) throw new Error(\"The upload finished, but the server did not say where it stored it.\");\n return { stored };\n }\n if (response?.status === 404) {\n return { unavailable: \"the server no longer knows this upload — another instance may have answered\" };\n }\n if (response?.status === 413) {\n await send(\"DELETE\", path, tus).catch(() => undefined);\n return { unavailable: \"a chunk is over the server's request body limit\" };\n }\n if (response && !isRetryable(response.status)) {\n await send(\"DELETE\", path, tus).catch(() => undefined);\n throw response.error ?? new Error(`The upload was refused (${response.status}).`);\n }\n if (response) lastFailure = response.error ?? new Error(`The upload failed (${response.status}).`);\n\n if (attempt >= retryDelays.length) throw lastFailure ?? new Error(\"The upload failed.\");\n await sleep(retryDelays[attempt++]);\n // Resume from what the server holds, which may be more than was\n // acknowledged (the chunk arrived, its answer did not) or less.\n try {\n const progress = await send(\"HEAD\", path, tus);\n if (progress.status === 404) {\n return { unavailable: \"the server no longer knows this upload — another instance may have answered\" };\n }\n const held = Number(progress.header(\"Upload-Offset\"));\n if (progress.status >= 200 && progress.status < 300 && Number.isFinite(held)) offset = held;\n } catch {\n // Still no connection: the next attempt asks again.\n }\n }\n}\n"],"mappings":";;;;;;;AA4CA,IAAM,qBAAqB;AAC3B,IAAM,0BAA0B;CAAC;CAAK;CAAO;AAAK;;AAGlD,SAAS,WAAW,OAAuB;CACvC,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK;CAC5C,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,OAAO,UAAU,OAAO,aAAa,IAAI;CAC5D,OAAO,KAAK,MAAM;AACtB;;AAGA,IAAM,eAAe,WAAmB,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AAExG,IAAM,SAAS,OAAe,IAAI,SAAc,YAAW,WAAW,SAAS,EAAE,CAAC;;AAGlF,SAAS,eAAe,QAAgD;CACpE,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI;CACJ,IAAI;EACA,QAAQ,KAAK,MAAM,mBAAmB,MAAM,CAAC;CACjD,QAAQ;EACJ,OAAO;CACX;CACA,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,MAAM,SAAS,QAAQ,MAAM,MAAM,KAAA;CACzC,MAAM,SAAS,YAAY,QAAQ,MAAM,SAAS,KAAA;CAClD,MAAM,aAAa,gBAAgB,QAAQ,MAAM,aAAa,KAAA;CAC9D,IAAI,OAAO,QAAQ,YAAY,OAAO,WAAW,YAAY,OAAO,eAAe,UAAU,OAAO;CACpG,OAAO;EAAE;EAAK;EAAQ;CAAW;AACrC;;;;;;;;;;;;;;;;;AAkBA,eAAsB,gBAAgB,SAAkE;CACpG,MAAM,EAAE,MAAM,SAAS;CACvB,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,aAAA,OAAuC;CAC7E,MAAM,cAAc,QAAQ,iBAAiB;CAC7C,MAAM,MAAM,EAAE,iBAAiB,QAAQ;CAEvC,MAAM,UAAU,MAAM,KAAK,QAAQ,IAAI;EACnC,GAAG;EACH,iBAAiB,OAAO,KAAK,IAAI;EACjC,mBAAmB,OAAO,QAAQ,QAAQ,QAAQ,CAAC,CAC9C,KAAK,CAAC,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,KAAK,GAAG,CAAC,CACtD,KAAK,GAAG;CACjB,CAAC;CACD,IAAI,QAAQ,WAAW,OAAO,QAAQ,WAAW,OAAO,QAAQ,WAAW,KACvE,OAAO,EAAE,aAAa,gDAAgD,QAAQ,OAAO,GAAG;CAE5F,IAAI,QAAQ,SAAS,OAAO,QAAQ,UAAU,KAC1C,MAAM,QAAQ,yBAAS,IAAI,MAAM,2BAA2B,QAAQ,OAAO,GAAG;CAElF,MAAM,KAAK,QAAQ,OAAO,UAAU,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,IAAI;CACpF,IAAI,CAAC,IAAI,OAAO,EAAE,aAAa,gDAAgD;CAC/E,MAAM,OAAO,IAAI,mBAAmB,EAAE;CACtC,IAAI,QAAQ,OAAO,sBAAsB,MAAM,oBAAoB;EAC/D,MAAM,KAAK,UAAU,MAAM,GAAG,CAAC,CAAC,YAAY,KAAA,CAAS;EACrD,OAAO,EAAE,aAAa,kEAAkE;CAC5F;CAEA,IAAI,SAAS;CACb,IAAI;CACJ,KAAK,IAAI,UAAU,KAAM;EACrB,MAAM,MAAM,KAAK,IAAI,KAAK,MAAM,SAAS,SAAS;EAClD,IAAI;EACJ,IAAI;GACA,WAAW,MAAM,KAAK,SAAS,MAAM;IACjC,GAAG;IACH,iBAAiB,OAAO,MAAM;IAC9B,gBAAgB;GACpB,GAAG,KAAK,MAAM,QAAQ,GAAG,CAAC;EAC9B,SAAS,OAAgB;GAErB,cAAc,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;EAC1E;EAEA,IAAI,YAAY,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;GAC7D,UAAU;GACV,MAAM,WAAW,OAAO,SAAS,OAAO,eAAe,CAAC;GACxD,SAAS,OAAO,SAAS,QAAQ,KAAK,WAAW,SAAS,WAAW;GACrE,IAAI,SAAS,KAAK,MAAM;GACxB,MAAM,SAAS,eAAe,SAAS,OAAO,sBAAsB,CAAC;GACrE,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,qEAAqE;GAClG,OAAO,EAAE,OAAO;EACpB;EACA,IAAI,UAAU,WAAW,KACrB,OAAO,EAAE,aAAa,8EAA8E;EAExG,IAAI,UAAU,WAAW,KAAK;GAC1B,MAAM,KAAK,UAAU,MAAM,GAAG,CAAC,CAAC,YAAY,KAAA,CAAS;GACrD,OAAO,EAAE,aAAa,kDAAkD;EAC5E;EACA,IAAI,YAAY,CAAC,YAAY,SAAS,MAAM,GAAG;GAC3C,MAAM,KAAK,UAAU,MAAM,GAAG,CAAC,CAAC,YAAY,KAAA,CAAS;GACrD,MAAM,SAAS,yBAAS,IAAI,MAAM,2BAA2B,SAAS,OAAO,GAAG;EACpF;EACA,IAAI,UAAU,cAAc,SAAS,yBAAS,IAAI,MAAM,sBAAsB,SAAS,OAAO,GAAG;EAEjG,IAAI,WAAW,YAAY,QAAQ,MAAM,+BAAe,IAAI,MAAM,oBAAoB;EACtF,MAAM,MAAM,YAAY,UAAU;EAGlC,IAAI;GACA,MAAM,WAAW,MAAM,KAAK,QAAQ,MAAM,GAAG;GAC7C,IAAI,SAAS,WAAW,KACpB,OAAO,EAAE,aAAa,8EAA8E;GAExG,MAAM,OAAO,OAAO,SAAS,OAAO,eAAe,CAAC;GACpD,IAAI,SAAS,UAAU,OAAO,SAAS,SAAS,OAAO,OAAO,SAAS,IAAI,GAAG,SAAS;EAC3F,QAAQ,CAER;CACJ;AACJ"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
//#region ../types/src/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* The single error type thrown across the entire Rebase client surface —
|
|
8
|
+
* HTTP data/control-plane calls, realtime/WebSocket operations, and
|
|
9
|
+
* client-side logic errors (e.g. an unknown collection accessor). A `catch`
|
|
10
|
+
* block only ever needs to check for this one class:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { RebaseApiError } from "@rebasepro/client"; // re-exported
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* await client.data.products.update(id, { price: 9 });
|
|
17
|
+
* } catch (e) {
|
|
18
|
+
* if (e instanceof RebaseApiError) {
|
|
19
|
+
* if (e.status === 404) { ... } // HTTP failures carry a status
|
|
20
|
+
* console.error(e.code, e.details);
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* `status` is present for HTTP failures and `undefined` otherwise, so its
|
|
26
|
+
* presence distinguishes transport-level errors from realtime/logic errors.
|
|
27
|
+
*
|
|
28
|
+
* @group Errors
|
|
29
|
+
*/
|
|
30
|
+
var RebaseApiError = class extends Error {
|
|
31
|
+
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
32
|
+
status;
|
|
33
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
34
|
+
code;
|
|
35
|
+
/** Structured error payload from the server, when present. */
|
|
36
|
+
details;
|
|
37
|
+
/** See {@link RebaseErrorInit.requestId}. Quote it in a bug report. */
|
|
38
|
+
requestId;
|
|
39
|
+
/** See {@link RebaseErrorInit.retryAfterSeconds}. */
|
|
40
|
+
retryAfterSeconds;
|
|
41
|
+
constructor(message, init = {}) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "RebaseApiError";
|
|
44
|
+
this.status = init.status;
|
|
45
|
+
this.code = init.code;
|
|
46
|
+
this.details = init.details;
|
|
47
|
+
this.requestId = init.requestId;
|
|
48
|
+
this.retryAfterSeconds = init.retryAfterSeconds;
|
|
49
|
+
if (init.cause !== void 0) this.cause = init.cause;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Client-side logic error — raised before any request is made (e.g. accessing
|
|
54
|
+
* an unknown collection accessor when a typed dictionary is configured).
|
|
55
|
+
*
|
|
56
|
+
* A subclass of {@link RebaseApiError} (with no `status`), so a single
|
|
57
|
+
* `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.
|
|
58
|
+
*
|
|
59
|
+
* @group Errors
|
|
60
|
+
*/
|
|
61
|
+
var RebaseClientError = class extends RebaseApiError {
|
|
62
|
+
/**
|
|
63
|
+
* `init` is the same one {@link RebaseApiError} takes, and it is what makes
|
|
64
|
+
* `code` reachable at all.
|
|
65
|
+
*
|
|
66
|
+
* The constructor used to accept a message and nothing else, so every
|
|
67
|
+
* client-side failure — an undefined filter value, an unknown accessor,
|
|
68
|
+
* `listen()` on a client built with `realtime: false`, a function name with
|
|
69
|
+
* a `/` in it, `refreshSession()` while signed out — arrived with `code ===
|
|
70
|
+
* undefined`. The documented `switch (e.code)` in this file's own example
|
|
71
|
+
* fell to `default: throw e` for all of them, and the only client-side error
|
|
72
|
+
* that *did* carry a code was `OFFLINE`, because that one path minted a
|
|
73
|
+
* `RebaseApiError` instead.
|
|
74
|
+
*/
|
|
75
|
+
constructor(message, init = {}) {
|
|
76
|
+
super(message, init);
|
|
77
|
+
this.name = "RebaseClientError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Brand for a contract method a particular client cannot serve.
|
|
82
|
+
*
|
|
83
|
+
* `Symbol.for` rather than a fresh symbol: two copies of `@rebasepro/types` in
|
|
84
|
+
* one tree — which happens, see `docs/dependency-duplication-traps.md` — must
|
|
85
|
+
* agree about it, and a module-local symbol would not.
|
|
86
|
+
*/
|
|
87
|
+
var UNSUPPORTED_METHOD = Symbol.for("rebase.unsupportedMethod");
|
|
88
|
+
function unsupportedMethod(message) {
|
|
89
|
+
const stub = () => {
|
|
90
|
+
throw new RebaseClientError(message, { code: "REALTIME_DISABLED" });
|
|
91
|
+
};
|
|
92
|
+
stub[UNSUPPORTED_METHOD] = true;
|
|
93
|
+
return stub;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Can this method actually do anything?
|
|
97
|
+
*
|
|
98
|
+
* `true` for a stub from {@link unsupportedMethod} **and** for a method that is
|
|
99
|
+
* simply not there — a partial client, a hand-built test double, an
|
|
100
|
+
* implementation written against an older shape of the interface. Both mean the
|
|
101
|
+
* same thing to a caller, so both answer the same way, and an adapter that
|
|
102
|
+
* checks this cannot be caught out by either.
|
|
103
|
+
*
|
|
104
|
+
* Ordinary code does not need it: calling the method and letting it throw is
|
|
105
|
+
* the normal path. Adapters do — the admin panel chooses between subscribing
|
|
106
|
+
* and a one-shot `find()` by asking whether the client can listen, and a UI
|
|
107
|
+
* that subscribes into a throw is worse than one that polls. This is the
|
|
108
|
+
* question `if (accessor.listen)` used to be asking, made explicit now that the
|
|
109
|
+
* method is always there to call.
|
|
110
|
+
*
|
|
111
|
+
* @group Errors
|
|
112
|
+
*/
|
|
113
|
+
function isUnsupported(method) {
|
|
114
|
+
if (typeof method !== "function") return true;
|
|
115
|
+
return method[UNSUPPORTED_METHOD] === true;
|
|
116
|
+
}
|
|
117
|
+
/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
|
|
118
|
+
var MAX_LIST_LIMIT = 1e3;
|
|
119
|
+
/**
|
|
120
|
+
* Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
|
|
121
|
+
* serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
|
|
122
|
+
* verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
|
|
123
|
+
*
|
|
124
|
+
* @group Errors
|
|
125
|
+
*/
|
|
126
|
+
var ListLimitError = class ListLimitError extends RebaseApiError {
|
|
127
|
+
/** The ceiling that was exceeded — what the caller should page by instead. */
|
|
128
|
+
maxLimit;
|
|
129
|
+
constructor(message, maxLimit) {
|
|
130
|
+
super(message, {
|
|
131
|
+
status: 400,
|
|
132
|
+
code: "INVALID_LIMIT"
|
|
133
|
+
});
|
|
134
|
+
this.name = "ListLimitError";
|
|
135
|
+
this.maxLimit = maxLimit;
|
|
136
|
+
Object.setPrototypeOf(this, ListLimitError.prototype);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
141
|
+
*
|
|
142
|
+
* - An absent / blank limit falls back to the mode default:
|
|
143
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
144
|
+
* - A limit that is present must be an integer in `[1, maxLimit]`. Anything
|
|
145
|
+
* else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
|
|
146
|
+
* {@link ListLimitError} rather than being coerced into range, because every
|
|
147
|
+
* coercion answers a question the caller did not ask with a page it cannot
|
|
148
|
+
* tell apart from the whole collection.
|
|
149
|
+
*
|
|
150
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
151
|
+
* through this can produce an unbounded read.
|
|
152
|
+
*
|
|
153
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
154
|
+
*/
|
|
155
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
156
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
157
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
158
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
|
|
159
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new ListLimitError(`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`, maxLimit);
|
|
160
|
+
if (parsed > maxLimit) throw new ListLimitError(`\`limit\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows per read and page through the rest with \`offset\` — answering with a smaller page would be indistinguishable from there being no more rows.`, maxLimit);
|
|
161
|
+
return parsed;
|
|
162
|
+
}
|
|
163
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
164
|
+
}
|
|
165
|
+
var snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;
|
|
166
|
+
var toSnakeCase = (str) => {
|
|
167
|
+
if (!str || typeof str !== "string") return "";
|
|
168
|
+
const regExpMatchArray = str.match(snakeCaseRegex);
|
|
169
|
+
if (!regExpMatchArray) return "";
|
|
170
|
+
return regExpMatchArray.map((x) => x.toLowerCase()).join("_");
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Are these two identifiers one edit apart, ignoring case?
|
|
174
|
+
*
|
|
175
|
+
* Tight on purpose. A suggester that fires on two edits offers `columnWidth`
|
|
176
|
+
* for `colWith` and `readOnly` for `required`, and a "did you mean" that is
|
|
177
|
+
* usually wrong teaches people to stop reading them. One edit covers what
|
|
178
|
+
* actually happens — a dropped letter (`multilne`), a doubled one, a transposed
|
|
179
|
+
* pair (`validaton`), a wrong case — and nothing else.
|
|
180
|
+
*
|
|
181
|
+
* Lives here rather than beside either caller because both the CLI's manifest
|
|
182
|
+
* reader and the server's collection validator answer the same question about
|
|
183
|
+
* an unknown key, and two copies of a similarity rule is two rules the moment
|
|
184
|
+
* one of them is tuned.
|
|
185
|
+
*/
|
|
186
|
+
function isNearMiss(a, b) {
|
|
187
|
+
const x = a.toLowerCase();
|
|
188
|
+
const y = b.toLowerCase();
|
|
189
|
+
if (x === y) return true;
|
|
190
|
+
if (Math.abs(x.length - y.length) > 1) return false;
|
|
191
|
+
const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
|
|
192
|
+
let i = 0;
|
|
193
|
+
let j = 0;
|
|
194
|
+
let edits = 0;
|
|
195
|
+
while (i < shorter.length && j < longer.length) {
|
|
196
|
+
if (shorter[i] === longer[j]) {
|
|
197
|
+
i++;
|
|
198
|
+
j++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (++edits > 1) return false;
|
|
202
|
+
if (shorter.length === longer.length) i++;
|
|
203
|
+
j++;
|
|
204
|
+
}
|
|
205
|
+
return edits + (longer.length - j) + (shorter.length - i) <= 1;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The key in `known` that `key` was probably meant to be, if any.
|
|
209
|
+
*
|
|
210
|
+
* `undefined` when nothing is close — which is the common case for a key that
|
|
211
|
+
* is simply from a newer version, and the reason the caller must be able to say
|
|
212
|
+
* "unknown" without saying "wrong".
|
|
213
|
+
*/
|
|
214
|
+
function suggestNearMiss(known, key) {
|
|
215
|
+
return known.find((candidate) => isNearMiss(candidate, key));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The terms a user meant when they typed into a search box.
|
|
219
|
+
*
|
|
220
|
+
* A search box is not a substring box. Typing `sebastian melendez` on a
|
|
221
|
+
* collection whose name is split across `first_name` and `last_name` has to
|
|
222
|
+
* find the row, and matching the whole string against each field separately
|
|
223
|
+
* cannot: no single column holds both words. Splitting on whitespace and
|
|
224
|
+
* requiring *every* term to match *some* field is what does, and it is what
|
|
225
|
+
* `websearch_to_tsquery` already does for collections that declared a `search`
|
|
226
|
+
* block — so this is also what keeps the two search paths answering the same
|
|
227
|
+
* question.
|
|
228
|
+
*
|
|
229
|
+
* It fixes the trailing space too: `sebastian ` matched nothing at all, because
|
|
230
|
+
* the space was part of the pattern.
|
|
231
|
+
*
|
|
232
|
+
* A double-quoted run is one term, again matching the full-text path, so a
|
|
233
|
+
* caller who really does want the phrase can ask for it. An unbalanced quote is
|
|
234
|
+
* stripped rather than honoured — the alternative is that everything after a
|
|
235
|
+
* stray `"` silently becomes one long term that matches nothing.
|
|
236
|
+
*
|
|
237
|
+
* Returns an empty array for a string with no terms in it at all (whitespace,
|
|
238
|
+
* or nothing but quotes); the caller decides what an empty search means, since
|
|
239
|
+
* only it knows whether "no constraint" or "no rows" is the safe reading.
|
|
240
|
+
*/
|
|
241
|
+
function splitSearchTerms(searchString, maxTerms = 8) {
|
|
242
|
+
const terms = [];
|
|
243
|
+
const pattern = /"([^"]*)"|(\S+)/g;
|
|
244
|
+
let match;
|
|
245
|
+
while ((match = pattern.exec(searchString)) !== null) {
|
|
246
|
+
const term = (match[1] ?? match[2].replace(/"/g, "")).trim();
|
|
247
|
+
if (!term) continue;
|
|
248
|
+
terms.push(term);
|
|
249
|
+
if (terms.length >= maxTerms) break;
|
|
250
|
+
}
|
|
251
|
+
return terms;
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
export { MAX_LIST_LIMIT as a, RebaseClientError as c, ListLimitError as i, isUnsupported as l, suggestNearMiss as n, resolveClientListLimit as o, toSnakeCase as r, RebaseApiError as s, splitSearchTerms as t, unsupportedMethod as u };
|
|
255
|
+
|
|
256
|
+
//# sourceMappingURL=strings.es-BeZWAodn.js.map
|