@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +3 -0
- package/common/managed/finding-detail.js +66 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +7 -0
- package/common/managed/npm-packages.js +51 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +359 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +6 -0
- package/common/managed/team-app.js +47 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2K6UN3PC.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7JBSSRD7.js +3 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FMXWXDR4.js +36 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-KOGNQDSJ.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-VDRXHIHN.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WRAJVD35.js +1 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +364 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +23 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +76 -0
- package/server-managed/comparison.ts +52 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +468 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +143 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +98 -0
- package/server-managed/dependency-reviews.ts +364 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +376 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +136 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5503 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +152 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +225 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +351 -0
- package/server-managed/pull-dependency-reviews.ts +334 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +448 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +166 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +212 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +319 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { DEPENDENCY_REVIEW_COLUMNS, DEPENDENCY_REVIEW_INDEXES, DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
2
|
+
import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
3
|
+
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
|
+
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
5
|
+
import { GITHUB_LINK_SCHEMA, GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
6
|
+
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
7
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
8
|
+
import { WebSocket } from 'ws'
|
|
9
|
+
import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
|
|
10
|
+
import { revisionSchema } from './revisions.ts'
|
|
11
|
+
import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
12
|
+
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
13
|
+
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
14
|
+
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
15
|
+
import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
|
|
16
|
+
import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
|
|
17
|
+
import { COMMENT_SCHEMA } from './comments.ts'
|
|
18
|
+
import { ACTIVITY_SCHEMA, uploadAction } from './activity.ts'
|
|
19
|
+
import { type ManagedSqlDriver, scopeManagedMethods } from './sql.ts'
|
|
20
|
+
import { countStatement, timed } from './request-timing.ts'
|
|
21
|
+
import { postgresSchema, postgresSql } from './sql-postgres.ts'
|
|
22
|
+
import { managedTableRenames } from './db-table-names.ts'
|
|
23
|
+
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
24
|
+
import { allocateMissingSlugs } from './slugs.ts'
|
|
25
|
+
|
|
26
|
+
export interface PgConnection {
|
|
27
|
+
query(sql: string, params?: unknown[]): Promise<{
|
|
28
|
+
rows: Record<string, unknown>[]; rowCount?: number | null
|
|
29
|
+
fields?: { name: string; dataTypeID: number }[]
|
|
30
|
+
}>
|
|
31
|
+
// After a failure the connection's state is unknown: discard it, never reuse it.
|
|
32
|
+
release(discard?: boolean): Promise<void>
|
|
33
|
+
}
|
|
34
|
+
export type PgConnect = () => Promise<PgConnection>
|
|
35
|
+
// Content/access mutations cooperate on this transaction-scoped lock. Reads,
|
|
36
|
+
// presence updates, and maintenance leases do not take it. Also fences migrations.
|
|
37
|
+
const LOCK = 'SELECT pg_advisory_xact_lock(1937006964, 1835101793)'
|
|
38
|
+
|
|
39
|
+
// Whether every version up to the latest is recorded, in one round trip at
|
|
40
|
+
// a cold start: a database with no versions table yet (undefined_table) has
|
|
41
|
+
// none. Outside a transaction, so the failed read aborts nothing.
|
|
42
|
+
const SCHEMA_VERSIONS = 33
|
|
43
|
+
async function currentSchema(db: PgConnection): Promise<boolean> {
|
|
44
|
+
try {
|
|
45
|
+
const { rows } = await db.query(`SELECT count(*) AS n FROM managed_schema_version WHERE version BETWEEN 1 AND ${SCHEMA_VERSIONS}`)
|
|
46
|
+
return Number(rows[0]?.['n']) === SCHEMA_VERSIONS
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if ((error as { code?: unknown })?.code === '42P01') return false
|
|
49
|
+
throw error
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function migrateRepositoryDefaultCache(db: PgConnection): Promise<void> {
|
|
54
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 12')).rows.length > 0) return
|
|
55
|
+
await db.query('ALTER TABLE managed_selected_repo ADD COLUMN IF NOT EXISTS cached_default_branch TEXT')
|
|
56
|
+
await db.query('INSERT INTO managed_schema_version VALUES (12)')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function migrateBundleBuildLeases(db: PgConnection): Promise<void> {
|
|
60
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 13')).rows.length > 0) return
|
|
61
|
+
await db.query(postgresSchema(BUNDLE_BUILD_LEASE_SCHEMA))
|
|
62
|
+
await db.query('INSERT INTO managed_schema_version VALUES (13)')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function migrateGithubMetadata(db: PgConnection): Promise<void> {
|
|
66
|
+
await db.query(postgresSchema(GITHUB_METADATA_SCHEMA + MANAGED_ISSUE_SCHEMA))
|
|
67
|
+
await db.query(`ALTER TABLE managed_github_metadata ADD COLUMN IF NOT EXISTS state_reason ${GITHUB_STATE_REASON_COLUMN}`)
|
|
68
|
+
await db.query('ALTER TABLE managed_github_metadata ADD COLUMN IF NOT EXISTS attempted_at BIGINT')
|
|
69
|
+
await db.query('INSERT INTO managed_schema_version VALUES (14) ON CONFLICT DO NOTHING')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function migrateBundleVisibility(db: PgConnection): Promise<void> {
|
|
73
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 15')).rows.length === 0) {
|
|
74
|
+
await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS visible INTEGER NOT NULL DEFAULT 1')
|
|
75
|
+
await db.query('INSERT INTO managed_schema_version VALUES (15)')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function migrateLinkReports(db: PgConnection): Promise<void> {
|
|
80
|
+
await db.query(postgresSchema(revisionSchema(true)))
|
|
81
|
+
await db.query('INSERT INTO managed_schema_version VALUES (16) ON CONFLICT DO NOTHING')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function migrateRepositoryAliases(db: PgConnection): Promise<void> {
|
|
85
|
+
await db.query(postgresSchema(REPOSITORY_ALIAS_SCHEMA))
|
|
86
|
+
await db.query('INSERT INTO managed_schema_version VALUES (17) ON CONFLICT DO NOTHING')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function migrateHiddenTeams(db: PgConnection): Promise<void> {
|
|
90
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 18')).rows.length > 0) return
|
|
91
|
+
await db.query('ALTER TABLE managed_team ADD COLUMN IF NOT EXISTS hidden INTEGER NOT NULL DEFAULT 0')
|
|
92
|
+
await db.query('INSERT INTO managed_schema_version VALUES (18)')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function migrateIssueFixes(db: PgConnection): Promise<void> {
|
|
96
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 19')).rows.length > 0) return
|
|
97
|
+
await db.query('ALTER TABLE managed_finding_issue ADD COLUMN IF NOT EXISTS auto_fix_url TEXT, ADD COLUMN IF NOT EXISTS auto_fix_checked_at BIGINT')
|
|
98
|
+
await db.query(postgresSchema(revisionSchema(true)))
|
|
99
|
+
await db.query('INSERT INTO managed_schema_version VALUES (19)')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Existing rows stay NULL (unknown): some may have been built on the server.
|
|
103
|
+
async function migrateBundleProvenance(db: PgConnection): Promise<void> {
|
|
104
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 20')).rows.length > 0) return
|
|
105
|
+
await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS provenance TEXT')
|
|
106
|
+
await createUploadTrigger(db, 'bundle', false)
|
|
107
|
+
await db.query('INSERT INTO managed_schema_version VALUES (20)')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function migrateUpstreamCache(db: PgConnection): Promise<void> {
|
|
111
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 21')).rows.length > 0) return
|
|
112
|
+
await db.query(postgresSchema(UPSTREAM_CACHE_SCHEMA))
|
|
113
|
+
await db.query('INSERT INTO managed_schema_version VALUES (21)')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Existing builds stay NULL: their conditions were never recorded.
|
|
117
|
+
async function migrateBundleBuildConditions(db: PgConnection): Promise<void> {
|
|
118
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 22')).rows.length > 0) return
|
|
119
|
+
await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS build_conditions TEXT')
|
|
120
|
+
await db.query('INSERT INTO managed_schema_version VALUES (22)')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Existing sessions create their upload key on first use.
|
|
124
|
+
async function migrateSessionUploadKeys(db: PgConnection): Promise<void> {
|
|
125
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 23')).rows.length > 0) return
|
|
126
|
+
await db.query('ALTER TABLE managed_session ADD COLUMN IF NOT EXISTS upload_key TEXT')
|
|
127
|
+
await db.query('INSERT INTO managed_schema_version VALUES (23)')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function migrateViewSessions(db: PgConnection): Promise<void> {
|
|
131
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 24')).rows.length > 0) return
|
|
132
|
+
await db.query('ALTER TABLE managed_session ADD COLUMN IF NOT EXISTS viewer_session TEXT REFERENCES managed_session(id) ON DELETE CASCADE')
|
|
133
|
+
await db.query(VIEW_SESSION_INDEX)
|
|
134
|
+
await db.query('INSERT INTO managed_schema_version VALUES (24)')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function migrateGithubCommits(db: PgConnection): Promise<void> {
|
|
138
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 25')).rows.length > 0) return
|
|
139
|
+
await db.query(postgresSchema(GITHUB_COMMIT_SCHEMA))
|
|
140
|
+
await db.query('INSERT INTO managed_schema_version VALUES (25)')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function migrateTeamNpmScopes(db: PgConnection): Promise<void> {
|
|
144
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 26')).rows.length > 0) return
|
|
145
|
+
await db.query(postgresSchema(TEAM_NPM_SCOPE_SCHEMA))
|
|
146
|
+
await db.query('INSERT INTO managed_schema_version VALUES (26)')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function migrateDependencyReviews(db: PgConnection): Promise<void> {
|
|
150
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 27')).rows.length > 0) return
|
|
151
|
+
await db.query('ALTER TABLE managed_team ADD COLUMN IF NOT EXISTS dependency_review INTEGER NOT NULL DEFAULT 0')
|
|
152
|
+
await db.query(postgresSchema(DEPENDENCY_REVIEW_SCHEMA))
|
|
153
|
+
await db.query('INSERT INTO managed_schema_version VALUES (27)')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Existing names are the sign-in accounts' own: none was chosen yet.
|
|
157
|
+
async function migrateGithubLinks(db: PgConnection): Promise<void> {
|
|
158
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 28')).rows.length > 0) return
|
|
159
|
+
await db.query(postgresSchema(GITHUB_LINK_SCHEMA))
|
|
160
|
+
await db.query(`ALTER TABLE managed_user ${GITHUB_LINK_USER_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
161
|
+
await db.query('UPDATE managed_user SET github_name = name')
|
|
162
|
+
await db.query('INSERT INTO managed_schema_version VALUES (28)')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function migrateRepositoryDependencyReviews(db: PgConnection): Promise<void> {
|
|
166
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 29')).rows.length > 0) return
|
|
167
|
+
await db.query(`ALTER TABLE managed_selected_repo ${REPOSITORY_DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
168
|
+
await db.query('INSERT INTO managed_schema_version VALUES (29)')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The lockfiles a repository's dependency review holds to, its columns
|
|
172
|
+
// added since 29 added there too; and a team's published reviews, listed by
|
|
173
|
+
// it, indexed so.
|
|
174
|
+
async function migrateRepositoryDependencyReviewLockfiles(db: PgConnection): Promise<void> {
|
|
175
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 30')).rows.length > 0) return
|
|
176
|
+
await db.query(`ALTER TABLE managed_selected_repo ${REPOSITORY_DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
177
|
+
await db.query('CREATE INDEX IF NOT EXISTS managed_dependency_review_team_idx ON managed_dependency_review_team(team_id, review_id)')
|
|
178
|
+
await db.query('INSERT INTO managed_schema_version VALUES (30)')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function migrateRepoUploadIndexes(db: PgConnection): Promise<void> {
|
|
182
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 31')).rows.length > 0) return
|
|
183
|
+
await db.query(REPO_UPLOAD_INDEXES)
|
|
184
|
+
await db.query('INSERT INTO managed_schema_version VALUES (31)')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The shasums of the tarballs a dependency review reads, none known of
|
|
188
|
+
// those written before.
|
|
189
|
+
async function migrateDependencyReviewShasums(db: PgConnection): Promise<void> {
|
|
190
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 32')).rows.length > 0) return
|
|
191
|
+
await db.query(`ALTER TABLE managed_dependency_review ${DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
192
|
+
await db.query('INSERT INTO managed_schema_version VALUES (32)')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The patch a dependency review is of, and where it was read, none of
|
|
196
|
+
// those written before; each author's one draft of a comparison held to its
|
|
197
|
+
// patch too.
|
|
198
|
+
async function migrateDependencyReviewPatches(db: PgConnection): Promise<void> {
|
|
199
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 33')).rows.length > 0) return
|
|
200
|
+
await db.query(`ALTER TABLE managed_dependency_review ${DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
201
|
+
await db.query(DEPENDENCY_REVIEW_INDEXES)
|
|
202
|
+
await db.query('INSERT INTO managed_schema_version VALUES (33)')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function initialize(db: PgConnection): Promise<void> {
|
|
206
|
+
if (await currentSchema(db)) return
|
|
207
|
+
await db.query('BEGIN')
|
|
208
|
+
try {
|
|
209
|
+
await db.query(LOCK)
|
|
210
|
+
await db.query('CREATE TABLE IF NOT EXISTS managed_schema_version (version INTEGER PRIMARY KEY)')
|
|
211
|
+
const initialized = (await db.query('SELECT version FROM managed_schema_version WHERE version = 1')).rows.length > 0
|
|
212
|
+
const prefixed = (await db.query('SELECT version FROM managed_schema_version WHERE version = 3')).rows.length > 0
|
|
213
|
+
if (initialized && !prefixed) {
|
|
214
|
+
const { rows } = await db.query("SELECT relname AS name FROM pg_class WHERE relkind IN ('r', 'p') AND pg_table_is_visible(oid)")
|
|
215
|
+
for (const [from, to] of managedTableRenames(new Set(rows.map(row => String(row['name']))))) {
|
|
216
|
+
await db.query(`ALTER TABLE ${from} RENAME TO ${to}`)
|
|
217
|
+
}
|
|
218
|
+
// PL/pgSQL function bodies retain literal table names after ALTER TABLE.
|
|
219
|
+
for (const type of ['report', 'bundle']) await createUploadTrigger(db, type, false)
|
|
220
|
+
}
|
|
221
|
+
if (!initialized) {
|
|
222
|
+
await db.query(postgresSchema(MANAGED_SCHEMA + COMMENT_SCHEMA + ACTIVITY_SCHEMA))
|
|
223
|
+
await db.query(`ALTER TABLE managed_finding_triage_event ADD COLUMN report_id TEXT, ADD COLUMN report TEXT, ADD COLUMN repo TEXT;
|
|
224
|
+
CREATE UNIQUE INDEX managed_team_slug_idx ON managed_team(slug);
|
|
225
|
+
CREATE UNIQUE INDEX managed_report_slug_idx ON managed_report(slug);
|
|
226
|
+
CREATE INDEX managed_activity_actor_at_idx ON managed_activity(actor_id, at);`)
|
|
227
|
+
for (const type of ['report', 'bundle']) await createUploadTrigger(db, type)
|
|
228
|
+
await db.query('INSERT INTO managed_schema_version VALUES (1)')
|
|
229
|
+
}
|
|
230
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 2')).rows.length === 0) {
|
|
231
|
+
await db.query('CREATE INDEX IF NOT EXISTS managed_report_bundle_hash_idx ON managed_report(bundle_id, sha256)')
|
|
232
|
+
await db.query('INSERT INTO managed_schema_version VALUES (2)')
|
|
233
|
+
}
|
|
234
|
+
if (!prefixed) await db.query('INSERT INTO managed_schema_version VALUES (3)')
|
|
235
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 4')).rows.length === 0) {
|
|
236
|
+
await db.query(postgresSchema(WORKSPACE_SHARE_SCHEMA))
|
|
237
|
+
await db.query('INSERT INTO managed_schema_version VALUES (4)')
|
|
238
|
+
}
|
|
239
|
+
await migrateGithubMetadata(db)
|
|
240
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 5')).rows.length === 0) {
|
|
241
|
+
await db.query(`ALTER TABLE managed_workspace_share ADD COLUMN IF NOT EXISTS dependencies INTEGER NOT NULL DEFAULT 0,
|
|
242
|
+
ADD COLUMN IF NOT EXISTS security INTEGER NOT NULL DEFAULT 0`)
|
|
243
|
+
await db.query('INSERT INTO managed_schema_version VALUES (5)')
|
|
244
|
+
}
|
|
245
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 6')).rows.length === 0) {
|
|
246
|
+
await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS slug TEXT')
|
|
247
|
+
const { rows } = await db.query('SELECT id, slug FROM managed_bundle ORDER BY id')
|
|
248
|
+
for (const row of allocateMissingSlugs(rows as { id: string; slug: string | null }[])) {
|
|
249
|
+
await db.query('UPDATE managed_bundle SET slug = $1 WHERE id = $2', [row.slug, row.id])
|
|
250
|
+
}
|
|
251
|
+
await db.query(`ALTER TABLE managed_bundle ALTER COLUMN slug SET NOT NULL;
|
|
252
|
+
CREATE UNIQUE INDEX IF NOT EXISTS managed_bundle_slug_idx ON managed_bundle(slug);
|
|
253
|
+
INSERT INTO managed_schema_version VALUES (6)`)
|
|
254
|
+
}
|
|
255
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 7')).rows.length === 0) {
|
|
256
|
+
await db.query("ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS repo_directory TEXT NOT NULL DEFAULT ''")
|
|
257
|
+
await db.query('INSERT INTO managed_schema_version VALUES (7)')
|
|
258
|
+
}
|
|
259
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 8')).rows.length === 0) {
|
|
260
|
+
await db.query('CREATE INDEX IF NOT EXISTS managed_report_hash_idx ON managed_report(sha256, uploaded_at, id)')
|
|
261
|
+
await db.query('INSERT INTO managed_schema_version VALUES (8)')
|
|
262
|
+
}
|
|
263
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 9')).rows.length === 0) {
|
|
264
|
+
await db.query(postgresSchema(STORAGE_SCHEMA))
|
|
265
|
+
for (const table of ['managed_report', 'managed_bundle']) {
|
|
266
|
+
await db.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS data_key TEXT, ADD COLUMN IF NOT EXISTS storage_encrypted INTEGER NOT NULL DEFAULT 0`)
|
|
267
|
+
}
|
|
268
|
+
await db.query('ALTER TABLE managed_user ADD COLUMN IF NOT EXISTS gh_tokens_encrypted INTEGER NOT NULL DEFAULT 0')
|
|
269
|
+
await db.query('INSERT INTO managed_schema_version VALUES (9)')
|
|
270
|
+
}
|
|
271
|
+
await db.query(postgresSchema(LINK_REPORT_SCHEMA))
|
|
272
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 10')).rows.length === 0) {
|
|
273
|
+
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (10)')
|
|
274
|
+
}
|
|
275
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 11')).rows.length === 0) {
|
|
276
|
+
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
|
|
277
|
+
}
|
|
278
|
+
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews, migrateGithubLinks, migrateRepositoryDependencyReviews,
|
|
279
|
+
migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes, migrateDependencyReviewShasums, migrateDependencyReviewPatches]) await migrate(db)
|
|
280
|
+
await db.query('COMMIT')
|
|
281
|
+
} catch (err) {
|
|
282
|
+
await db.query('ROLLBACK')
|
|
283
|
+
throw err
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function createUploadTrigger(db: PgConnection, type: string, createTrigger = true): Promise<void> {
|
|
288
|
+
await db.query(`CREATE OR REPLACE FUNCTION managed_${type}_activity_insert() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
289
|
+
BEGIN
|
|
290
|
+
INSERT INTO managed_activity (id, kind, actor, action, repo, report_id, report, at, bundle_id, actor_id)
|
|
291
|
+
VALUES ('${type}-upload:' || NEW.id, 'upload',
|
|
292
|
+
COALESCE(NEW.uploaded_by_login, (SELECT login FROM managed_user WHERE id = NEW.uploaded_by)),
|
|
293
|
+
${uploadAction(type, 'NEW')}, (SELECT full_name FROM managed_selected_repo WHERE repo_id = NEW.repo_id),
|
|
294
|
+
${type === 'report' ? 'NEW.id' : 'NULL'}, NEW.filename, NEW.uploaded_at,
|
|
295
|
+
${type === 'bundle' ? 'NEW.id' : 'NULL'}, NEW.uploaded_by);
|
|
296
|
+
RETURN NEW;
|
|
297
|
+
END $$;`)
|
|
298
|
+
if (createTrigger) {
|
|
299
|
+
await db.query(`CREATE TRIGGER managed_${type}_activity AFTER INSERT ON managed_${type}
|
|
300
|
+
FOR EACH ROW EXECUTE FUNCTION managed_${type}_activity_insert();`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function requestConnections(connect: PgConnect) {
|
|
305
|
+
const requests = new AsyncLocalStorage<{ connection?: PgConnection; queue: Promise<unknown>; ended: boolean }>()
|
|
306
|
+
async function withConnection<T>(work: (db: PgConnection) => Promise<T>): Promise<T> {
|
|
307
|
+
const request = requests.getStore()
|
|
308
|
+
if (!request || request.ended) {
|
|
309
|
+
const connection = await connect()
|
|
310
|
+
let result: T
|
|
311
|
+
try { result = await work(connection) }
|
|
312
|
+
catch (error) { await connection.release(true).catch(() => {}); throw error }
|
|
313
|
+
await connection.release()
|
|
314
|
+
return result
|
|
315
|
+
}
|
|
316
|
+
// Serialize transactions on the leased connection, never interleave BEGINs
|
|
317
|
+
// from concurrent Blob/catalog tasks. Remote Blob reads remain concurrent.
|
|
318
|
+
const result = request.queue.then(async () => {
|
|
319
|
+
const connection = request.connection ??= await connect()
|
|
320
|
+
try { return await work(connection) }
|
|
321
|
+
catch (error) {
|
|
322
|
+
delete request.connection
|
|
323
|
+
await connection.release(true).catch(() => {})
|
|
324
|
+
throw error
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
request.queue = result.catch(() => {})
|
|
328
|
+
return result
|
|
329
|
+
}
|
|
330
|
+
async function withRequest<T>(work: () => Promise<T>): Promise<T> {
|
|
331
|
+
if (requests.getStore()) return work()
|
|
332
|
+
const request: { connection?: PgConnection; queue: Promise<unknown>; ended: boolean } = { queue: Promise.resolve(), ended: false }
|
|
333
|
+
let failed = false
|
|
334
|
+
try { return await requests.run(request, work) }
|
|
335
|
+
catch (error) { failed = true; throw error }
|
|
336
|
+
finally {
|
|
337
|
+
request.ended = true
|
|
338
|
+
await request.queue
|
|
339
|
+
if (failed) await request.connection?.release(true).catch(() => {})
|
|
340
|
+
else await request.connection?.release()
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { withConnection, withRequest }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Exposed for parity tests against real PostgreSQL semantics via PGlite.
|
|
347
|
+
export async function openPostgresManagedDb(leased: PgConnect, options: ManagedDbOptions = {}, reuseConnections = false): Promise<ManagedDb> {
|
|
348
|
+
// Each statement told in its request's Server-Timing (request-timing.ts).
|
|
349
|
+
const connect: PgConnect = async () => {
|
|
350
|
+
const connection = await leased()
|
|
351
|
+
return { query: (sql, params) => { countStatement(); return connection.query(sql, params) }, release: discard => connection.release(discard) }
|
|
352
|
+
}
|
|
353
|
+
const initial = await connect()
|
|
354
|
+
try { await initialize(initial) } catch (err) { await initial.release(true).catch(() => {}); throw err }
|
|
355
|
+
await initial.release()
|
|
356
|
+
const { withConnection, withRequest } = requestConnections(connect)
|
|
357
|
+
const context = new AsyncLocalStorage<PgConnection>()
|
|
358
|
+
let closed = false
|
|
359
|
+
const driver: ManagedSqlDriver = {
|
|
360
|
+
prepare(source) {
|
|
361
|
+
const { sql, names } = postgresSql(source)
|
|
362
|
+
async function query(args: unknown[]) {
|
|
363
|
+
const db = context.getStore()
|
|
364
|
+
if (!db) throw new Error('Managed query outside transaction')
|
|
365
|
+
const values = names.length > 0 ? names.map(name => (args[0] as Record<string, unknown>)[name])
|
|
366
|
+
: args.length === 1 && typeof args[0] === 'object' && args[0] !== null ? [] : args
|
|
367
|
+
const result = await db.query(sql, values)
|
|
368
|
+
// Postgres int8 is returned as text by node-postgres/Neon. All store
|
|
369
|
+
// numbers are JS safe integers (timestamps, IDs, sizes and sequences).
|
|
370
|
+
for (const field of result.fields ?? []) {
|
|
371
|
+
if (field.dataTypeID !== 20) continue
|
|
372
|
+
for (const row of result.rows) {
|
|
373
|
+
if (row[field.name] == null) continue
|
|
374
|
+
const value = Number(row[field.name])
|
|
375
|
+
if (!Number.isSafeInteger(value)) throw new Error('Managed integer exceeds safe range')
|
|
376
|
+
row[field.name] = value
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return result
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
get: async (...args) => (await query(args)).rows[0],
|
|
383
|
+
all: async (...args) => (await query(args)).rows,
|
|
384
|
+
run: async (...args) => ({ changes: (await query(args)).rowCount ?? 0 }),
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
async scope(write, work, { lock = write, statement = false } = {}) {
|
|
388
|
+
if (closed) throw new Error('Managed database is closed')
|
|
389
|
+
let committed = false
|
|
390
|
+
try { return await withConnection(async db => {
|
|
391
|
+
if (statement) return context.run(db, work)
|
|
392
|
+
let commitAttempted = false
|
|
393
|
+
try {
|
|
394
|
+
await db.query(write ? 'BEGIN' : 'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY')
|
|
395
|
+
if (lock) await db.query(LOCK)
|
|
396
|
+
const result = await context.run(db, work)
|
|
397
|
+
commitAttempted = true
|
|
398
|
+
await db.query('COMMIT')
|
|
399
|
+
committed = true
|
|
400
|
+
return result
|
|
401
|
+
} catch (err) {
|
|
402
|
+
await db.query('ROLLBACK').catch(() => {})
|
|
403
|
+
if (write && commitAttempted) throw new Error('Managed commit outcome is uncertain', { cause: err })
|
|
404
|
+
throw err
|
|
405
|
+
}
|
|
406
|
+
}) } catch (error) {
|
|
407
|
+
if (write && committed) throw new Error('Managed commit outcome is uncertain', { cause: error })
|
|
408
|
+
throw error
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
close() { closed = true },
|
|
412
|
+
}
|
|
413
|
+
const methods = scopeManagedMethods(createManagedMethods(driver, options), driver)
|
|
414
|
+
if (reuseConnections) methods.withRequest = withRequest
|
|
415
|
+
return methods
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Idle connections stay open this long between leases, so a warm instance's
|
|
419
|
+
// requests and feed polls (three seconds apart) skip the TLS, WebSocket and
|
|
420
|
+
// Postgres handshake a new connection repeats.
|
|
421
|
+
const NEON_POOL_IDLE_MS = 5_000
|
|
422
|
+
|
|
423
|
+
type NeonPoolClient = {
|
|
424
|
+
query: PgConnection['query']
|
|
425
|
+
release(discard?: boolean): void
|
|
426
|
+
on(event: string, listener: (...args: unknown[]) => void): void
|
|
427
|
+
}
|
|
428
|
+
export type NeonPool = {
|
|
429
|
+
connect(): Promise<NeonPoolClient>
|
|
430
|
+
end(): Promise<void>
|
|
431
|
+
on(event: string, listener: (...args: unknown[]) => void): void
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// `attachPool` is Vercel's attachDatabasePool on serverless instances: each
|
|
435
|
+
// release keeps the invocation alive until idle connections have closed, so
|
|
436
|
+
// no socket survives a frozen instance.
|
|
437
|
+
export async function openNeonManagedDb(url: string, options: ManagedDbOptions = {}, attachPool?: (pool: NeonPool) => void): Promise<ManagedDb> {
|
|
438
|
+
// Reuse the e2e optional-driver boundary.
|
|
439
|
+
const { Pool, neonConfig } = await import('../server-e2e/neon-driver.ts') as unknown as {
|
|
440
|
+
Pool: new (config: { connectionString: string; max: number; idleTimeoutMillis: number }) => NeonPool
|
|
441
|
+
neonConfig: { webSocketConstructor: typeof WebSocket }
|
|
442
|
+
}
|
|
443
|
+
neonConfig.webSocketConstructor = WebSocket
|
|
444
|
+
// Leases stay unbounded, one per request or operation as before: a cap would
|
|
445
|
+
// stall a request that holds one while it awaits work needing another.
|
|
446
|
+
const pool = new Pool({ connectionString: url, max: Infinity, idleTimeoutMillis: NEON_POOL_IDLE_MS })
|
|
447
|
+
// The pool evicts an idle connection the server closed. Neither that nor a
|
|
448
|
+
// remote disconnect while leased may become an unhandled EventEmitter error;
|
|
449
|
+
// a leased one must instead reject its next query, and is then discarded.
|
|
450
|
+
pool.on('error', () => {})
|
|
451
|
+
attachPool?.(pool)
|
|
452
|
+
const failed = new WeakSet<NeonPoolClient>(), watched = new WeakSet<NeonPoolClient>()
|
|
453
|
+
let ending: Promise<void> | undefined
|
|
454
|
+
const end = () => ending ??= pool.end()
|
|
455
|
+
let db: ManagedDb
|
|
456
|
+
try {
|
|
457
|
+
db = await openPostgresManagedDb(async () => {
|
|
458
|
+
// A lease's wait told in its request's Server-Timing: a new connection's
|
|
459
|
+
// TLS, WebSocket and Postgres handshake, where the pool had none idle.
|
|
460
|
+
const client = await timed('conn', () => pool.connect())
|
|
461
|
+
if (!watched.has(client)) { watched.add(client); client.on('error', () => { failed.add(client) }) }
|
|
462
|
+
return { query: (sql, params) => client.query(sql, params),
|
|
463
|
+
release: discard => Promise.try(() => client.release(discard === true || failed.has(client))) }
|
|
464
|
+
}, options, true)
|
|
465
|
+
} catch (err) { await end().catch(() => {}); throw err }
|
|
466
|
+
const close = db.close
|
|
467
|
+
return { ...db, async close() { try { await close() } finally { await end() } } }
|
|
468
|
+
}
|