p2s_mapper 1.0.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/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/alignmentQuality.d.ts +58 -0
- package/dist/alignmentQuality.js +108 -0
- package/dist/alignmentQuality.js.map +1 -0
- package/dist/alphaFoldModels.d.ts +29 -0
- package/dist/alphaFoldModels.js +60 -0
- package/dist/alphaFoldModels.js.map +1 -0
- package/dist/caCoordsToPdb.d.ts +18 -0
- package/dist/caCoordsToPdb.js +95 -0
- package/dist/caCoordsToPdb.js.map +1 -0
- package/dist/chooseMappedEntity.d.ts +55 -0
- package/dist/chooseMappedEntity.js +140 -0
- package/dist/chooseMappedEntity.js.map +1 -0
- package/dist/coordinates.d.ts +66 -0
- package/dist/coordinates.js +66 -0
- package/dist/coordinates.js.map +1 -0
- package/dist/dbIds.d.ts +16 -0
- package/dist/dbIds.js +42 -0
- package/dist/dbIds.js.map +1 -0
- package/dist/extractPerResidueConfidence.d.ts +49 -0
- package/dist/extractPerResidueConfidence.js +42 -0
- package/dist/extractPerResidueConfidence.js.map +1 -0
- package/dist/extractStructureSequences.d.ts +142 -0
- package/dist/extractStructureSequences.js +217 -0
- package/dist/extractStructureSequences.js.map +1 -0
- package/dist/fetchUtils.d.ts +34 -0
- package/dist/fetchUtils.js +91 -0
- package/dist/fetchUtils.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/invertMap.d.ts +2 -0
- package/dist/invertMap.js +8 -0
- package/dist/invertMap.js.map +1 -0
- package/dist/isoformRanking.d.ts +47 -0
- package/dist/isoformRanking.js +91 -0
- package/dist/isoformRanking.js.map +1 -0
- package/dist/mappings.d.ts +58 -0
- package/dist/mappings.js +193 -0
- package/dist/mappings.js.map +1 -0
- package/dist/package.json +1 -0
- package/dist/pairwiseAlignment.d.ts +35 -0
- package/dist/pairwiseAlignment.js +337 -0
- package/dist/pairwiseAlignment.js.map +1 -0
- package/dist/pdbUniProtMapping.d.ts +75 -0
- package/dist/pdbUniProtMapping.js +241 -0
- package/dist/pdbUniProtMapping.js.map +1 -0
- package/dist/pdbeBestStructures.d.ts +23 -0
- package/dist/pdbeBestStructures.js +79 -0
- package/dist/pdbeBestStructures.js.map +1 -0
- package/dist/pdbeEntryMolecules.d.ts +7 -0
- package/dist/pdbeEntryMolecules.js +39 -0
- package/dist/pdbeEntryMolecules.js.map +1 -0
- package/dist/siftsNumbering.d.ts +23 -0
- package/dist/siftsNumbering.js +44 -0
- package/dist/siftsNumbering.js.map +1 -0
- package/dist/stripStopCodon.d.ts +15 -0
- package/dist/stripStopCodon.js +24 -0
- package/dist/stripStopCodon.js.map +1 -0
- package/dist/structureFormat.d.ts +40 -0
- package/dist/structureFormat.js +54 -0
- package/dist/structureFormat.js.map +1 -0
- package/dist/structureSources.d.ts +28 -0
- package/dist/structureSources.js +76 -0
- package/dist/structureSources.js.map +1 -0
- package/dist/structureUrls.d.ts +54 -0
- package/dist/structureUrls.js +147 -0
- package/dist/structureUrls.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/dist/uniProtSearch.d.ts +32 -0
- package/dist/uniProtSearch.js +114 -0
- package/dist/uniProtSearch.js.map +1 -0
- package/esm/alignmentQuality.d.ts +58 -0
- package/esm/alignmentQuality.js +100 -0
- package/esm/alignmentQuality.js.map +1 -0
- package/esm/alphaFoldModels.d.ts +29 -0
- package/esm/alphaFoldModels.js +55 -0
- package/esm/alphaFoldModels.js.map +1 -0
- package/esm/caCoordsToPdb.d.ts +18 -0
- package/esm/caCoordsToPdb.js +91 -0
- package/esm/caCoordsToPdb.js.map +1 -0
- package/esm/chooseMappedEntity.d.ts +55 -0
- package/esm/chooseMappedEntity.js +134 -0
- package/esm/chooseMappedEntity.js.map +1 -0
- package/esm/coordinates.d.ts +66 -0
- package/esm/coordinates.js +58 -0
- package/esm/coordinates.js.map +1 -0
- package/esm/dbIds.d.ts +16 -0
- package/esm/dbIds.js +35 -0
- package/esm/dbIds.js.map +1 -0
- package/esm/extractPerResidueConfidence.d.ts +49 -0
- package/esm/extractPerResidueConfidence.js +38 -0
- package/esm/extractPerResidueConfidence.js.map +1 -0
- package/esm/extractStructureSequences.d.ts +142 -0
- package/esm/extractStructureSequences.js +205 -0
- package/esm/extractStructureSequences.js.map +1 -0
- package/esm/fetchUtils.d.ts +34 -0
- package/esm/fetchUtils.js +82 -0
- package/esm/fetchUtils.js.map +1 -0
- package/esm/index.d.ts +23 -0
- package/esm/index.js +30 -0
- package/esm/index.js.map +1 -0
- package/esm/invertMap.d.ts +2 -0
- package/esm/invertMap.js +5 -0
- package/esm/invertMap.js.map +1 -0
- package/esm/isoformRanking.d.ts +47 -0
- package/esm/isoformRanking.js +86 -0
- package/esm/isoformRanking.js.map +1 -0
- package/esm/mappings.d.ts +58 -0
- package/esm/mappings.js +179 -0
- package/esm/mappings.js.map +1 -0
- package/esm/pairwiseAlignment.d.ts +35 -0
- package/esm/pairwiseAlignment.js +328 -0
- package/esm/pairwiseAlignment.js.map +1 -0
- package/esm/pdbUniProtMapping.d.ts +75 -0
- package/esm/pdbUniProtMapping.js +231 -0
- package/esm/pdbUniProtMapping.js.map +1 -0
- package/esm/pdbeBestStructures.d.ts +23 -0
- package/esm/pdbeBestStructures.js +73 -0
- package/esm/pdbeBestStructures.js.map +1 -0
- package/esm/pdbeEntryMolecules.d.ts +7 -0
- package/esm/pdbeEntryMolecules.js +35 -0
- package/esm/pdbeEntryMolecules.js.map +1 -0
- package/esm/siftsNumbering.d.ts +23 -0
- package/esm/siftsNumbering.js +40 -0
- package/esm/siftsNumbering.js.map +1 -0
- package/esm/stripStopCodon.d.ts +15 -0
- package/esm/stripStopCodon.js +20 -0
- package/esm/stripStopCodon.js.map +1 -0
- package/esm/structureFormat.d.ts +40 -0
- package/esm/structureFormat.js +48 -0
- package/esm/structureFormat.js.map +1 -0
- package/esm/structureSources.d.ts +28 -0
- package/esm/structureSources.js +71 -0
- package/esm/structureSources.js.map +1 -0
- package/esm/structureUrls.d.ts +54 -0
- package/esm/structureUrls.js +134 -0
- package/esm/structureUrls.js.map +1 -0
- package/esm/types.d.ts +14 -0
- package/esm/types.js +23 -0
- package/esm/types.js.map +1 -0
- package/esm/uniProtSearch.d.ts +32 -0
- package/esm/uniProtSearch.js +110 -0
- package/esm/uniProtSearch.js.map +1 -0
- package/package.json +63 -0
- package/src/alignmentQuality.ts +132 -0
- package/src/alphaFoldModels.ts +91 -0
- package/src/caCoordsToPdb.ts +111 -0
- package/src/chooseMappedEntity.ts +198 -0
- package/src/coordinates.ts +119 -0
- package/src/dbIds.ts +46 -0
- package/src/extractPerResidueConfidence.ts +88 -0
- package/src/extractStructureSequences.ts +342 -0
- package/src/fetchUtils.ts +110 -0
- package/src/index.ts +34 -0
- package/src/invertMap.ts +4 -0
- package/src/isoformRanking.ts +139 -0
- package/src/mappings.ts +230 -0
- package/src/pairwiseAlignment.ts +423 -0
- package/src/pdbUniProtMapping.ts +320 -0
- package/src/pdbeBestStructures.ts +94 -0
- package/src/pdbeEntryMolecules.ts +37 -0
- package/src/siftsNumbering.ts +61 -0
- package/src/stripStopCodon.ts +20 -0
- package/src/structureFormat.ts +70 -0
- package/src/structureSources.ts +101 -0
- package/src/structureUrls.ts +171 -0
- package/src/types.ts +30 -0
- package/src/uniProtSearch.ts +212 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The experimental structures 3D-Beacons knows for a UniProt entry: every PDBe
|
|
2
|
+
// entry mapped to the accession, with the UniProt range it covers and its
|
|
3
|
+
// resolution, in one CORS-enabled call. That is what lets a page offer a
|
|
4
|
+
// crystal structure beside a prediction without a PDB search of its own; take
|
|
5
|
+
// the entry id from here and ask `fetchUniProtStructureMappings` for its
|
|
6
|
+
// residue mapping.
|
|
7
|
+
|
|
8
|
+
import { rawfetch } from './fetchUtils.ts'
|
|
9
|
+
|
|
10
|
+
import type { FetchOptions } from './fetchUtils.ts'
|
|
11
|
+
|
|
12
|
+
export interface ExperimentalStructure {
|
|
13
|
+
pdbId: string
|
|
14
|
+
method: string
|
|
15
|
+
/** Å; absent for NMR and some EM entries */
|
|
16
|
+
resolution?: number
|
|
17
|
+
/** 1-based inclusive UniProt residues the entry covers */
|
|
18
|
+
start: number
|
|
19
|
+
end: number
|
|
20
|
+
/** fraction of the UniProt sequence the entry covers, 0..1 */
|
|
21
|
+
coverage: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function beaconsSummaryUrl(uniprotId: string) {
|
|
25
|
+
return `https://www.ebi.ac.uk/pdbe/pdbe-kb/3dbeacons/api/uniprot/summary/${encodeURIComponent(uniprotId)}.json?provider=pdbe`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function finiteNumber(value: unknown) {
|
|
33
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* PDBe entries only, best-covering first and sharpest within a tie.
|
|
38
|
+
*
|
|
39
|
+
* Predicted entries (AlphaFold, SWISS-MODEL, AlphaFill) are left out because a
|
|
40
|
+
* prediction comes from AlphaFold's own API, which answers per model. And
|
|
41
|
+
* 3D-Beacons files SASBDB's small-angle-scattering fits under "experimentally
|
|
42
|
+
* determined" too, with numeric ids and near-full coverage — dystrophin's best
|
|
43
|
+
* "structure" by coverage was SASBDB 436 — which are not entries a residue
|
|
44
|
+
* mapping exists for. Hence the `provider === 'PDBe'` gate rather than the
|
|
45
|
+
* category alone.
|
|
46
|
+
*/
|
|
47
|
+
export function parseExperimentalStructures(
|
|
48
|
+
json: unknown,
|
|
49
|
+
): ExperimentalStructure[] {
|
|
50
|
+
const structures = isRecord(json) ? json.structures : undefined
|
|
51
|
+
if (!Array.isArray(structures)) {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
return structures
|
|
55
|
+
.flatMap(entry => {
|
|
56
|
+
const s = isRecord(entry) ? entry.summary : undefined
|
|
57
|
+
if (!isRecord(s)) {
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
const start = finiteNumber(s.uniprot_start)
|
|
61
|
+
const end = finiteNumber(s.uniprot_end)
|
|
62
|
+
const coverage = finiteNumber(s.coverage)
|
|
63
|
+
return typeof s.model_identifier === 'string' &&
|
|
64
|
+
s.provider === 'PDBe' &&
|
|
65
|
+
s.model_category === 'EXPERIMENTALLY DETERMINED' &&
|
|
66
|
+
start !== undefined &&
|
|
67
|
+
end !== undefined &&
|
|
68
|
+
coverage !== undefined
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
pdbId: s.model_identifier.toLowerCase(),
|
|
72
|
+
method:
|
|
73
|
+
typeof s.experimental_method === 'string'
|
|
74
|
+
? s.experimental_method
|
|
75
|
+
: 'experimental',
|
|
76
|
+
resolution: finiteNumber(s.resolution),
|
|
77
|
+
start,
|
|
78
|
+
end,
|
|
79
|
+
coverage,
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
: []
|
|
83
|
+
})
|
|
84
|
+
.sort(
|
|
85
|
+
(a, b) =>
|
|
86
|
+
b.coverage - a.coverage ||
|
|
87
|
+
(a.resolution ?? Infinity) - (b.resolution ?? Infinity),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Best-effort: an unreachable API reads as "no entries", which costs a
|
|
92
|
+
* structure suggestion and nothing else. */
|
|
93
|
+
export async function fetchExperimentalStructures(
|
|
94
|
+
uniprotId: string,
|
|
95
|
+
opts?: FetchOptions,
|
|
96
|
+
): Promise<ExperimentalStructure[]> {
|
|
97
|
+
const res = await rawfetch(beaconsSummaryUrl(uniprotId), opts).catch(
|
|
98
|
+
() => undefined,
|
|
99
|
+
)
|
|
100
|
+
return res?.ok ? parseExperimentalStructures(await res.json()) : []
|
|
101
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Pure structure-URL builders and target parsers. Kept dependency-free so both
|
|
2
|
+
// the launch flow and the ProteinView model (e.g. resolving a `uniprotId`/`pdbId`
|
|
3
|
+
// shorthand at hydration) can import them without pulling in heavy launch code.
|
|
4
|
+
|
|
5
|
+
const ALPHAFOLD_VERSION = 'v6'
|
|
6
|
+
|
|
7
|
+
// UniProt endpoints. rest.* is the machine API (the GFF the feature tracks and
|
|
8
|
+
// the alignment's annotation rows both read, the FASTA the temporary protein
|
|
9
|
+
// assembly is built from); www.*/entry is the human-facing entry page the UI
|
|
10
|
+
// links to. Centralized so a path change is one edit rather than a grep.
|
|
11
|
+
export function uniprotGffUrl(uniprotId: string) {
|
|
12
|
+
return `https://rest.uniprot.org/uniprotkb/${uniprotId}.gff`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function uniprotFastaUrl(uniprotId: string) {
|
|
16
|
+
return `https://rest.uniprot.org/uniprotkb/${uniprotId}.fasta`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function uniprotEntryUrl(uniprotId: string) {
|
|
20
|
+
return `https://www.uniprot.org/uniprotkb/${uniprotId}/entry`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The guessed filename of an accession's canonical AlphaFold model. A guess:
|
|
25
|
+
* the F1 fragment does not exist past the length cap and the version moves, so
|
|
26
|
+
* this is the last resort when the prediction API cannot be reached. Ask
|
|
27
|
+
* `fetchAlphaFoldModels` for the real files.
|
|
28
|
+
*/
|
|
29
|
+
export function getAlphaFoldStructureUrl(
|
|
30
|
+
uniprotId: string,
|
|
31
|
+
version = ALPHAFOLD_VERSION,
|
|
32
|
+
) {
|
|
33
|
+
return `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-model_${version}.cif`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getPdbStructureUrl(pdbId: string) {
|
|
37
|
+
return `https://files.rcsb.org/download/${pdbId}.cif`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the `{ pdbId }` shorthand to a concrete structure URL, so a
|
|
42
|
+
* hand-authored snapshot or a short launch URL doesn't have to know RCSB's
|
|
43
|
+
* filename format. An explicit `url` (or inline `data`) always wins.
|
|
44
|
+
* Idempotent: re-resolving an already-resolved spec returns the same url.
|
|
45
|
+
*
|
|
46
|
+
* `uniprotId` deliberately resolves to no url here, and still outranks a
|
|
47
|
+
* `pdbId`. An accession's files are whatever AlphaFold DB currently holds — an
|
|
48
|
+
* isoform model, several fragments, a new model version — so the structure
|
|
49
|
+
* keeps the accession and the structure loader asks the prediction API which
|
|
50
|
+
* file to open.
|
|
51
|
+
*
|
|
52
|
+
* Shared by the Structure model's snapshot preprocessor and the
|
|
53
|
+
* LaunchView-ProteinView extension point, which would otherwise each carry
|
|
54
|
+
* their own copy of the same precedence rule.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveStructureUrl({
|
|
57
|
+
url,
|
|
58
|
+
data,
|
|
59
|
+
uniprotId,
|
|
60
|
+
pdbId,
|
|
61
|
+
}: {
|
|
62
|
+
url?: string
|
|
63
|
+
data?: string
|
|
64
|
+
uniprotId?: string
|
|
65
|
+
pdbId?: string
|
|
66
|
+
}) {
|
|
67
|
+
if (url !== undefined || data !== undefined) {
|
|
68
|
+
return url
|
|
69
|
+
}
|
|
70
|
+
return uniprotId !== undefined || pdbId === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: getPdbStructureUrl(pdbId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Hosts that serve the PDB archive, so a 4-character filename there really is
|
|
76
|
+
// that PDB entry. Restricted deliberately: a user-uploaded model happening to be
|
|
77
|
+
// named 1abc.cif must not pick up 1ABC's UniProt annotations.
|
|
78
|
+
const PDB_ARCHIVE_HOSTS = ['files.rcsb.org', 'www.ebi.ac.uk', 'ftp.ebi.ac.uk']
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The PDB id of a structure URL from the PDB archive, lowercased, or undefined
|
|
82
|
+
* for any other URL. Accepts the filename forms those hosts serve, e.g.
|
|
83
|
+
* `1TUP.cif`, `pdb1tup.ent.gz`, `1tup_updated.cif`.
|
|
84
|
+
*/
|
|
85
|
+
export function getPdbIdFromUrl(url: string) {
|
|
86
|
+
const { host, file } = (() => {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = new URL(url)
|
|
89
|
+
return { host: parsed.hostname, file: parsed.pathname.split('/').pop() }
|
|
90
|
+
} catch {
|
|
91
|
+
return { host: undefined, file: undefined }
|
|
92
|
+
}
|
|
93
|
+
})()
|
|
94
|
+
if (host === undefined || !PDB_ARCHIVE_HOSTS.includes(host)) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
const match =
|
|
98
|
+
/^(?:pdb)?([1-9][a-z0-9]{3})(?:_updated)?\.(?:cif|bcif|pdb|ent)(?:\.gz)?$/i.exec(
|
|
99
|
+
file ?? '',
|
|
100
|
+
)
|
|
101
|
+
return match?.[1]?.toLowerCase()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Foldseek targets may contain a description after the ID separated by a
|
|
105
|
+
// space, e.g. "AF-P16442-F1-model_v6 Histo-blood group ABO transferase".
|
|
106
|
+
function extractTargetId(target: string) {
|
|
107
|
+
return target.split(' ')[0]!
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The accession of an AlphaFold model id or url, with its isoform suffix:
|
|
111
|
+
* `AF-P16442-F1-model_v6` gives P16442, `AF-P04637-2-F1-model_v6` P04637-2. */
|
|
112
|
+
export function getUniprotIdFromAlphaFoldTarget(target: string) {
|
|
113
|
+
const match = /AF-([A-Z0-9]+(?:-\d+)?)-F\d+/.exec(extractTargetId(target))
|
|
114
|
+
return match?.[1]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Foldseek names a pdb100 hit by the assembly file and chain it searched,
|
|
118
|
+
// `8f2h-assembly1.cif.gz_A`; the entry id is the first four characters.
|
|
119
|
+
export function getStructureUrlFromTarget(target: string, db: string) {
|
|
120
|
+
const targetId = extractTargetId(target)
|
|
121
|
+
if (targetId.startsWith('AF-')) {
|
|
122
|
+
return `https://alphafold.ebi.ac.uk/files/${targetId}.cif`
|
|
123
|
+
}
|
|
124
|
+
const pdbId =
|
|
125
|
+
db === 'pdb100'
|
|
126
|
+
? /^([1-9][a-z0-9]{3})(?:[-_.]|$)/i.exec(targetId)?.[1]
|
|
127
|
+
: undefined
|
|
128
|
+
return pdbId ? getPdbStructureUrl(pdbId) : undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getConfidenceUrlFromTarget(target: string) {
|
|
132
|
+
const targetId = extractTargetId(target)
|
|
133
|
+
if (targetId.startsWith('AF-')) {
|
|
134
|
+
const confidenceId = targetId.replace('-model_', '-confidence_')
|
|
135
|
+
return `https://alphafold.ebi.ac.uk/files/${confidenceId}.json`
|
|
136
|
+
}
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What a structure is called in the UI: its PDB id, its AlphaFold accession,
|
|
142
|
+
* else the file's name. Shared by the view title, the alignment panel heading
|
|
143
|
+
* and the hover readout, so one structure reads the same everywhere.
|
|
144
|
+
*/
|
|
145
|
+
export function structureDisplayLabel({
|
|
146
|
+
url,
|
|
147
|
+
data,
|
|
148
|
+
uniprotId,
|
|
149
|
+
}: {
|
|
150
|
+
url?: string
|
|
151
|
+
data?: string
|
|
152
|
+
uniprotId?: string
|
|
153
|
+
}) {
|
|
154
|
+
if (url === undefined) {
|
|
155
|
+
return data !== undefined
|
|
156
|
+
? 'Uploaded structure'
|
|
157
|
+
: uniprotId
|
|
158
|
+
? `AlphaFold ${uniprotId}`
|
|
159
|
+
: ''
|
|
160
|
+
}
|
|
161
|
+
const pdbId = getPdbIdFromUrl(url)
|
|
162
|
+
if (pdbId) {
|
|
163
|
+
return pdbId.toUpperCase()
|
|
164
|
+
}
|
|
165
|
+
const urlAccession = getUniprotIdFromAlphaFoldTarget(url)
|
|
166
|
+
if (urlAccession) {
|
|
167
|
+
return `AlphaFold ${urlAccession}`
|
|
168
|
+
}
|
|
169
|
+
const name = url.split('/').pop()?.split('?')[0]
|
|
170
|
+
return name === undefined || name === '' ? url : name
|
|
171
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type AlignmentAlgorithm = 'needleman_wunsch' | 'smith_waterman'
|
|
2
|
+
|
|
3
|
+
export const ALIGNMENT_ALGORITHMS = {
|
|
4
|
+
NEEDLEMAN_WUNSCH: 'needleman_wunsch',
|
|
5
|
+
SMITH_WATERMAN: 'smith_waterman',
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_ALIGNMENT_ALGORITHM: AlignmentAlgorithm = 'smith_waterman'
|
|
9
|
+
|
|
10
|
+
export const ALIGNMENT_ALGORITHM_VALUES: AlignmentAlgorithm[] = [
|
|
11
|
+
'needleman_wunsch',
|
|
12
|
+
'smith_waterman',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses an untrusted algorithm name (a URL session-spec param) into the union.
|
|
17
|
+
* The only place a coercion is needed: the model property is an enumeration, so
|
|
18
|
+
* everything downstream of hydration is already typed.
|
|
19
|
+
*/
|
|
20
|
+
export function coerceAlignmentAlgorithm(value: string): AlignmentAlgorithm {
|
|
21
|
+
return (
|
|
22
|
+
ALIGNMENT_ALGORITHM_VALUES.find(v => v === value) ??
|
|
23
|
+
DEFAULT_ALIGNMENT_ALGORITHM
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ALIGNMENT_ALGORITHM_LABELS: Record<AlignmentAlgorithm, string> = {
|
|
28
|
+
needleman_wunsch: 'Needleman-Wunsch',
|
|
29
|
+
smith_waterman: 'Smith-Waterman',
|
|
30
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { jsonfetch } from './fetchUtils.ts'
|
|
2
|
+
import {
|
|
3
|
+
buildUniProtXrefQuery,
|
|
4
|
+
isRecognizedDatabaseId,
|
|
5
|
+
stripTrailingVersion,
|
|
6
|
+
} from './dbIds.ts'
|
|
7
|
+
|
|
8
|
+
import type { FetchOptions } from './fetchUtils.ts'
|
|
9
|
+
|
|
10
|
+
interface UniProtApiResult {
|
|
11
|
+
results: {
|
|
12
|
+
entryType: string
|
|
13
|
+
primaryAccession: string
|
|
14
|
+
uniProtkbId?: string
|
|
15
|
+
genes?: {
|
|
16
|
+
geneName?: {
|
|
17
|
+
value: string
|
|
18
|
+
}
|
|
19
|
+
}[]
|
|
20
|
+
organism?: {
|
|
21
|
+
taxonId: number
|
|
22
|
+
scientificName?: string
|
|
23
|
+
commonName?: string
|
|
24
|
+
}
|
|
25
|
+
proteinDescription?: {
|
|
26
|
+
recommendedName?: {
|
|
27
|
+
fullName?: {
|
|
28
|
+
value: string
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UniProtEntry {
|
|
36
|
+
accession: string
|
|
37
|
+
id?: string
|
|
38
|
+
geneName?: string
|
|
39
|
+
organismName?: string
|
|
40
|
+
proteinName?: string
|
|
41
|
+
isReviewed: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const UNIPROT_FIELDS =
|
|
45
|
+
'accession,id,gene_names,organism_name,protein_name,reviewed'
|
|
46
|
+
|
|
47
|
+
function mapApiResultToEntry(
|
|
48
|
+
result: UniProtApiResult['results'][0],
|
|
49
|
+
): UniProtEntry {
|
|
50
|
+
return {
|
|
51
|
+
accession: result.primaryAccession,
|
|
52
|
+
id: result.uniProtkbId,
|
|
53
|
+
geneName: result.genes?.[0]?.geneName?.value,
|
|
54
|
+
organismName:
|
|
55
|
+
result.organism?.commonName ?? result.organism?.scientificName,
|
|
56
|
+
proteinName: result.proteinDescription?.recommendedName?.fullName?.value,
|
|
57
|
+
isReviewed: result.entryType === 'UniProtKB reviewed (Swiss-Prot)',
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function searchUniProt(
|
|
62
|
+
query: string,
|
|
63
|
+
size: number,
|
|
64
|
+
opts?: FetchOptions,
|
|
65
|
+
): Promise<UniProtEntry[]> {
|
|
66
|
+
const url = `https://rest.uniprot.org/uniprotkb/search?query=${encodeURIComponent(query)}&fields=${UNIPROT_FIELDS}&size=${size}`
|
|
67
|
+
const data = await jsonfetch<UniProtApiResult>(url, opts)
|
|
68
|
+
return data.results.map(mapApiResultToEntry)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `gene_exact` rather than `gene`, which also matches synonyms and returns
|
|
73
|
+
* paralogs. Without a taxon the query runs across every species, so a mouse
|
|
74
|
+
* assembly whose tracks carry no taxId lists mouse beside human instead of
|
|
75
|
+
* silently answering with the human entry.
|
|
76
|
+
*
|
|
77
|
+
* The symbol is quoted because a bare one containing `:` — an HLA allele, a
|
|
78
|
+
* name straight out of an annotation file — reads as another field to
|
|
79
|
+
* UniProt's parser, which answers 400.
|
|
80
|
+
*/
|
|
81
|
+
export function buildGeneNameQuery(geneName: string, organismId?: number) {
|
|
82
|
+
return [
|
|
83
|
+
`gene_exact:"${geneName.replaceAll('"', '\\"')}"`,
|
|
84
|
+
organismId ? `organism_id:${organismId}` : undefined,
|
|
85
|
+
'reviewed:true',
|
|
86
|
+
]
|
|
87
|
+
.filter(s => s !== undefined)
|
|
88
|
+
.join(' AND ')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface SearchAttempt {
|
|
92
|
+
entries: UniProtEntry[]
|
|
93
|
+
error: unknown
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function searchByXref(
|
|
97
|
+
id: string,
|
|
98
|
+
opts?: FetchOptions,
|
|
99
|
+
): Promise<SearchAttempt> {
|
|
100
|
+
const query = buildUniProtXrefQuery(id)
|
|
101
|
+
if (!query) {
|
|
102
|
+
return { entries: [], error: undefined }
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return { entries: await searchUniProt(query, 10, opts), error: undefined }
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(`xref search failed for ${id}:`, e)
|
|
108
|
+
return { entries: [], error: e }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function searchByGeneName(
|
|
113
|
+
geneName: string,
|
|
114
|
+
organismId: number | undefined,
|
|
115
|
+
opts?: FetchOptions,
|
|
116
|
+
): Promise<SearchAttempt> {
|
|
117
|
+
try {
|
|
118
|
+
const entries = await searchUniProt(
|
|
119
|
+
buildGeneNameQuery(geneName, organismId),
|
|
120
|
+
organismId ? 5 : 10,
|
|
121
|
+
opts,
|
|
122
|
+
)
|
|
123
|
+
return { entries, error: undefined }
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(`gene name search failed for ${geneName}:`, e)
|
|
126
|
+
return { entries: [], error: e }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function deduplicateEntries(entries: UniProtEntry[]) {
|
|
131
|
+
const seen = new Set<string>()
|
|
132
|
+
const result: UniProtEntry[] = []
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!seen.has(entry.accession)) {
|
|
135
|
+
seen.add(entry.accession)
|
|
136
|
+
result.push(entry)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface UniProtSearchResult {
|
|
143
|
+
entries: UniProtEntry[]
|
|
144
|
+
attemptedCount: number
|
|
145
|
+
failedCount: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function searchUniProtEntries(
|
|
149
|
+
{
|
|
150
|
+
recognizedIds = [],
|
|
151
|
+
geneId,
|
|
152
|
+
geneName,
|
|
153
|
+
organismId,
|
|
154
|
+
}: {
|
|
155
|
+
recognizedIds?: string[]
|
|
156
|
+
geneId?: string
|
|
157
|
+
geneName?: string
|
|
158
|
+
/** NCBI taxon id; undefined searches every species */
|
|
159
|
+
organismId?: number
|
|
160
|
+
},
|
|
161
|
+
opts?: FetchOptions,
|
|
162
|
+
): Promise<UniProtSearchResult> {
|
|
163
|
+
const idsToSearch = new Set(recognizedIds)
|
|
164
|
+
const strippedGeneId = geneId ? stripTrailingVersion(geneId) : undefined
|
|
165
|
+
if (strippedGeneId && isRecognizedDatabaseId(strippedGeneId)) {
|
|
166
|
+
idsToSearch.add(strippedGeneId)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The gene-name query runs alongside the xrefs rather than after them: it is
|
|
170
|
+
// only consulted when no xref found a reviewed entry, but waiting for that
|
|
171
|
+
// answer before starting it doubled the latency of the commonest case.
|
|
172
|
+
const [xrefResults, geneResult] = await Promise.all([
|
|
173
|
+
Promise.all([...idsToSearch].map(id => searchByXref(id, opts))),
|
|
174
|
+
geneName ? searchByGeneName(geneName, organismId, opts) : undefined,
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
let entries = deduplicateEntries(xrefResults.flatMap(r => r.entries))
|
|
178
|
+
// The gene-name answer is consulted only where no xref found a reviewed
|
|
179
|
+
// entry. Counting it otherwise reported "failed for 1 of 2 identifiers" over
|
|
180
|
+
// a gene that had in fact resolved, off a query nothing was waiting for.
|
|
181
|
+
const consultedGeneResult =
|
|
182
|
+
geneResult && !entries.some(e => e.isReviewed) ? geneResult : undefined
|
|
183
|
+
if (consultedGeneResult) {
|
|
184
|
+
entries = deduplicateEntries([...entries, ...consultedGeneResult.entries])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const attemptedCount = idsToSearch.size + (consultedGeneResult ? 1 : 0)
|
|
188
|
+
const failedCount =
|
|
189
|
+
xrefResults.filter(r => r.error !== undefined).length +
|
|
190
|
+
(consultedGeneResult?.error === undefined ? 0 : 1)
|
|
191
|
+
|
|
192
|
+
// Every attempt failing is a network problem, not an empty result. Throwing
|
|
193
|
+
// it stops consumers reporting "No UniProt ID found" over a dead connection.
|
|
194
|
+
if (
|
|
195
|
+
entries.length === 0 &&
|
|
196
|
+
attemptedCount > 0 &&
|
|
197
|
+
attemptedCount === failedCount
|
|
198
|
+
) {
|
|
199
|
+
throw (
|
|
200
|
+
consultedGeneResult?.error ??
|
|
201
|
+
xrefResults.find(r => r.error !== undefined)?.error
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
entries: entries.toSorted(
|
|
207
|
+
(a, b) => Number(b.isReviewed) - Number(a.isReviewed),
|
|
208
|
+
),
|
|
209
|
+
attemptedCount,
|
|
210
|
+
failedCount,
|
|
211
|
+
}
|
|
212
|
+
}
|