@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.
Files changed (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -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":"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"}
@@ -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