@rebasepro/server 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{GCSStorageController-C7ARrDAp.js → GCSStorageController-40FyFeSx.js} +8 -4
- package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
- package/dist/{S3StorageController-qevJ0pVE.js → S3StorageController-Bl5shp94.js} +8 -4
- package/dist/S3StorageController-Bl5shp94.js.map +1 -0
- package/dist/access-BFAUvsSs.js +1897 -0
- package/dist/access-BFAUvsSs.js.map +1 -0
- package/dist/api/branch-routes.d.ts +25 -0
- package/dist/api/closed-database.d.ts +25 -0
- package/dist/api/live-schema-routes.d.ts +10 -0
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/idempotency.d.ts +15 -1
- package/dist/api/root-error-handler.d.ts +6 -5
- package/dist/api/types.d.ts +12 -0
- package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
- package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
- package/dist/auth/access.d.ts +10 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/adapter-service-key.d.ts +24 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
- package/dist/auth/api-keys/api-key-store.d.ts +8 -1
- package/dist/auth/api-keys/key-grant.d.ts +18 -1
- package/dist/auth/expired-token-sweep.d.ts +5 -0
- package/dist/auth/impersonation.d.ts +3 -1
- package/dist/auth/jwt.d.ts +8 -2
- package/dist/auth/oauth-signin-policy.d.ts +21 -35
- package/dist/auth/rls-scope.d.ts +17 -7
- package/dist/auth/rls-simulator-routes.d.ts +51 -0
- package/dist/auth/simulation-read-only.d.ts +28 -0
- package/dist/auth/simulation.d.ts +141 -0
- package/dist/auth/sql-rate-limit-store.d.ts +15 -1
- package/dist/{auth-CteGWxsL.js → auth-izI7AEzC.js} +391 -1893
- package/dist/auth-izI7AEzC.js.map +1 -0
- package/dist/auth_adapter-B7RUMk6e.js +89 -0
- package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
- package/dist/backend/boot-announcements.d.ts +13 -0
- package/dist/backend/create-backend.d.ts +10 -0
- package/dist/backend/options.d.ts +9 -4
- package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
- package/dist/backend-CaG_Jyso.js.map +1 -0
- package/dist/{backup-Dq0lyJ_D.js → backup-BVbHoKo8.js} +8 -8
- package/dist/{backup-Dq0lyJ_D.js.map → backup-BVbHoKo8.js.map} +1 -1
- package/dist/boot/boot.d.ts +2 -15
- package/dist/boot/ddl-bootstrap.d.ts +34 -0
- package/dist/boot/env.d.ts +6 -0
- package/dist/boot/fetch-bundle.d.ts +5 -1
- package/dist/boot/provision-only.d.ts +23 -0
- package/dist/boot/provision.d.ts +53 -2
- package/dist/boot/role.d.ts +2 -0
- package/dist/boot/schema-change-recorder.d.ts +45 -0
- package/dist/boot/security-headers.d.ts +22 -0
- package/dist/branch-routes-B2vvEx53.js +84 -0
- package/dist/branch-routes-B2vvEx53.js.map +1 -0
- package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CU2A3JnF.js} +3 -3
- package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
- package/dist/cron/cron-store.d.ts +8 -1
- package/dist/{cron-loader-NHqRfKF0.js → cron-loader-mMhqbVsl.js} +3 -3
- package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
- package/dist/{cron-routes-Dhf82HzT.js → cron-routes-Cs33j_Kr.js} +5 -5
- package/dist/{cron-routes-Dhf82HzT.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
- package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-Be7CNghL.js} +3 -3
- package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
- package/dist/{cron-store-DEpVstUU.js → cron-store-DLn11M8H.js} +11 -10
- package/dist/cron-store-DLn11M8H.js.map +1 -0
- package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
- package/dist/data_driver-D0mg1Y_x.js.map +1 -0
- package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-CjRrn12l.js} +92 -3
- package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +1 -1
- package/dist/email/dev-sink.d.ts +6 -0
- package/dist/email/index.d.ts +1 -1
- package/dist/{errors-_ytoGi8A.js → errors-OMWkKSH-.js} +66 -5
- package/dist/errors-OMWkKSH-.js.map +1 -0
- package/dist/{soft-delete-params-BAgpmBvp.js → field-access-DYsvjI_P.js} +27 -57
- package/dist/field-access-DYsvjI_P.js.map +1 -0
- package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
- package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
- package/dist/{function-loader-DbCSoNJj.js → function-loader-B8YzXEsQ.js} +22 -15
- package/dist/function-loader-B8YzXEsQ.js.map +1 -0
- package/dist/{function-routes-DNV4RUF0.js → function-routes-RxrCHNuM.js} +2 -2
- package/dist/{function-routes-DNV4RUF0.js.map → function-routes-RxrCHNuM.js.map} +1 -1
- package/dist/functions/hono-like.d.ts +12 -0
- package/dist/functions/index.js +37 -2
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +3 -3
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/history/retention-sweep.d.ts +5 -0
- package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-Bsau7h4X.js} +7 -7
- package/dist/history-recorder-Bsau7h4X.js.map +1 -0
- package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
- package/dist/history-store-BQz5YCkW.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.es.js +3003 -681
- package/dist/index.es.js.map +1 -1
- package/dist/init/process-safety.d.ts +4 -3
- package/dist/init/shutdown.d.ts +8 -0
- package/dist/init.d.ts +6 -1
- package/dist/jobs/job-store.d.ts +8 -1
- package/dist/{jobs-8G-_agld.js → jobs-Co6Ni5HP.js} +7 -7
- package/dist/jobs-Co6Ni5HP.js.map +1 -0
- package/dist/{jwt-YhciR93H.js → jwt-rDS0884_.js} +5 -4
- package/dist/{jwt-YhciR93H.js.map → jwt-rDS0884_.js.map} +1 -1
- package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
- package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
- package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
- package/dist/logger-CW8pmTS6.js.map +1 -0
- package/dist/{logs-routes-CelTiS7C.js → logs-routes-CBt7mMIL.js} +3 -3
- package/dist/{logs-routes-CelTiS7C.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
- package/dist/mcp/consent-page.d.ts +38 -1
- package/dist/mcp/mcp-routes.d.ts +9 -0
- package/dist/mcp/mcp-tools.d.ts +7 -1
- package/dist/mcp/oauth-store.d.ts +12 -5
- package/dist/metrics/history-recorder.d.ts +4 -1
- package/dist/metrics/history-store.d.ts +8 -0
- package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
- package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
- package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
- package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
- package/dist/{openapi-generator-BamY6-ga.js → openapi-generator-CyxEtYEX.js} +61 -22
- package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
- package/dist/{proxy-BEKbsil6.js → proxy-BYir8tWK.js} +5 -7
- package/dist/proxy-BYir8tWK.js.map +1 -0
- package/dist/{query-parser-CGohjyWI.js → query-parser-CDXWf2hH.js} +7 -6
- package/dist/{query-parser-CGohjyWI.js.map → query-parser-CDXWf2hH.js.map} +1 -1
- package/dist/{request-timeout-CRny5KD0.js → request-timeout-DVjo-c_8.js} +6 -6
- package/dist/request-timeout-DVjo-c_8.js.map +1 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
- package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
- package/dist/{schema-editor-routes-CnNGCayU.js → schema-editor-routes-Dc3DU_jm.js} +4 -4
- package/dist/{schema-editor-routes-CnNGCayU.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
- package/dist/soft-delete-params-BRONCOA2.js +59 -0
- package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
- package/dist/{sql-export-routes-BgTJ7K3S.js → sql-export-routes-QycX8W8S.js} +40 -6
- package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
- package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
- package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +2 -0
- package/dist/storage/LocalStorageController.d.ts +8 -3
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/maintenance-keys.d.ts +17 -0
- package/dist/storage/maintenance-routes.d.ts +28 -5
- package/dist/storage/maintenance.d.ts +136 -4
- package/dist/storage/orphan-scans.d.ts +120 -0
- package/dist/storage/types.d.ts +11 -0
- package/dist/strings.es-B7QLnHh2.js +96 -0
- package/dist/strings.es-B7QLnHh2.js.map +1 -0
- package/dist/{upload-validation-I9cCUBIW.js → upload-validation-Bgo626uY.js} +2 -2
- package/dist/upload-validation-Bgo626uY.js.map +1 -0
- package/dist/utils/dev-dependency-command.d.ts +15 -0
- package/dist/utils/dev-port.d.ts +8 -0
- package/dist/utils/logger.d.ts +24 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-C7ARrDAp.js.map +0 -1
- package/dist/S3StorageController-qevJ0pVE.js.map +0 -1
- package/dist/auth-CteGWxsL.js.map +0 -1
- package/dist/auth_adapter-CIItCTc8.js +0 -21
- package/dist/auth_adapter-CIItCTc8.js.map +0 -1
- package/dist/backend-DTAOsLQc.js.map +0 -1
- package/dist/cron-store-DEpVstUU.js.map +0 -1
- package/dist/ddl-bootstrap-BNqi2NZe.js.map +0 -1
- package/dist/errors-_ytoGi8A.js.map +0 -1
- package/dist/function-loader-DbCSoNJj.js.map +0 -1
- package/dist/history-recorder-RZ1Ow4YY.js.map +0 -1
- package/dist/history-store-DPofGVDp.js.map +0 -1
- package/dist/jobs-8G-_agld.js.map +0 -1
- package/dist/logger-Bt32sHFw.js.map +0 -1
- package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
- package/dist/openapi-generator-BamY6-ga.js.map +0 -1
- package/dist/proxy-BEKbsil6.js.map +0 -1
- package/dist/request-timeout-CRny5KD0.js.map +0 -1
- package/dist/soft-delete-params-BAgpmBvp.js.map +0 -1
- package/dist/sql-export-routes-BgTJ7K3S.js.map +0 -1
- package/dist/strings.es-BeZWAodn.js.map +0 -1
- package/dist/upload-validation-I9cCUBIW.js.map +0 -1
|
@@ -24,6 +24,8 @@ export declare class GCSStorageController implements StorageController {
|
|
|
24
24
|
* @see S3StorageController.knownBuckets
|
|
25
25
|
*/
|
|
26
26
|
knownBuckets(): string[];
|
|
27
|
+
/** See {@link StorageController.storageLocation}: bucket names are global. */
|
|
28
|
+
storageLocation(): string;
|
|
27
29
|
putObject({ file, key, metadata, bucket }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;
|
|
28
30
|
/**
|
|
29
31
|
* Read the object's metadata and nothing else — no signing.
|
|
@@ -67,6 +67,11 @@ export declare class LocalStorageController implements StorageController {
|
|
|
67
67
|
* not have been written to yet.
|
|
68
68
|
*/
|
|
69
69
|
knownBuckets(): string[];
|
|
70
|
+
/**
|
|
71
|
+
* See {@link StorageController.storageLocation}: the default bucket's
|
|
72
|
+
* directory, with the storage root's links resolved.
|
|
73
|
+
*/
|
|
74
|
+
storageLocation(): string;
|
|
70
75
|
/**
|
|
71
76
|
* Ensure directory exists, creating it if necessary
|
|
72
77
|
*/
|
|
@@ -128,11 +133,11 @@ export declare class LocalStorageController implements StorageController {
|
|
|
128
133
|
/**
|
|
129
134
|
* A folder's entries for a listing page.
|
|
130
135
|
*
|
|
131
|
-
* A page token
|
|
132
|
-
* page used to read the whole folder again to find its place: a folder of
|
|
136
|
+
* A page token names the last of these entries a page read, and every
|
|
137
|
+
* continuation page used to read the whole folder again to find its place: a folder of
|
|
133
138
|
* n entries listed in pages of p cost n/p full reads. A first page reads
|
|
134
139
|
* the folder and, when there is a next page, keeps the read; a
|
|
135
|
-
* continuation page
|
|
140
|
+
* continuation page finds its place in it while all of these hold:
|
|
136
141
|
*
|
|
137
142
|
* - the folder's mtime is the one read with it. Adding, removing or
|
|
138
143
|
* renaming an entry changes it, which is the invalidation;
|
|
@@ -24,6 +24,8 @@ export declare class S3StorageController implements StorageController {
|
|
|
24
24
|
* can reach — and a mistyped one came back as "file not found".
|
|
25
25
|
*/
|
|
26
26
|
knownBuckets(): string[];
|
|
27
|
+
/** See {@link StorageController.storageLocation}: the endpoint (AWS when none) and the bucket. */
|
|
28
|
+
storageLocation(): string;
|
|
27
29
|
/** See {@link StorageController.maxFileSize}. */
|
|
28
30
|
maxFileSize(): number;
|
|
29
31
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where storage maintenance keeps its own records inside a source: the state
|
|
3
|
+
* and result of an orphan scan (`rebase storage orphans`), which a worker
|
|
4
|
+
* writes and any server process reads back.
|
|
5
|
+
*
|
|
6
|
+
* Reserved at the route layer like the rendition space, in both directions. A
|
|
7
|
+
* scan's result is the list a later delete acts on, so a caller who could
|
|
8
|
+
* write one could name files for an administrator's delete to remove; and it
|
|
9
|
+
* lists keys a caller may not be allowed to see.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAINTENANCE_PREFIX = "_rebase/maintenance/";
|
|
12
|
+
/**
|
|
13
|
+
* True for a key in the reserved maintenance space — the prefix itself, or
|
|
14
|
+
* anything under it. Folded the way the filesystem under a local bucket folds
|
|
15
|
+
* it, as {@link isRenditionKey} is.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isMaintenanceKey(key: string): boolean;
|
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `/admin/storage` — the routes behind `rebase storage orphans` and
|
|
3
3
|
* `rebase storage move`. Admin-only; the caller mounts them behind the admin
|
|
4
|
-
* gate (`database:read` to read, `database:write`
|
|
4
|
+
* gate (`database:read` to read, `database:write` for every other method).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* POST /orphans/scans?storageId=&olderThanHours= start a scan, in the background
|
|
7
|
+
* GET /orphans/scans/:id?storageId= its progress, then its report
|
|
8
|
+
* DELETE /orphans?scan=:id&storageId= delete a completed scan's orphans, in the background
|
|
9
|
+
* GET /orphans?storageId=&olderThanHours= deprecated: a scan inside the request
|
|
10
|
+
* DELETE /orphans?storageId=&olderThanHours= deprecated: scan and delete inside the request
|
|
8
11
|
* POST /move { from, to, prefix?, cursor?, limit?, deleteSource? }
|
|
12
|
+
* POST /move/verify (the same body) what a move left behind
|
|
9
13
|
*/
|
|
10
14
|
import { Hono } from "hono";
|
|
11
|
-
import type { CollectionConfig, DataDriver } from "@rebasepro/types";
|
|
15
|
+
import type { CollectionCallbacks, CollectionConfig, DataDriver } from "@rebasepro/types";
|
|
12
16
|
import type { HonoEnv } from "../api/types.js";
|
|
17
|
+
import type { JobQueueClient } from "../jobs/types.js";
|
|
13
18
|
import type { StorageController } from "./types.js";
|
|
14
19
|
import type { StorageRegistry } from "./storage-registry.js";
|
|
20
|
+
import { type OrphanScanService } from "./orphan-scans.js";
|
|
15
21
|
export interface StorageMaintenanceRoutesConfig {
|
|
16
22
|
registry?: StorageRegistry;
|
|
17
23
|
controller?: StorageController;
|
|
18
24
|
/** The collections whose rows are read for references, read per request. */
|
|
19
25
|
collections: () => readonly CollectionConfig[];
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The unscoped driver serving a collection: every row, whoever wrote it.
|
|
28
|
+
* Its `scanStoredRows` is read when it has one — rows as stored, past
|
|
29
|
+
* every read hook.
|
|
30
|
+
*/
|
|
21
31
|
driverFor: (collection: CollectionConfig) => DataDriver | undefined;
|
|
32
|
+
/** The global callbacks, read per request: a global read hook makes every collection hooked. */
|
|
33
|
+
globalCallbacks?: () => CollectionCallbacks | undefined;
|
|
34
|
+
/** Row history, when kept: a file only an entry names is restored by a revert. */
|
|
35
|
+
history?: {
|
|
36
|
+
scanStoredValues?(pageSize?: number): AsyncIterable<unknown[]>;
|
|
37
|
+
};
|
|
38
|
+
/** The durable job queue a scan runs on, read per scan. Absent: scans run in this process. */
|
|
39
|
+
jobs?: () => JobQueueClient | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The scan service, when the backend shares one with the job queue's
|
|
42
|
+
* `storage.orphans` handler. Built from the fields above when left out.
|
|
43
|
+
*/
|
|
44
|
+
orphanScans?: OrphanScanService;
|
|
22
45
|
}
|
|
23
46
|
export declare function createStorageMaintenanceRoutes(config: StorageMaintenanceRoutesConfig): Hono<HonoEnv>;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* whichever property holds it. A file is reported only when nothing points at
|
|
12
12
|
* it at all.
|
|
13
13
|
*/
|
|
14
|
-
import type { CollectionConfig, DataDriver } from "@rebasepro/types";
|
|
14
|
+
import type { CollectionCallbacks, CollectionConfig, DataDriver } from "@rebasepro/types";
|
|
15
15
|
import type { StorageController } from "./types.js";
|
|
16
16
|
/** One stored object, as a listing describes it. */
|
|
17
17
|
export interface StoredObjectEntry {
|
|
@@ -51,6 +51,12 @@ export interface UnscannedCollection {
|
|
|
51
51
|
collection: string;
|
|
52
52
|
reason: string;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether reading a collection through `fetchCollection` could hide a row or
|
|
56
|
+
* change a value: a `beforeQuery` or an `afterRead`, the collection's own, a
|
|
57
|
+
* global one, or a property's.
|
|
58
|
+
*/
|
|
59
|
+
export declare function hasReadHooks(collection: CollectionConfig, globalCallbacks?: CollectionCallbacks): boolean;
|
|
54
60
|
/**
|
|
55
61
|
* Remove from `keys` every key some row names.
|
|
56
62
|
*
|
|
@@ -59,13 +65,28 @@ export interface UnscannedCollection {
|
|
|
59
65
|
* serves the collection. What is left in `keys` afterwards is named by no row.
|
|
60
66
|
* A collection that could not be read is returned, and its rows count for
|
|
61
67
|
* nothing: a caller must not treat what is left as unused when any is.
|
|
68
|
+
*
|
|
69
|
+
* Rows are read as stored, through the driver's `scanStoredRows`. A driver
|
|
70
|
+
* without it is read through `fetchCollection` only for a collection with no
|
|
71
|
+
* read hooks: a `beforeQuery` scoping rows to `context.user` — there is none
|
|
72
|
+
* here — or an `afterRead` rewriting a file field would hide a reference, and
|
|
73
|
+
* the file it names would be deleted as unused. Such a collection is
|
|
74
|
+
* returned as unscanned instead.
|
|
62
75
|
*/
|
|
63
76
|
export declare function removeReferencedKeys(keys: Set<string>, collections: readonly CollectionConfig[], driverFor: (collection: CollectionConfig) => DataDriver | undefined, options?: {
|
|
64
77
|
pageSize?: number;
|
|
78
|
+
globalCallbacks?: CollectionCallbacks;
|
|
79
|
+
/** Counted into as it goes, and `onProgress` called after each page. */
|
|
80
|
+
progress?: ScanProgress;
|
|
81
|
+
onProgress?: () => void;
|
|
82
|
+
/** Checked between pages: an aborted scan stops, and throws. */
|
|
83
|
+
signal?: AbortSignal;
|
|
65
84
|
}): Promise<{
|
|
66
85
|
scanned: string[];
|
|
67
86
|
unscanned: UnscannedCollection[];
|
|
68
87
|
}>;
|
|
88
|
+
/** How row history is named among the scanned and unscanned collections. */
|
|
89
|
+
export declare const HISTORY_SCAN_NAME = "(row history)";
|
|
69
90
|
/** What `findOrphans` found. */
|
|
70
91
|
export interface OrphanReport {
|
|
71
92
|
/** Objects in the source, internal ones aside. */
|
|
@@ -80,6 +101,42 @@ export interface OrphanReport {
|
|
|
80
101
|
scanned: string[];
|
|
81
102
|
unscanned: UnscannedCollection[];
|
|
82
103
|
}
|
|
104
|
+
/** Row history, as a scan reads it. */
|
|
105
|
+
export interface StoredHistory {
|
|
106
|
+
scanStoredValues?(pageSize?: number): AsyncIterable<unknown[]>;
|
|
107
|
+
}
|
|
108
|
+
/** Where references to stored files are read from: every row, and history. */
|
|
109
|
+
export interface ReferenceSources {
|
|
110
|
+
collections: readonly CollectionConfig[];
|
|
111
|
+
driverFor: (collection: CollectionConfig) => DataDriver | undefined;
|
|
112
|
+
/** The callbacks every collection runs, which may scope or rewrite its reads. */
|
|
113
|
+
globalCallbacks?: CollectionCallbacks;
|
|
114
|
+
/**
|
|
115
|
+
* Row history, when the backend keeps it: a revert restores the values an
|
|
116
|
+
* entry holds, so a file only history names is still in use. One that
|
|
117
|
+
* cannot be read is reported unscanned, as {@link HISTORY_SCAN_NAME}.
|
|
118
|
+
*/
|
|
119
|
+
history?: StoredHistory;
|
|
120
|
+
}
|
|
121
|
+
/** How far a scan has got. Counted into in place, so a caller can report it while it runs. */
|
|
122
|
+
export interface ScanProgress {
|
|
123
|
+
phase: "listing" | "rows" | "history" | "deleting" | "done";
|
|
124
|
+
/** Objects listed in the source so far. */
|
|
125
|
+
filesScanned: number;
|
|
126
|
+
/** Rows read so far, over every collection. */
|
|
127
|
+
rowsScanned: number;
|
|
128
|
+
/** History entries read so far. */
|
|
129
|
+
historyEntriesScanned: number;
|
|
130
|
+
/** Collections finished, read or not. */
|
|
131
|
+
collectionsScanned: number;
|
|
132
|
+
collectionsTotal: number;
|
|
133
|
+
/** The collection being read now. */
|
|
134
|
+
currentCollection?: string;
|
|
135
|
+
/** The collections that could not be read, as they are found. */
|
|
136
|
+
unscanned: UnscannedCollection[];
|
|
137
|
+
}
|
|
138
|
+
/** A progress record at the start of a scan. */
|
|
139
|
+
export declare function emptyScanProgress(): ScanProgress;
|
|
83
140
|
/**
|
|
84
141
|
* The objects of one source that no row names.
|
|
85
142
|
*
|
|
@@ -87,13 +144,52 @@ export interface OrphanReport {
|
|
|
87
144
|
* an orphan: the admin panel uploads a file before the row that names it
|
|
88
145
|
* is saved. One whose age the listing does not say is not either.
|
|
89
146
|
*/
|
|
90
|
-
export declare function findOrphans(options: {
|
|
147
|
+
export declare function findOrphans(options: ReferenceSources & {
|
|
91
148
|
controller: StorageController;
|
|
92
|
-
collections: readonly CollectionConfig[];
|
|
93
|
-
driverFor: (collection: CollectionConfig) => DataDriver | undefined;
|
|
94
149
|
olderThanMs: number;
|
|
95
150
|
now?: number;
|
|
151
|
+
/** Counted into as the scan goes; `onProgress` is called after each page. */
|
|
152
|
+
progress?: ScanProgress;
|
|
153
|
+
onProgress?: () => void;
|
|
154
|
+
/** Checked between pages: an aborted scan stops, and throws. */
|
|
155
|
+
signal?: AbortSignal;
|
|
96
156
|
}): Promise<OrphanReport>;
|
|
157
|
+
/** What deleting a scan's orphans did. */
|
|
158
|
+
export interface OrphanDeletion {
|
|
159
|
+
deleted: string[];
|
|
160
|
+
/** Candidates not deleted, because something changed since the scan. */
|
|
161
|
+
kept: {
|
|
162
|
+
key: string;
|
|
163
|
+
reason: string;
|
|
164
|
+
}[];
|
|
165
|
+
failed: {
|
|
166
|
+
key: string;
|
|
167
|
+
error: string;
|
|
168
|
+
}[];
|
|
169
|
+
/** Collections the re-check could not read; when any is, nothing was deleted. */
|
|
170
|
+
unscanned: UnscannedCollection[];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Delete the orphans an earlier scan found — each only if it is still one.
|
|
174
|
+
*
|
|
175
|
+
* Every row and history entry is read again, against the candidates alone
|
|
176
|
+
* (the source is not listed again): a row written since the scan may name one
|
|
177
|
+
* now. When a collection cannot be read, nothing is deleted. Then, right
|
|
178
|
+
* before each delete, the object is asked for again: one that is gone, was
|
|
179
|
+
* written again since the scan (another size or time), or is now inside the
|
|
180
|
+
* grace period is kept.
|
|
181
|
+
*/
|
|
182
|
+
export declare function deleteScannedOrphans(options: ReferenceSources & {
|
|
183
|
+
controller: StorageController;
|
|
184
|
+
candidates: readonly StoredObjectEntry[];
|
|
185
|
+
olderThanMs: number;
|
|
186
|
+
now?: () => number;
|
|
187
|
+
progress?: ScanProgress;
|
|
188
|
+
onProgress?: () => void;
|
|
189
|
+
/** Called after each file deleted. */
|
|
190
|
+
onDeleted?: (key: string) => void;
|
|
191
|
+
signal?: AbortSignal;
|
|
192
|
+
}): Promise<OrphanDeletion>;
|
|
97
193
|
/** What happened to one object in a move. */
|
|
98
194
|
export interface MovedObject {
|
|
99
195
|
key: string;
|
|
@@ -124,3 +220,39 @@ export declare function moveObjects(options: {
|
|
|
124
220
|
moved: MovedObject[];
|
|
125
221
|
cursor?: string;
|
|
126
222
|
}>;
|
|
223
|
+
/** A file a move did not move, and why it counts as not moved. */
|
|
224
|
+
export interface NotMovedObject {
|
|
225
|
+
key: string;
|
|
226
|
+
reason: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* List the source again after a move and name every file it did not move,
|
|
230
|
+
* `limit` source objects per call; hand the returned `cursor` back for the
|
|
231
|
+
* next ones.
|
|
232
|
+
*
|
|
233
|
+
* Defence in depth: a move reports what its own walk saw, and a walk that
|
|
234
|
+
* skipped files (as the local listing's page token once did, over a folder
|
|
235
|
+
* the move was emptying) reported success over the half it moved. A fresh
|
|
236
|
+
* listing does not share the walk's state. With `deleteSource`, every file
|
|
237
|
+
* still in the source is not moved; without it, a file the target holds no
|
|
238
|
+
* copy of the same size of.
|
|
239
|
+
*/
|
|
240
|
+
export declare function verifyMove(options: {
|
|
241
|
+
from: StorageController;
|
|
242
|
+
to: StorageController;
|
|
243
|
+
prefix?: string;
|
|
244
|
+
cursor?: string;
|
|
245
|
+
limit?: number;
|
|
246
|
+
deleteSource?: boolean;
|
|
247
|
+
}): Promise<{
|
|
248
|
+
checked: number;
|
|
249
|
+
notMoved: NotMovedObject[];
|
|
250
|
+
cursor?: string;
|
|
251
|
+
}>;
|
|
252
|
+
/**
|
|
253
|
+
* Whether two sources store in the same place — one bucket or directory under
|
|
254
|
+
* two names. Asked by their `storageLocation` when both can say; otherwise by
|
|
255
|
+
* a probe object written to `a` under the internal folder, looked for in `b`,
|
|
256
|
+
* and deleted.
|
|
257
|
+
*/
|
|
258
|
+
export declare function storesInSamePlace(a: StorageController, b: StorageController): Promise<boolean>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CollectionCallbacks, CollectionConfig, DataDriver } from "@rebasepro/types";
|
|
2
|
+
import type { JobQueueClient } from "../jobs/types.js";
|
|
3
|
+
import type { StorageController } from "./types.js";
|
|
4
|
+
import type { StorageRegistry } from "./storage-registry.js";
|
|
5
|
+
import { type OrphanReport, type ScanProgress, type StoredHistory } from "./maintenance.js";
|
|
6
|
+
/** The job task a scan, or a scan's delete, runs as. Frozen: workers of another release claim it by name. */
|
|
7
|
+
export declare const STORAGE_ORPHANS_TASK = "storage.orphans";
|
|
8
|
+
/** Where scan records are kept inside the scanned source. */
|
|
9
|
+
export declare const ORPHAN_SCAN_FOLDER = "_rebase/maintenance/orphan-scans";
|
|
10
|
+
/**
|
|
11
|
+
* How long after it completed a scan's result may still be deleted from.
|
|
12
|
+
* Every candidate is checked again before it goes, so this is not what keeps
|
|
13
|
+
* a file in use; it keeps a delete acting on what the person just read.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ORPHAN_SCAN_TTL_MS: number;
|
|
16
|
+
/**
|
|
17
|
+
* A scan, or a delete, that has written no progress for this long is reported
|
|
18
|
+
* failed: the process running it stopped — a deploy, a crash — and nothing
|
|
19
|
+
* else will finish it. Progress is written at least once a page.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ORPHAN_SCAN_STALL_MS: number;
|
|
22
|
+
/** The default grace period: an upload whose row is not saved yet is a day old at most. */
|
|
23
|
+
export declare const DEFAULT_OLDER_THAN_HOURS = 24;
|
|
24
|
+
/** Who runs a scan: the job queue's workers, or the server process that was asked. */
|
|
25
|
+
export type OrphanScanRunner = "worker" | "in-process";
|
|
26
|
+
export type OrphanScanStatus = "queued" | "running" | "completed" | "failed";
|
|
27
|
+
/** A delete from a completed scan, as its record holds it. */
|
|
28
|
+
export interface OrphanScanDeletion {
|
|
29
|
+
status: OrphanScanStatus;
|
|
30
|
+
runner: OrphanScanRunner;
|
|
31
|
+
jobId?: string;
|
|
32
|
+
requestedAt: string;
|
|
33
|
+
startedAt?: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
completedAt?: string;
|
|
36
|
+
progress: ScanProgress & {
|
|
37
|
+
candidates: number;
|
|
38
|
+
deleted: number;
|
|
39
|
+
};
|
|
40
|
+
result?: {
|
|
41
|
+
deleted: string[];
|
|
42
|
+
kept: {
|
|
43
|
+
key: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}[];
|
|
46
|
+
failed: {
|
|
47
|
+
key: string;
|
|
48
|
+
error: string;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
error?: string;
|
|
52
|
+
/** Set on the way out when a running delete has stopped reporting. */
|
|
53
|
+
note?: string;
|
|
54
|
+
}
|
|
55
|
+
/** A scan's record: what `GET /orphans/scans/:id` answers. */
|
|
56
|
+
export interface OrphanScanState {
|
|
57
|
+
id: string;
|
|
58
|
+
storageId: string;
|
|
59
|
+
olderThanHours: number;
|
|
60
|
+
runner: OrphanScanRunner;
|
|
61
|
+
jobId?: string;
|
|
62
|
+
status: OrphanScanStatus;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
startedAt?: string;
|
|
65
|
+
updatedAt: string;
|
|
66
|
+
completedAt?: string;
|
|
67
|
+
progress: ScanProgress;
|
|
68
|
+
/** The report, once completed: what the synchronous `GET /orphans` answered. */
|
|
69
|
+
result?: OrphanReport;
|
|
70
|
+
error?: string;
|
|
71
|
+
/** Said on the way out, never stored: why a queued scan may not have started, or that one stalled. */
|
|
72
|
+
note?: string;
|
|
73
|
+
deletion?: OrphanScanDeletion;
|
|
74
|
+
}
|
|
75
|
+
/** A `storage.orphans` job's payload. */
|
|
76
|
+
export interface OrphanScanTaskPayload {
|
|
77
|
+
scanId: string;
|
|
78
|
+
storageId: string;
|
|
79
|
+
mode: "scan" | "delete";
|
|
80
|
+
}
|
|
81
|
+
export interface OrphanScanServiceConfig {
|
|
82
|
+
registry?: StorageRegistry;
|
|
83
|
+
controller?: StorageController;
|
|
84
|
+
/** The collections whose rows are read for references, read when a scan runs. */
|
|
85
|
+
collections: () => readonly CollectionConfig[];
|
|
86
|
+
/** The unscoped driver serving a collection. See `StorageMaintenanceRoutesConfig.driverFor`. */
|
|
87
|
+
driverFor: (collection: CollectionConfig) => DataDriver | undefined;
|
|
88
|
+
globalCallbacks?: () => CollectionCallbacks | undefined;
|
|
89
|
+
history?: StoredHistory;
|
|
90
|
+
/**
|
|
91
|
+
* The durable job queue, read when a scan starts: present, the scan is a
|
|
92
|
+
* `storage.orphans` job its workers run; absent, it runs in this process.
|
|
93
|
+
*/
|
|
94
|
+
jobs?: () => JobQueueClient | undefined;
|
|
95
|
+
/** For tests. */
|
|
96
|
+
now?: () => number;
|
|
97
|
+
}
|
|
98
|
+
export interface OrphanScanService {
|
|
99
|
+
/** Record a scan and set it going. Resolves with its record, `queued`. */
|
|
100
|
+
start(options: {
|
|
101
|
+
storageId?: string;
|
|
102
|
+
olderThanHours?: number;
|
|
103
|
+
}): Promise<OrphanScanState>;
|
|
104
|
+
/** A scan's record, or `undefined` when this source holds none by that id. */
|
|
105
|
+
status(storageId: string | undefined, id: string): Promise<OrphanScanState | undefined>;
|
|
106
|
+
/** Start deleting a completed scan's orphans. Throws the refusal when it may not. */
|
|
107
|
+
requestDeletion(storageId: string | undefined, id: string): Promise<OrphanScanState>;
|
|
108
|
+
/** Run a `storage.orphans` job: the scan, or its delete. */
|
|
109
|
+
runTask(payload: OrphanScanTaskPayload, signal?: AbortSignal): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The storage source a request names, or the 404 that lists the ones served.
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveStorageSource(sources: {
|
|
115
|
+
registry?: StorageRegistry;
|
|
116
|
+
controller?: StorageController;
|
|
117
|
+
}, rawId: unknown, label: string): StorageController;
|
|
118
|
+
/** `olderThanHours` as a request sends it, checked. */
|
|
119
|
+
export declare function parseOlderThanHours(raw: string | number | undefined): number;
|
|
120
|
+
export declare function createOrphanScanService(config: OrphanScanServiceConfig): OrphanScanService;
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -210,6 +210,17 @@ export interface StorageController {
|
|
|
210
210
|
* when the caller names none, and each provider maps it to its own.
|
|
211
211
|
*/
|
|
212
212
|
knownBuckets?(): string[];
|
|
213
|
+
/**
|
|
214
|
+
* Where this controller's default bucket physically is, as a string two
|
|
215
|
+
* controllers return alike exactly when they store in the same place: a
|
|
216
|
+
* resolved directory, an endpoint and bucket.
|
|
217
|
+
*
|
|
218
|
+
* Two sources declared over one bucket or directory are one store under
|
|
219
|
+
* two names, and `rebase storage move --delete-source` between them found
|
|
220
|
+
* every object "already there" — it is the same object — and deleted it
|
|
221
|
+
* from both. A controller without this is found out by a probe object.
|
|
222
|
+
*/
|
|
223
|
+
storageLocation?(): string;
|
|
213
224
|
/**
|
|
214
225
|
* The largest file this controller accepts, in bytes.
|
|
215
226
|
*
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
var snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;
|
|
6
|
+
var toSnakeCase = (str) => {
|
|
7
|
+
if (!str || typeof str !== "string") return "";
|
|
8
|
+
const regExpMatchArray = str.match(snakeCaseRegex);
|
|
9
|
+
if (!regExpMatchArray) return "";
|
|
10
|
+
return regExpMatchArray.map((x) => x.toLowerCase()).join("_");
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Are these two identifiers one edit apart, ignoring case?
|
|
14
|
+
*
|
|
15
|
+
* Tight on purpose. A suggester that fires on two edits offers `columnWidth`
|
|
16
|
+
* for `colWith` and `readOnly` for `required`, and a "did you mean" that is
|
|
17
|
+
* usually wrong teaches people to stop reading them. One edit covers what
|
|
18
|
+
* actually happens — a dropped letter (`multilne`), a doubled one, a transposed
|
|
19
|
+
* pair (`validaton`), a wrong case — and nothing else.
|
|
20
|
+
*
|
|
21
|
+
* Lives here rather than beside either caller because both the CLI's manifest
|
|
22
|
+
* reader and the server's collection validator answer the same question about
|
|
23
|
+
* an unknown key, and two copies of a similarity rule is two rules the moment
|
|
24
|
+
* one of them is tuned.
|
|
25
|
+
*/
|
|
26
|
+
function isNearMiss(a, b) {
|
|
27
|
+
const x = a.toLowerCase();
|
|
28
|
+
const y = b.toLowerCase();
|
|
29
|
+
if (x === y) return true;
|
|
30
|
+
if (Math.abs(x.length - y.length) > 1) return false;
|
|
31
|
+
const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
|
|
32
|
+
let i = 0;
|
|
33
|
+
let j = 0;
|
|
34
|
+
let edits = 0;
|
|
35
|
+
while (i < shorter.length && j < longer.length) {
|
|
36
|
+
if (shorter[i] === longer[j]) {
|
|
37
|
+
i++;
|
|
38
|
+
j++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (++edits > 1) return false;
|
|
42
|
+
if (shorter.length === longer.length) i++;
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
return edits + (longer.length - j) + (shorter.length - i) <= 1;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The key in `known` that `key` was probably meant to be, if any.
|
|
49
|
+
*
|
|
50
|
+
* `undefined` when nothing is close — which is the common case for a key that
|
|
51
|
+
* is simply from a newer version, and the reason the caller must be able to say
|
|
52
|
+
* "unknown" without saying "wrong".
|
|
53
|
+
*/
|
|
54
|
+
function suggestNearMiss(known, key) {
|
|
55
|
+
return known.find((candidate) => isNearMiss(candidate, key));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The terms a user meant when they typed into a search box.
|
|
59
|
+
*
|
|
60
|
+
* A search box is not a substring box. Typing `sebastian melendez` on a
|
|
61
|
+
* collection whose name is split across `first_name` and `last_name` has to
|
|
62
|
+
* find the row, and matching the whole string against each field separately
|
|
63
|
+
* cannot: no single column holds both words. Splitting on whitespace and
|
|
64
|
+
* requiring *every* term to match *some* field is what does, and it is what
|
|
65
|
+
* `websearch_to_tsquery` already does for collections that declared a `search`
|
|
66
|
+
* block — so this is also what keeps the two search paths answering the same
|
|
67
|
+
* question.
|
|
68
|
+
*
|
|
69
|
+
* It fixes the trailing space too: `sebastian ` matched nothing at all, because
|
|
70
|
+
* the space was part of the pattern.
|
|
71
|
+
*
|
|
72
|
+
* A double-quoted run is one term, again matching the full-text path, so a
|
|
73
|
+
* caller who really does want the phrase can ask for it. An unbalanced quote is
|
|
74
|
+
* stripped rather than honoured — the alternative is that everything after a
|
|
75
|
+
* stray `"` silently becomes one long term that matches nothing.
|
|
76
|
+
*
|
|
77
|
+
* Returns an empty array for a string with no terms in it at all (whitespace,
|
|
78
|
+
* or nothing but quotes); the caller decides what an empty search means, since
|
|
79
|
+
* only it knows whether "no constraint" or "no rows" is the safe reading.
|
|
80
|
+
*/
|
|
81
|
+
function splitSearchTerms(searchString, maxTerms = 8) {
|
|
82
|
+
const terms = [];
|
|
83
|
+
const pattern = /"([^"]*)"|(\S+)/g;
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = pattern.exec(searchString)) !== null) {
|
|
86
|
+
const term = (match[1] ?? match[2].replace(/"/g, "")).trim();
|
|
87
|
+
if (!term) continue;
|
|
88
|
+
terms.push(term);
|
|
89
|
+
if (terms.length >= maxTerms) break;
|
|
90
|
+
}
|
|
91
|
+
return terms;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { suggestNearMiss as n, toSnakeCase as r, splitSearchTerms as t };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=strings.es-B7QLnHh2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strings.es-B7QLnHh2.js","names":[],"sources":["../../utils/dist/strings.es.js"],"sourcesContent":["//#region src/strings.ts\nvar tokenizeRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;\nvar toKebabCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(tokenizeRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"-\");\n};\nvar snakeCaseRegex = tokenizeRegex;\nvar toSnakeCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(snakeCaseRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"_\");\n};\nfunction camelCase(str) {\n\tif (!str) return \"\";\n\tif (str.length === 1) return str.toLowerCase();\n\tconst parts = str.split(/[-_ ]+/).filter(Boolean);\n\tif (parts.length === 0) return \"\";\n\treturn parts[0].toLowerCase() + parts.slice(1).map((part) => part.charAt(0).toUpperCase() + part.substring(1).toLowerCase()).join(\"\");\n}\n/**\n* A random base-36 string of exactly `strLength` characters.\n*\n* Not `Math.random().toString(36).slice(2, 2 + strLength)`: that has no\n* guaranteed length. Base-36 of a double drops trailing zeros, so the source\n* string is short about once in 36 calls and the slice quietly returns fewer\n* characters than asked for — `randomString(10)` returning 9. These values\n* prefix uploaded filenames to keep them apart, so a short one is a likelier\n* collision, and it fails at the rate that makes a test look flaky.\n*/\nfunction randomString(strLength = 5) {\n\tconst alphabet = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\tlet result = \"\";\n\tfor (let i = 0; i < strLength; i++) result += alphabet.charAt(Math.floor(Math.random() * 36));\n\treturn result;\n}\nfunction randomColor() {\n\treturn Math.floor(Math.random() * 16777215).toString(16);\n}\nfunction slugify(text, separator = \"_\", lowercase = true) {\n\tif (!text) return \"\";\n\tconst from = \"ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;-\";\n\tconst to = `aaaaaeeeeeiiiiooooouuuunc${separator}${separator}${separator}${separator}${separator}${separator}${separator}`;\n\tfor (let i = 0, l = 32; i < l; i++) text = text.replace(new RegExp(from.charAt(i), \"g\"), to.charAt(i));\n\ttext = text.toString().trim().replace(/^\\s+|\\s+$/g, \"\").replace(/\\s+/g, separator).replace(/&/g, separator).replace(/[^\\w\\\\-]+/g, \"\").replace(new RegExp(\"\\\\\" + separator + \"\\\\\" + separator + \"+\", \"g\"), separator);\n\treturn lowercase ? text.toLowerCase() : text;\n}\nfunction unslugify(slug) {\n\tif (!slug) return \"\";\n\tif (slug.includes(\"-\") || slug.includes(\"_\") || !slug.includes(\" \")) return slug.replace(/[-_]/g, \" \").replace(/\\w\\S*/g, function(txt) {\n\t\treturn txt.charAt(0).toUpperCase() + txt.substring(1);\n\t}).trim();\n\telse return slug.trim();\n}\n/**\n* Are these two identifiers one edit apart, ignoring case?\n*\n* Tight on purpose. A suggester that fires on two edits offers `columnWidth`\n* for `colWith` and `readOnly` for `required`, and a \"did you mean\" that is\n* usually wrong teaches people to stop reading them. One edit covers what\n* actually happens — a dropped letter (`multilne`), a doubled one, a transposed\n* pair (`validaton`), a wrong case — and nothing else.\n*\n* Lives here rather than beside either caller because both the CLI's manifest\n* reader and the server's collection validator answer the same question about\n* an unknown key, and two copies of a similarity rule is two rules the moment\n* one of them is tuned.\n*/\nfunction isNearMiss(a, b) {\n\tconst x = a.toLowerCase();\n\tconst y = b.toLowerCase();\n\tif (x === y) return true;\n\tif (Math.abs(x.length - y.length) > 1) return false;\n\tconst [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];\n\tlet i = 0;\n\tlet j = 0;\n\tlet edits = 0;\n\twhile (i < shorter.length && j < longer.length) {\n\t\tif (shorter[i] === longer[j]) {\n\t\t\ti++;\n\t\t\tj++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (++edits > 1) return false;\n\t\tif (shorter.length === longer.length) i++;\n\t\tj++;\n\t}\n\treturn edits + (longer.length - j) + (shorter.length - i) <= 1;\n}\n/**\n* The key in `known` that `key` was probably meant to be, if any.\n*\n* `undefined` when nothing is close — which is the common case for a key that\n* is simply from a newer version, and the reason the caller must be able to say\n* \"unknown\" without saying \"wrong\".\n*/\nfunction suggestNearMiss(known, key) {\n\treturn known.find((candidate) => isNearMiss(candidate, key));\n}\nfunction prettifyIdentifier(input) {\n\tif (!input) return \"\";\n\tlet text = input;\n\ttext = text.replace(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/g, \"$1$3 $2$4\");\n\ttext = text.replace(/[_-]+/g, \" \");\n\treturn text.trim().replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n/**\n* How many terms one search string is allowed to contribute.\n*\n* Every term multiplies the work: the substring fallback compiles to one\n* `ILIKE '%term%'` per term *per searchable column*, evaluated per row on a\n* sequential scan. Eight is far past what a person types into a search box and\n* still bounds a pasted paragraph to a fixed number of predicates. Terms past\n* the cap are dropped rather than rejected — dropping only widens the result\n* set, and a search box that errors on a long paste is worse than one that\n* matches on its first eight words.\n*/\nvar MAX_SEARCH_TERMS = 8;\n/**\n* The terms a user meant when they typed into a search box.\n*\n* A search box is not a substring box. Typing `sebastian melendez` on a\n* collection whose name is split across `first_name` and `last_name` has to\n* find the row, and matching the whole string against each field separately\n* cannot: no single column holds both words. Splitting on whitespace and\n* requiring *every* term to match *some* field is what does, and it is what\n* `websearch_to_tsquery` already does for collections that declared a `search`\n* block — so this is also what keeps the two search paths answering the same\n* question.\n*\n* It fixes the trailing space too: `sebastian ` matched nothing at all, because\n* the space was part of the pattern.\n*\n* A double-quoted run is one term, again matching the full-text path, so a\n* caller who really does want the phrase can ask for it. An unbalanced quote is\n* stripped rather than honoured — the alternative is that everything after a\n* stray `\"` silently becomes one long term that matches nothing.\n*\n* Returns an empty array for a string with no terms in it at all (whitespace,\n* or nothing but quotes); the caller decides what an empty search means, since\n* only it knows whether \"no constraint\" or \"no rows\" is the safe reading.\n*/\nfunction splitSearchTerms(searchString, maxTerms = MAX_SEARCH_TERMS) {\n\tconst terms = [];\n\tconst pattern = /\"([^\"]*)\"|(\\S+)/g;\n\tlet match;\n\twhile ((match = pattern.exec(searchString)) !== null) {\n\t\tconst term = (match[1] ?? match[2].replace(/\"/g, \"\")).trim();\n\t\tif (!term) continue;\n\t\tterms.push(term);\n\t\tif (terms.length >= maxTerms) break;\n\t}\n\treturn terms;\n}\n//#endregion\nexport { MAX_SEARCH_TERMS, camelCase, isNearMiss, prettifyIdentifier, randomColor, randomString, slugify, splitSearchTerms, suggestNearMiss, toKebabCase, toSnakeCase, unslugify };\n\n//# sourceMappingURL=strings.es.js.map"],"mappings":";;;;AAQA,IAAI,iBAAiB;AACrB,IAAI,eAAe,QAAQ;CAC1B,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU,OAAO;CAC5C,MAAM,mBAAmB,IAAI,MAAM,cAAc;CACjD,IAAI,CAAC,kBAAkB,OAAO;CAC9B,OAAO,iBAAiB,KAAK,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,GAAG;AAC7D;;;;;;;;;;;;;;;AAwDA,SAAS,WAAW,GAAG,GAAG;CACzB,MAAM,IAAI,EAAE,YAAY;CACxB,MAAM,IAAI,EAAE,YAAY;CACxB,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO;CAC9C,MAAM,CAAC,SAAS,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;CAC/D,IAAI,IAAI;CACR,IAAI,IAAI;CACR,IAAI,QAAQ;CACZ,OAAO,IAAI,QAAQ,UAAU,IAAI,OAAO,QAAQ;EAC/C,IAAI,QAAQ,OAAO,OAAO,IAAI;GAC7B;GACA;GACA;EACD;EACA,IAAI,EAAE,QAAQ,GAAG,OAAO;EACxB,IAAI,QAAQ,WAAW,OAAO,QAAQ;EACtC;CACD;CACA,OAAO,SAAS,OAAO,SAAS,MAAM,QAAQ,SAAS,MAAM;AAC9D;;;;;;;;AAQA,SAAS,gBAAgB,OAAO,KAAK;CACpC,OAAO,MAAM,MAAM,cAAc,WAAW,WAAW,GAAG,CAAC;AAC5D;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAS,iBAAiB,cAAc,WAAA,GAA6B;CACpE,MAAM,QAAQ,CAAC;CACf,MAAM,UAAU;CAChB,IAAI;CACJ,QAAQ,QAAQ,QAAQ,KAAK,YAAY,OAAO,MAAM;EACrD,MAAM,QAAQ,MAAM,MAAM,MAAM,EAAE,CAAC,QAAQ,MAAM,EAAE,EAAA,CAAG,KAAK;EAC3D,IAAI,CAAC,MAAM;EACX,MAAM,KAAK,IAAI;EACf,IAAI,MAAM,UAAU,UAAU;CAC/B;CACA,OAAO;AACR"}
|
|
@@ -2,7 +2,7 @@ import { createRequire as __rebaseCreateRequire } from "module";
|
|
|
2
2
|
import __rebaseProcess from "process";
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
import { t as ApiError } from "./errors-
|
|
5
|
+
import { t as ApiError } from "./errors-OMWkKSH-.js";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
//#region src/storage/types.ts
|
|
8
8
|
/**
|
|
@@ -169,4 +169,4 @@ function validateUploadedFile(file, maxSize, allowedMimeTypes) {
|
|
|
169
169
|
//#endregion
|
|
170
170
|
export { readStreamFully as a, DOCUMENT_MIME_TYPES as c, fileWebStream as i, IMAGE_MIME_TYPES as l, bufferWebStream as n, webStreamFrom as o, bytesView as r, DEFAULT_MAX_FILE_SIZE as s, validateUploadedFile as t };
|
|
171
171
|
|
|
172
|
-
//# sourceMappingURL=upload-validation-
|
|
172
|
+
//# sourceMappingURL=upload-validation-Bgo626uY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-validation-Bgo626uY.js","names":[],"sources":["../src/storage/types.ts","../src/storage/streams.ts","../src/storage/upload-validation.ts"],"sourcesContent":["/**\n * Storage configuration and types for Rebase backend\n */\n\nimport { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, DownloadMetadata, StorageListResult, StorageReference } from \"@rebasepro/types\";\n\n/**\n * Local filesystem storage configuration\n */\nexport interface LocalStorageConfig {\n type: \"local\";\n /** Base directory for file storage (e.g., './uploads') */\n basePath: string;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** Base URL for generating download URLs (default: auto-detected from request) */\n baseUrl?: string;\n /**\n * Set when this local backend stands in for an object store the source\n * declared and nothing bound — `bucket(\"media\", { engine: \"s3\" })` with no\n * `S3_BUCKET__MEDIA` in a development process. Names the engine it stands\n * in for, so boot can say so and `rebase status` can show it. Never set in\n * production, where an unbound object store stays unbound.\n */\n standsInFor?: string;\n /**\n * Set when nothing chose this backend: no `STORAGE_TYPE` and no declared\n * engine, so local disk is only the default a source falls back to.\n * Production drops a local backend either way. This only decides how that\n * is reported: an implicit default being dropped means the project uses no\n * storage, which is not a misconfiguration.\n */\n implicit?: boolean;\n}\n\n/**\n * S3-compatible storage configuration (works with AWS S3 and MinIO)\n */\nexport interface S3StorageConfig {\n type: \"s3\";\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g., 'us-east-1') */\n region?: string;\n /** Custom endpoint URL (required for MinIO, Cloudflare R2, Hetzner Object Storage) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId: string;\n /** AWS secret access key */\n secretAccessKey: string;\n /** Use path-style URLs (required for MinIO) */\n forcePathStyle?: boolean;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n}\n\n/**\n * Google Cloud Storage configuration (works with GCS and Firebase Storage)\n */\nexport interface GCSStorageConfig {\n type: \"gcs\";\n /** GCS bucket name (e.g. \"my-project.appspot.com\" for Firebase Storage) */\n bucket: string;\n /** GCP project ID (optional, auto-detected from credentials) */\n projectId?: string;\n /** Path to service account JSON key file */\n keyFilename?: string;\n /** Service account credentials object (alternative to keyFilename) */\n credentials?: { client_email: string; private_key: string; project_id?: string };\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types */\n allowedMimeTypes?: string[];\n}\n\n/**\n * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.\n *\n * **Built-in providers:**\n * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).\n * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,\n * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).\n * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.\n *\n * **Custom providers:**\n * For other cloud storage (Azure Blob, etc.), implement the `StorageController`\n * interface and pass the instance directly to the `storage` config.\n */\nexport type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;\n\n/** What a storage request is trying to do to an object. */\nexport type {\n StorageOperation,\n StorageAuthorizeUser,\n StorageAuthorizeContext,\n StorageAuthorizeData,\n StorageAuthorize\n} from \"@rebasepro/types\";\n\n/**\n * How the server writes an object. See {@link StorageController.putObject}.\n */\nexport interface PutObjectOptions {\n /**\n * Hold this write to the controller's upload limits: `maxFileSize` and\n * `allowedMimeTypes`. Defaults to `true`.\n *\n * Those limits say what a user may upload. A write the server makes for\n * itself, such as a database backup, passes `false`. A dump is neither of\n * an allowed type nor, past a small database, under 50 MB. The upload\n * routes never pass it.\n */\n uploadLimits?: boolean;\n}\n\n/**\n * Storage controller interface for backend implementations\n */\nexport interface StorageController {\n /**\n * Upload an object.\n *\n * A write is held to the controller's upload limits — `maxFileSize` and\n * `allowedMimeTypes` — unless `options.uploadLimits` is `false`. Only the\n * server's own writes pass that; nothing a request sends reaches it.\n */\n putObject(props: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;\n\n /**\n * Get a download URL (signed URL equivalent) for an object.\n *\n * For server-side code only: no route hands this URL to a client. A\n * client reads a private object through `/file/*` with the download\n * token `GET /metadata/*` mints. The built-in S3 and GCS controllers sign\n * it for an hour.\n */\n getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;\n\n /**\n * Describe an object — size, type, custom metadata — without producing a\n * URL for it, or `null` when there is no such object.\n *\n * `GET /metadata/*` wants only this: the client builds its own `/file/*`\n * URL and spends the download token the route mints. Asking\n * `getSignedUrl` for it made every metadata read sign a URL nobody used,\n * and on GCS signing is not free — without a key file the client asks the\n * IAM Credentials API to sign, which needs `iam.serviceAccounts.signBlob`\n * on the runtime account itself. Cloud Run does not grant that, so every\n * private object answered 500 there.\n *\n * A controller that does not implement this keeps the old behaviour: the\n * route falls back to `getSignedUrl` and reads its `metadata`.\n */\n getMetadata?(key: string, bucket?: string): Promise<DownloadMetadata | null>;\n\n /**\n * Get object as a File\n */\n getObject(key: string, bucket?: string): Promise<File | null>;\n\n /**\n * Describe an object for serving it — its size, stored type and version —\n * without reading its bytes, or `null` when there is no such object.\n *\n * Optional, together with {@link getObjectStream}. A controller that has\n * both is served streamed: `GET /file/*` answers a `304`, a `416`, a `HEAD`\n * and a cached image rendition from this alone, and otherwise reads only\n * the bytes it sends — the requested range, a chunk at a time. One without\n * them is served through {@link getObject}, which holds the whole object in\n * memory for every request, a `Range` request for one megabyte included.\n */\n statObject?(key: string, bucket?: string): Promise<StoredObjectInfo | null>;\n\n /**\n * Read an object, or one byte range of it, as a stream, or `null` when\n * there is no such object. See {@link statObject}.\n *\n * The stream holds a file or a connection until it is read to the end or\n * cancelled, so a caller that decides not to read it must cancel it.\n */\n getObjectStream?(key: string, options?: { bucket?: string; range?: StoredObjectRange }): Promise<StoredObjectStream | null>;\n\n /**\n * Delete an object\n */\n deleteObject(key: string, bucket?: string): Promise<void>;\n\n /**\n * List objects in a prefix\n */\n listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult>;\n\n /**\n * Get the storage provider identifier.\n *\n * Built-in values are `'local'` and `'s3'`. Custom implementations\n * should return their own identifier (e.g. `'gcs'`, `'azure'`).\n */\n getType(): string;\n\n /**\n * The buckets this controller serves, if it knows.\n *\n * The `bucket` a request may name is not a free-text field: on S3 and GCS it\n * goes straight to the provider, so an unvalidated one addresses *any*\n * bucket the deployment's credentials can reach, and locally it names a\n * directory the framework would create on demand. Either way, a caller who\n * mistypes it gets \"file not found\" — the same answer as a missing object —\n * with nothing to say the bucket was the problem.\n *\n * So a controller that knows its buckets says so, and the routes refuse\n * anything else with `UNKNOWN_STORAGE_SOURCE` naming what is served. A\n * controller that does not implement this keeps the old behaviour and is\n * handed whatever the caller wrote — a custom implementation may address\n * buckets this framework has no way to enumerate.\n *\n * `\"default\"` is always among them: it is the logical name every route uses\n * when the caller names none, and each provider maps it to its own.\n */\n knownBuckets?(): string[];\n\n /**\n * Where this controller's default bucket physically is, as a string two\n * controllers return alike exactly when they store in the same place: a\n * resolved directory, an endpoint and bucket.\n *\n * Two sources declared over one bucket or directory are one store under\n * two names, and `rebase storage move --delete-source` between them found\n * every object \"already there\" — it is the same object — and deleted it\n * from both. A controller without this is found out by a probe object.\n */\n storageLocation?(): string;\n\n /**\n * The largest file this controller accepts, in bytes.\n *\n * Every door that takes a file reads it, so the number a client is told is\n * the number it is held to: the upload route checks it before anything is\n * stored, the resumable route checks it at creation — before the first\n * chunk rather than after the last — and advertises it as `Tus-Max-Size`,\n * and the server's upload body limit is the largest of them. A controller\n * that does not implement this is held to the deployment-wide limit alone.\n */\n maxFileSize?(): number;\n}\n\n/**\n * What serving an object needs to know before deciding whether to read it.\n * See {@link StorageController.statObject}.\n */\nexport interface StoredObjectInfo {\n /** The whole object's size in bytes. */\n size: number;\n /** The stored content type — the uploader's claim, not a sniffed one. */\n contentType: string;\n /**\n * The object's own modification time, in milliseconds since the epoch.\n * The routes build `ETag`, `Last-Modified` and the transform cache key from\n * it, so it must be the object's version, not the time of the read.\n */\n lastModified: number;\n}\n\n/** An inclusive byte range, as `Range: bytes=start-end` names one. */\nexport interface StoredObjectRange {\n start: number;\n end: number;\n}\n\n/** An object's bytes as a stream. See {@link StorageController.getObjectStream}. */\nexport interface StoredObjectStream extends StoredObjectInfo {\n /** The bytes: the range when {@link range} is set, the whole object otherwise. */\n stream: ReadableStream<Uint8Array>;\n /**\n * The range {@link stream} holds — the one asked for — or absent when it\n * holds the whole object. A controller may answer a range with the whole\n * object, as HTTP lets a server do; the caller then serves it whole.\n */\n range?: StoredObjectRange;\n}\n\n/**\n * Default maximum file size (50MB)\n */\nexport const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;\n\n/**\n * Common image MIME types\n */\nexport const IMAGE_MIME_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/bmp\",\n \"image/tiff\"\n];\n\n/**\n * Common document MIME types\n */\nexport const DOCUMENT_MIME_TYPES = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\"\n];\n","/**\n * Moving object bytes without holding them.\n *\n * A stored object can be as large as the deployment's `maxFileSize` allows — a\n * video, a database dump — and the paths that used to read one whole (a\n * `File`, an `arrayBuffer()`, a `Buffer.concat`) held it three or four times\n * over per request. These helpers are what the streamed paths share instead.\n */\nimport fs from \"node:fs\";\n\n/** Anything that hands over bytes a chunk at a time: a Node readable, an SDK body. */\ntype ByteSource = AsyncIterable<Uint8Array>;\n\n/**\n * A byte source as a web stream that reads a chunk only when the consumer asks\n * for one, and releases the source when the consumer stops early.\n *\n * Pull-based with a zero high-water mark, so nothing is read ahead of the\n * reader: a slow client holds one chunk, not a queue of them. A source given\n * as a function is opened on the first read, which keeps a stream nobody reads\n * — a `HEAD`, a 304 decided late — from holding a file descriptor or a\n * connection open. Cancelling (a client that disconnects mid-download)\n * returns the iterator, which destroys a Node stream and so closes its file or\n * socket.\n */\nexport function webStreamFrom(source: ByteSource | (() => ByteSource)): ReadableStream<Uint8Array> {\n let iterator: AsyncIterator<Uint8Array> | undefined;\n const open = (): AsyncIterator<Uint8Array> => {\n iterator ??= (typeof source === \"function\" ? source() : source)[Symbol.asyncIterator]();\n return iterator;\n };\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const next = await open().next();\n if (next.done) controller.close();\n else controller.enqueue(next.value);\n } catch (err) {\n controller.error(err);\n }\n },\n async cancel() {\n // A source never opened has nothing to release — and opening it\n // now only to close it would be the cost this avoids.\n if (!iterator && typeof source === \"function\") return;\n try {\n await open().return?.();\n } catch {\n // Already failed or closed; either way it is released.\n }\n }\n }, { highWaterMark: 0 });\n}\n\n/**\n * A file on disk, or an inclusive byte range of it, as a web stream. The file\n * is opened on the first read; see {@link webStreamFrom}.\n */\nexport function fileWebStream(filePath: string, range?: { start: number; end: number }): ReadableStream<Uint8Array> {\n return webStreamFrom(() => fs.createReadStream(filePath, range ? { start: range.start, end: range.end } : {}));\n}\n\n/** A buffer already in memory as a web stream of views onto it, never copies. */\nexport function bufferWebStream(bytes: Uint8Array, chunkSize = 64 * 1024): ReadableStream<Uint8Array> {\n let offset = 0;\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n if (offset >= bytes.byteLength) {\n controller.close();\n return;\n }\n const end = Math.min(offset + chunkSize, bytes.byteLength);\n controller.enqueue(bytes.subarray(offset, end));\n offset = end;\n }\n }, { highWaterMark: 0 });\n}\n\n/**\n * Read a stream into one buffer.\n *\n * For the one consumer that needs the whole object at once — an image decode.\n * With the size known the bytes are copied once, into a buffer of that size,\n * rather than collected and then concatenated (which holds them twice); a\n * stream that turns out longer or shorter than announced still comes back\n * whole and exact.\n */\nexport async function readStreamFully(stream: ReadableStream<Uint8Array>, expectedSize?: number): Promise<Buffer> {\n const reader = stream.getReader();\n const target = expectedSize !== undefined && expectedSize > 0 ? Buffer.allocUnsafe(expectedSize) : undefined;\n let offset = 0;\n let overflow: Uint8Array[] | undefined;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (target && !overflow && offset + value.byteLength <= target.byteLength) {\n target.set(value, offset);\n } else {\n overflow ??= target ? [target.subarray(0, offset)] : [];\n overflow.push(value);\n }\n offset += value.byteLength;\n }\n } finally {\n reader.releaseLock();\n }\n if (overflow) return Buffer.concat(overflow, offset);\n if (!target) return Buffer.alloc(0);\n return offset === target.byteLength ? target : target.subarray(0, offset);\n}\n\n/**\n * The bytes of a buffer as a `Uint8Array` the response APIs accept, without\n * copying them. `new Uint8Array(buffer)` copies; a view does not. Only a buffer\n * over shared memory — which nothing here produces — is copied.\n */\nexport function bytesView(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n const backing = bytes.buffer;\n return backing instanceof ArrayBuffer\n ? new Uint8Array(backing, bytes.byteOffset, bytes.byteLength)\n : new Uint8Array(bytes);\n}\n","/**\n * What every storage controller checks about a file before it stores it: the\n * source's own size cap and, when configured, its allowed content types.\n *\n * One function for the three controllers, which carried a copy each. Both\n * refusals are `ApiError`s, so they reach the caller as what they are. A plain\n * `Error` was a 500 \"Internal Server Error\" on `/upload` and a 502 on TUS —\n * after the client had sent every byte.\n */\n\nimport { ApiError } from \"../api/errors\";\n\nexport function validateUploadedFile(\n file: { size: number; type: string },\n maxSize: number,\n allowedMimeTypes: string[] | undefined\n): void {\n if (file.size > maxSize) {\n throw new ApiError(\n 413,\n \"PAYLOAD_TOO_LARGE\",\n `File size ${file.size} exceeds this storage source's maximum of ${maxSize} bytes.`,\n { size: file.size, maxFileSize: maxSize }\n );\n }\n\n if (allowedMimeTypes && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(file.type)) {\n // The code a property's `acceptedFiles` refusal answers with: to the\n // caller, \"this source does not take this type\" is the same mistake.\n throw ApiError.badRequest(\n `File type ${file.type || \"(none)\"} is not allowed on this storage source. ` +\n `Allowed types: ${allowedMimeTypes.join(\", \")}.`,\n \"STORAGE_FILE_TYPE_REFUSED\",\n { allowedMimeTypes, contentType: file.type }\n );\n }\n}\n"],"mappings":";;;;;;;;;;AAoSA,IAAa,wBAAwB,KAAK,OAAO;;;;AAKjD,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;AAKA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;;;;;;ACvSA,SAAgB,cAAc,QAAqE;CAC/F,IAAI;CACJ,MAAM,aAAwC;EAC1C,cAAc,OAAO,WAAW,aAAa,OAAO,IAAI,OAAA,CAAQ,OAAO,cAAc,CAAC;EACtF,OAAO;CACX;CACA,OAAO,IAAI,eAA2B;EAClC,MAAM,KAAK,YAAY;GACnB,IAAI;IACA,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC,KAAK;IAC/B,IAAI,KAAK,MAAM,WAAW,MAAM;SAC3B,WAAW,QAAQ,KAAK,KAAK;GACtC,SAAS,KAAK;IACV,WAAW,MAAM,GAAG;GACxB;EACJ;EACA,MAAM,SAAS;GAGX,IAAI,CAAC,YAAY,OAAO,WAAW,YAAY;GAC/C,IAAI;IACA,MAAM,KAAK,CAAC,CAAC,SAAS;GAC1B,QAAQ,CAER;EACJ;CACJ,GAAG,EAAE,eAAe,EAAE,CAAC;AAC3B;;;;;AAMA,SAAgB,cAAc,UAAkB,OAAoE;CAChH,OAAO,oBAAoB,GAAG,iBAAiB,UAAU,QAAQ;EAAE,OAAO,MAAM;EAAO,KAAK,MAAM;CAAI,IAAI,CAAC,CAAC,CAAC;AACjH;;AAGA,SAAgB,gBAAgB,OAAmB,YAAY,KAAK,MAAkC;CAClG,IAAI,SAAS;CACb,OAAO,IAAI,eAA2B,EAClC,KAAK,YAAY;EACb,IAAI,UAAU,MAAM,YAAY;GAC5B,WAAW,MAAM;GACjB;EACJ;EACA,MAAM,MAAM,KAAK,IAAI,SAAS,WAAW,MAAM,UAAU;EACzD,WAAW,QAAQ,MAAM,SAAS,QAAQ,GAAG,CAAC;EAC9C,SAAS;CACb,EACJ,GAAG,EAAE,eAAe,EAAE,CAAC;AAC3B;;;;;;;;;;AAWA,eAAsB,gBAAgB,QAAoC,cAAwC;CAC9G,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAS,iBAAiB,KAAA,KAAa,eAAe,IAAI,OAAO,YAAY,YAAY,IAAI,KAAA;CACnG,IAAI,SAAS;CACb,IAAI;CACJ,IAAI;EACA,SAAS;GACL,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;GACV,IAAI,UAAU,CAAC,YAAY,SAAS,MAAM,cAAc,OAAO,YAC3D,OAAO,IAAI,OAAO,MAAM;QACrB;IACH,aAAa,SAAS,CAAC,OAAO,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC;IACtD,SAAS,KAAK,KAAK;GACvB;GACA,UAAU,MAAM;EACpB;CACJ,UAAU;EACN,OAAO,YAAY;CACvB;CACA,IAAI,UAAU,OAAO,OAAO,OAAO,UAAU,MAAM;CACnD,IAAI,CAAC,QAAQ,OAAO,OAAO,MAAM,CAAC;CAClC,OAAO,WAAW,OAAO,aAAa,SAAS,OAAO,SAAS,GAAG,MAAM;AAC5E;;;;;;AAOA,SAAgB,UAAU,OAA4C;CAClE,MAAM,UAAU,MAAM;CACtB,OAAO,mBAAmB,cACpB,IAAI,WAAW,SAAS,MAAM,YAAY,MAAM,UAAU,IAC1D,IAAI,WAAW,KAAK;AAC9B;;;;;;;;;;;;AC9GA,SAAgB,qBACZ,MACA,SACA,kBACI;CACJ,IAAI,KAAK,OAAO,SACZ,MAAM,IAAI,SACN,KACA,qBACA,aAAa,KAAK,KAAK,4CAA4C,QAAQ,UAC3E;EAAE,MAAM,KAAK;EAAM,aAAa;CAAQ,CAC5C;CAGJ,IAAI,oBAAoB,iBAAiB,SAAS,KAAK,CAAC,iBAAiB,SAAS,KAAK,IAAI,GAGvF,MAAM,SAAS,WACX,aAAa,KAAK,QAAQ,SAAS,yDACjB,iBAAiB,KAAK,IAAI,EAAE,IAC9C,6BACA;EAAE;EAAkB,aAAa,KAAK;CAAK,CAC/C;AAER"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `add -D <spec>` for the package the backend in `startDir` runs from, typed
|
|
3
|
+
* at the project root: `pnpm --dir backend add -D ts-morph@28.0.0` in a
|
|
4
|
+
* scaffold, `pnpm add -D ts-morph@28.0.0` where the backend is the project.
|
|
5
|
+
*/
|
|
6
|
+
export declare function devDependencyInstallCommand(spec: string, startDir?: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* How to turn on what needs `ts-morph`, as the end of a sentence that has
|
|
9
|
+
* said it is missing: "Run `pnpm --dir backend add -D ts-morph@28.0.0` from
|
|
10
|
+
* the project root, then restart the backend, to enable it."
|
|
11
|
+
*
|
|
12
|
+
* `pnpm`, never `npm`, in a pnpm project: running npm inside one rewrites
|
|
13
|
+
* node_modules into a hoisted layout pnpm then disagrees with (docs/bug-classes.md §5).
|
|
14
|
+
*/
|
|
15
|
+
export declare function tsMorphInstallSentence(startDir?: string): string;
|