@rebasepro/server 0.25.1 → 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-CRd9bFrc.js → GCSStorageController-40FyFeSx.js} +6 -2
- package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
- package/dist/{S3StorageController-D00EFa5P.js → S3StorageController-Bl5shp94.js} +6 -2
- 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/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-Dkvo2tTJ.js → auth-izI7AEzC.js} +370 -1880
- 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/create-backend.d.ts +6 -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-B9m3cb3X.js → backup-BVbHoKo8.js} +6 -6
- package/dist/{backup-B9m3cb3X.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-CsFXW7j4.js → contract-routes-CU2A3JnF.js} +2 -2
- package/dist/{contract-routes-CsFXW7j4.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
- package/dist/cron/cron-store.d.ts +8 -1
- package/dist/{cron-loader-Dj6lA4HQ.js → cron-loader-mMhqbVsl.js} +2 -2
- package/dist/{cron-loader-Dj6lA4HQ.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
- package/dist/{cron-routes-CwbLZguO.js → cron-routes-Cs33j_Kr.js} +3 -3
- package/dist/{cron-routes-CwbLZguO.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
- package/dist/{cron-scheduler-B04ACoUE.js → cron-scheduler-Be7CNghL.js} +2 -2
- package/dist/{cron-scheduler-B04ACoUE.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
- package/dist/{cron-store-DkyfuLOH.js → cron-store-DLn11M8H.js} +10 -9
- 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-Ck20GSIX.js → ddl-bootstrap-CjRrn12l.js} +91 -2
- 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/{soft-delete-params-BmlVwecC.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-KKDqTcIr.js → function-loader-B8YzXEsQ.js} +21 -14
- package/dist/function-loader-B8YzXEsQ.js.map +1 -0
- package/dist/functions/hono-like.d.ts +12 -0
- package/dist/functions/index.js +19 -1
- 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-B2rkuhuy.js → history-recorder-Bsau7h4X.js} +6 -6
- 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 +2449 -543
- 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-h1WRq3cp.js → jobs-Co6Ni5HP.js} +6 -6
- package/dist/jobs-Co6Ni5HP.js.map +1 -0
- package/dist/{jwt-D3oFauZI.js → jwt-rDS0884_.js} +4 -3
- package/dist/{jwt-D3oFauZI.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/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-BjHr6upl.js → openapi-generator-CyxEtYEX.js} +61 -22
- package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
- package/dist/{proxy-CH8Jsovg.js → proxy-BYir8tWK.js} +4 -6
- package/dist/proxy-BYir8tWK.js.map +1 -0
- package/dist/{query-parser-otFZqJsC.js → query-parser-CDXWf2hH.js} +6 -5
- package/dist/{query-parser-otFZqJsC.js.map → query-parser-CDXWf2hH.js.map} +1 -1
- package/dist/{request-timeout-59crS-9u.js → request-timeout-DVjo-c_8.js} +4 -4
- 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-Bsut0bCC.js → schema-editor-routes-Dc3DU_jm.js} +3 -3
- package/dist/{schema-editor-routes-Bsut0bCC.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-DmB_uZ4l.js → sql-export-routes-QycX8W8S.js} +38 -4
- 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-Bgo626uY.js.map +1 -1
- package/package.json +5 -5
- package/dist/GCSStorageController-CRd9bFrc.js.map +0 -1
- package/dist/S3StorageController-D00EFa5P.js.map +0 -1
- package/dist/auth-Dkvo2tTJ.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-DkyfuLOH.js.map +0 -1
- package/dist/ddl-bootstrap-Ck20GSIX.js.map +0 -1
- package/dist/function-loader-KKDqTcIr.js.map +0 -1
- package/dist/history-recorder-B2rkuhuy.js.map +0 -1
- package/dist/history-store-DPofGVDp.js.map +0 -1
- package/dist/jobs-h1WRq3cp.js.map +0 -1
- package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
- package/dist/openapi-generator-BjHr6upl.js.map +0 -1
- package/dist/proxy-CH8Jsovg.js.map +0 -1
- package/dist/request-timeout-59crS-9u.js.map +0 -1
- package/dist/soft-delete-params-BmlVwecC.js.map +0 -1
- package/dist/sql-export-routes-DmB_uZ4l.js.map +0 -1
- package/dist/strings.es-BeZWAodn.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"}
|