@jbrowse/plugin-rdf 4.2.1 → 5.0.0-beta.1

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.
@@ -1,11 +1,10 @@
1
1
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
- import type MyConfigSchema from './configSchema.ts';
2
+ import type { SPARQLAdapterConfig } from './configSchema.ts';
3
3
  import type PluginManager from '@jbrowse/core/PluginManager';
4
4
  import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
5
5
  import type { getSubAdapterType } from '@jbrowse/core/data_adapters/dataAdapterCache';
6
6
  import type { Feature } from '@jbrowse/core/util/simpleFeature';
7
7
  import type { NoAssemblyRegion } from '@jbrowse/core/util/types';
8
- import type { Instance } from '@jbrowse/mobx-state-tree';
9
8
  export default class SPARQLAdapter extends BaseFeatureDataAdapter {
10
9
  private endpoint;
11
10
  private queryTemplate;
@@ -13,7 +12,7 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
13
12
  private additionalQueryParams;
14
13
  private configRefNames;
15
14
  private refNames;
16
- constructor(config: Instance<typeof MyConfigSchema>, getSubAdapter?: getSubAdapterType, pluginManager?: PluginManager);
15
+ constructor(config: SPARQLAdapterConfig, getSubAdapter?: getSubAdapterType, pluginManager?: PluginManager);
17
16
  getRefNames(opts?: BaseOptions): Promise<string[]>;
18
17
  getFeatures(query: NoAssemblyRegion, opts?: BaseOptions): import("rxjs").Observable<Feature>;
19
18
  private querySparql;
@@ -2,7 +2,15 @@ import { readConfObject } from '@jbrowse/core/configuration';
2
2
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
3
3
  import { ObservableCreate } from '@jbrowse/core/util/rxjs';
4
4
  import SimpleFeature from '@jbrowse/core/util/simpleFeature';
5
- import format from 'string-template';
5
+ import { checkStopToken } from '@jbrowse/core/util/stopToken';
6
+ // fill `{name}` placeholders from data; `{{name}}` is an escaped literal.
7
+ // replaces the `string-template` dependency
8
+ const templateRegex = /\{([0-9a-zA-Z_]+)\}/g;
9
+ function fillTemplate(template, data) {
10
+ return template.replaceAll(templateRegex, (match, key, index) => template[index - 1] === '{' && template[index + match.length] === '}'
11
+ ? key
12
+ : `${data[key] ?? ''}`);
13
+ }
6
14
  export default class SPARQLAdapter extends BaseFeatureDataAdapter {
7
15
  endpoint;
8
16
  queryTemplate;
@@ -12,7 +20,8 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
12
20
  refNames;
13
21
  constructor(config, getSubAdapter, pluginManager) {
14
22
  super(config, getSubAdapter, pluginManager);
15
- this.endpoint = readConfObject(config, 'endpoint').uri;
23
+ const endpoint = readConfObject(config, 'endpoint');
24
+ this.endpoint = 'uri' in endpoint ? endpoint.uri : '';
16
25
  this.queryTemplate = readConfObject(config, 'queryTemplate');
17
26
  this.additionalQueryParams = readConfObject(config, 'additionalQueryParams');
18
27
  this.refNamesQueryTemplate = readConfObject(config, 'refNamesQueryTemplate');
@@ -34,8 +43,8 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
34
43
  }
35
44
  getFeatures(query, opts = {}) {
36
45
  return ObservableCreate(async (observer) => {
37
- const filledTemplate = encodeURIComponent(format(this.queryTemplate, query));
38
- const { refName } = query;
46
+ const { refName, start, end } = query;
47
+ const filledTemplate = encodeURIComponent(fillTemplate(this.queryTemplate, { refName, start, end }));
39
48
  const results = await this.querySparql(filledTemplate, opts);
40
49
  const features = this.resultsToFeatures(results, refName);
41
50
  for (const feature of features) {
@@ -44,21 +53,26 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
44
53
  observer.complete();
45
54
  }, opts.stopToken);
46
55
  }
47
- async querySparql(query, _opts) {
56
+ async querySparql(query, opts) {
48
57
  let additionalQueryParams = '';
49
58
  if (this.additionalQueryParams.length) {
50
59
  additionalQueryParams = `&${this.additionalQueryParams.join('&')}`;
51
60
  }
61
+ checkStopToken(opts?.stopToken);
52
62
  const url = `${this.endpoint}?query=${query}${additionalQueryParams}`;
53
63
  const response = await fetch(url, {
54
64
  headers: {
55
65
  accept: 'application/json,application/sparql-results+json',
56
66
  },
57
67
  });
68
+ if (!response.ok) {
69
+ throw new Error(`SPARQL query failed: ${response.status} ${response.statusText}`);
70
+ }
71
+ checkStopToken(opts?.stopToken);
58
72
  return response.json();
59
73
  }
60
74
  resultsToRefNames(response) {
61
- const rows = response.results.bindings || [];
75
+ const rows = response.results.bindings ?? [];
62
76
  const fields = response.head.vars;
63
77
  if (!fields.includes('refName')) {
64
78
  throw new Error('"refName" not found in refNamesQueryTemplate response');
@@ -66,7 +80,7 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
66
80
  return rows.map(row => row.refName.value);
67
81
  }
68
82
  resultsToFeatures(results, refName) {
69
- const rows = results.results.bindings || [];
83
+ const rows = results.results.bindings ?? [];
70
84
  const fields = results.head.vars;
71
85
  const requiredFields = ['start', 'end', 'uniqueId'];
72
86
  for (const requiredField of requiredFields) {
@@ -74,91 +88,59 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
74
88
  console.error(`Required field ${requiredField} missing from feature data`);
75
89
  }
76
90
  }
77
- const seenFeatures = {};
91
+ // Each row encodes a feature plus its sub_/sub_sub_ ancestors. Flatten
92
+ // every level into a single uniqueId->feature map and record each level's
93
+ // parent, then attach children to parents in a second pass. Building the
94
+ // full map first makes attachment independent of row/level ordering.
95
+ const featuresById = new Map();
96
+ const parentById = new Map();
78
97
  for (const row of rows) {
79
- const rawData = [{}];
80
- for (let field of fields) {
98
+ const levels = [{}];
99
+ for (const field of fields) {
81
100
  if (field in row) {
82
- const { value } = row[field];
83
- let idx = 0;
84
- while (field.startsWith('sub_')) {
85
- field = field.slice(4);
86
- idx += 1;
101
+ let name = field;
102
+ let depth = 0;
103
+ while (name.startsWith('sub_')) {
104
+ name = name.slice(4);
105
+ depth += 1;
87
106
  }
88
- while (idx > rawData.length - 1) {
89
- rawData.push({});
107
+ while (depth > levels.length - 1) {
108
+ levels.push({});
90
109
  }
91
- rawData[idx][field] = value;
110
+ levels[depth][name] = row[field].value;
92
111
  }
93
112
  }
94
- for (const [idx, rd] of rawData.entries()) {
95
- const { uniqueId, start, end, strand } = rd;
96
- if (idx < rawData.length - 1) {
97
- rawData[idx + 1].parentUniqueId = uniqueId;
113
+ for (const [depth, level] of levels.entries()) {
114
+ const { uniqueId, start, end, strand } = level;
115
+ featuresById.set(uniqueId, {
116
+ ...level,
117
+ uniqueId: uniqueId,
118
+ refName,
119
+ start: Number.parseInt(start, 10),
120
+ end: Number.parseInt(end, 10),
121
+ strand: Number.parseInt(strand, 10) || 0,
122
+ });
123
+ if (depth > 0) {
124
+ parentById.set(uniqueId, levels[depth - 1].uniqueId);
98
125
  }
99
- seenFeatures[uniqueId] = {
100
- data: {
101
- ...rd,
102
- uniqueId: uniqueId,
103
- refName,
104
- start: Number.parseInt(start, 10),
105
- end: Number.parseInt(end, 10),
106
- strand: Number.parseInt(strand, 10) || 0,
107
- },
108
- };
109
126
  }
110
127
  }
111
- for (const [uniqueId, f] of Object.entries(seenFeatures)) {
112
- const pid = f.data.parentUniqueId;
113
- f.data.parentUniqueId = undefined;
114
- if (pid) {
115
- const p = seenFeatures[pid];
116
- if (p) {
117
- if (!p.data.subfeatures) {
118
- p.data.subfeatures = [];
119
- }
120
- p.data.subfeatures.push({
121
- ...f.data,
122
- uniqueId,
123
- });
124
- delete seenFeatures[uniqueId];
125
- }
126
- else {
127
- const subfeatures = Object.values(seenFeatures)
128
- .map(sf => sf.data.subfeatures)
129
- .filter(sf => !!sf)
130
- .flat();
131
- let found = false;
132
- for (const subfeature of subfeatures) {
133
- if (subfeature.uniqueId === pid) {
134
- if (!subfeature.subfeatures) {
135
- subfeature.subfeatures = [];
136
- }
137
- subfeature.subfeatures.push({
138
- ...f.data,
139
- uniqueId,
140
- });
141
- delete seenFeatures[uniqueId];
142
- found = true;
143
- break;
144
- }
145
- if (subfeature.subfeatures) {
146
- for (const sf of subfeature.subfeatures) {
147
- subfeatures.push(sf);
148
- }
149
- }
150
- }
151
- if (!found) {
152
- console.error(`Could not find parentID ${pid}`);
153
- }
128
+ const roots = [];
129
+ for (const [uniqueId, feature] of featuresById) {
130
+ const pid = parentById.get(uniqueId);
131
+ const parent = pid ? featuresById.get(pid) : undefined;
132
+ if (parent) {
133
+ parent.subfeatures ??= [];
134
+ parent.subfeatures.push(feature);
135
+ }
136
+ else {
137
+ if (pid) {
138
+ console.error(`Could not find parentID ${pid}`);
154
139
  }
140
+ roots.push(feature);
155
141
  }
156
142
  }
157
- return Object.keys(seenFeatures).map(seenFeature => new SimpleFeature({
158
- ...seenFeatures[seenFeature].data,
159
- uniqueId: seenFeature,
160
- subfeatures: seenFeatures[seenFeature].data.subfeatures,
161
- }));
143
+ return roots.map(data => new SimpleFeature(data));
162
144
  }
163
145
  async hasDataForRefName(refName, opts = {}) {
164
146
  const refNames = await this.getRefNames(opts);
@@ -1,31 +1,72 @@
1
- declare const _default: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
2
- endpoint: {
3
- type: string;
4
- defaultValue: {
5
- uri: string;
6
- locationType: string;
1
+ import type { Instance } from '@jbrowse/mobx-state-tree';
2
+ /**
3
+ * #config SPARQLAdapter
4
+ * #trackType FeatureTrack
5
+ * #category adapter
6
+ * fetches features from a SPARQL endpoint, substituting the queried region into
7
+ * a query template
8
+ *
9
+ * #example
10
+ * `{refName}`, `{start}` and `{end}` are substituted per request, so the
11
+ * endpoint is queried for the visible window rather than the whole genome. The
12
+ * result columns become feature fields, so the query has to select at least
13
+ * `?start`, `?end` and a `?uniqueID`:
14
+ * ```js
15
+ * {
16
+ * type: 'SPARQLAdapter',
17
+ * endpoint: { uri: 'https://example.com/sparql' },
18
+ * queryTemplate: `SELECT ?uniqueID ?start ?end ?strand ?name WHERE {
19
+ * ?f a :Feature ; :ref "{refName}" ; :start ?start ; :end ?end .
20
+ * FILTER(?start < {end} && ?end > {start})
21
+ * }`,
22
+ * refNamesQueryTemplate: 'SELECT DISTINCT ?refName WHERE { ?f :ref ?refName }',
23
+ * }
24
+ * ```
25
+ */
26
+ declare const SPARQLAdapterConfigSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
27
+ /**
28
+ * #slot
29
+ */
30
+ readonly endpoint: {
31
+ readonly type: "fileLocation";
32
+ readonly defaultValue: {
33
+ readonly uri: "https://somesite.com/sparql";
34
+ readonly locationType: "UriLocation";
7
35
  };
8
- description: string;
36
+ readonly description: "URL of the SPARQL endpoint";
9
37
  };
10
- queryTemplate: {
11
- type: string;
12
- defaultValue: string;
13
- description: string;
38
+ /**
39
+ * #slot
40
+ */
41
+ readonly queryTemplate: {
42
+ readonly type: "text";
43
+ readonly defaultValue: "";
44
+ readonly description: "SPARQL query where {start} {end} and {refName} will get replaced for each call";
14
45
  };
15
- refNamesQueryTemplate: {
16
- type: string;
17
- defaultValue: string;
18
- description: string;
46
+ /**
47
+ * #slot
48
+ */
49
+ readonly refNamesQueryTemplate: {
50
+ readonly type: "text";
51
+ readonly defaultValue: "";
52
+ readonly description: "SPARQL query that returns the possible refNames in a ?refName column";
19
53
  };
20
- refNames: {
21
- type: string;
22
- defaultValue: never[];
23
- description: string;
54
+ /**
55
+ * #slot
56
+ */
57
+ readonly refNames: {
58
+ readonly type: "stringArray";
59
+ readonly defaultValue: readonly [];
60
+ readonly description: "Possible refNames used by the SPARQL endpoint (ignored if \"refNamesQueryTemplate\" is provided)";
24
61
  };
25
- additionalQueryParams: {
26
- type: string;
27
- defaultValue: never[];
28
- description: string;
62
+ /**
63
+ * #slot
64
+ */
65
+ readonly additionalQueryParams: {
66
+ readonly type: "stringArray";
67
+ readonly defaultValue: readonly [];
68
+ readonly description: "Additional parameters to add to the query, e.g. \"format=JSON\"";
29
69
  };
30
70
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
31
- export default _default;
71
+ export default SPARQLAdapterConfigSchema;
72
+ export type SPARQLAdapterConfig = Instance<typeof SPARQLAdapterConfigSchema>;
@@ -1,5 +1,32 @@
1
1
  import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
- export default ConfigurationSchema('SPARQLAdapter', {
2
+ /**
3
+ * #config SPARQLAdapter
4
+ * #trackType FeatureTrack
5
+ * #category adapter
6
+ * fetches features from a SPARQL endpoint, substituting the queried region into
7
+ * a query template
8
+ *
9
+ * #example
10
+ * `{refName}`, `{start}` and `{end}` are substituted per request, so the
11
+ * endpoint is queried for the visible window rather than the whole genome. The
12
+ * result columns become feature fields, so the query has to select at least
13
+ * `?start`, `?end` and a `?uniqueID`:
14
+ * ```js
15
+ * {
16
+ * type: 'SPARQLAdapter',
17
+ * endpoint: { uri: 'https://example.com/sparql' },
18
+ * queryTemplate: `SELECT ?uniqueID ?start ?end ?strand ?name WHERE {
19
+ * ?f a :Feature ; :ref "{refName}" ; :start ?start ; :end ?end .
20
+ * FILTER(?start < {end} && ?end > {start})
21
+ * }`,
22
+ * refNamesQueryTemplate: 'SELECT DISTINCT ?refName WHERE { ?f :ref ?refName }',
23
+ * }
24
+ * ```
25
+ */
26
+ const SPARQLAdapterConfigSchema = ConfigurationSchema('SPARQLAdapter', {
27
+ /**
28
+ * #slot
29
+ */
3
30
  endpoint: {
4
31
  type: 'fileLocation',
5
32
  defaultValue: {
@@ -8,24 +35,37 @@ export default ConfigurationSchema('SPARQLAdapter', {
8
35
  },
9
36
  description: 'URL of the SPARQL endpoint',
10
37
  },
38
+ /**
39
+ * #slot
40
+ */
11
41
  queryTemplate: {
12
42
  type: 'text',
13
43
  defaultValue: '',
14
44
  description: 'SPARQL query where {start} {end} and {refName} will get replaced for each call',
15
45
  },
46
+ /**
47
+ * #slot
48
+ */
16
49
  refNamesQueryTemplate: {
17
50
  type: 'text',
18
51
  defaultValue: '',
19
52
  description: 'SPARQL query that returns the possible refNames in a ?refName column',
20
53
  },
54
+ /**
55
+ * #slot
56
+ */
21
57
  refNames: {
22
58
  type: 'stringArray',
23
59
  defaultValue: [],
24
60
  description: 'Possible refNames used by the SPARQL endpoint (ignored if "refNamesQueryTemplate" is provided)',
25
61
  },
62
+ /**
63
+ * #slot
64
+ */
26
65
  additionalQueryParams: {
27
66
  type: 'stringArray',
28
67
  defaultValue: [],
29
68
  description: 'Additional parameters to add to the query, e.g. "format=JSON"',
30
69
  },
31
70
  }, { explicitlyTyped: true });
71
+ export default SPARQLAdapterConfigSchema;
@@ -1,5 +1,5 @@
1
1
  import { AdapterType } from '@jbrowse/core/pluggableElementTypes';
2
- import configSchema from "./configSchema.js";
2
+ import configSchema from './configSchema.js';
3
3
  export default function SPARQLAdapterF(pluginManager) {
4
4
  pluginManager.addAdapterType(() => new AdapterType({
5
5
  name: 'SPARQLAdapter',
@@ -8,6 +8,6 @@ export default function SPARQLAdapterF(pluginManager) {
8
8
  adapterMetadata: {
9
9
  category: 'Uncommon',
10
10
  },
11
- getAdapterClass: () => import("./SPARQLAdapter.js").then(r => r.default),
11
+ getAdapterClass: () => import('./SPARQLAdapter.js').then(r => r.default),
12
12
  }));
13
13
  }
package/esm/index.js CHANGED
@@ -1,23 +1,8 @@
1
1
  import Plugin from '@jbrowse/core/Plugin';
2
- import { getFileName } from '@jbrowse/core/util/tracks';
3
- import SPARQLAdapterF from "./SPARQLAdapter/index.js";
2
+ import SPARQLAdapterF from './SPARQLAdapter/index.js';
4
3
  export default class RdfPlugin extends Plugin {
5
4
  name = 'RdfPlugin';
6
5
  install(pluginManager) {
7
6
  SPARQLAdapterF(pluginManager);
8
- pluginManager.addToExtensionPoint('Core-guessAdapterForLocation', (adapterGuesser) => {
9
- return (file, index, adapterHint) => {
10
- const regexGuess = /\/sparql$/i;
11
- const adapterName = 'SPARQLAdapter';
12
- const fileName = getFileName(file);
13
- if (regexGuess.test(fileName) || adapterHint === adapterName) {
14
- return {
15
- type: adapterName,
16
- endpoint: file,
17
- };
18
- }
19
- return adapterGuesser(file, index, adapterHint);
20
- };
21
- });
22
7
  }
23
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-rdf",
3
- "version": "4.2.1",
3
+ "version": "5.0.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 RDF resources",
6
6
  "keywords": [
@@ -21,11 +21,10 @@
21
21
  "esm"
22
22
  ],
23
23
  "dependencies": {
24
- "@jbrowse/mobx-state-tree": "^5.6.0",
25
- "mobx": "^6.15.0",
24
+ "@jbrowse/mobx-state-tree": "^6.5.1",
25
+ "mobx": "^7.0.3",
26
26
  "rxjs": "^7.8.2",
27
- "string-template": "^1.0.0",
28
- "@jbrowse/core": "^4.2.1"
27
+ "@jbrowse/core": "^5.0.0-beta.1"
29
28
  },
30
29
  "publishConfig": {
31
30
  "access": "public"
@@ -35,14 +34,11 @@
35
34
  "build": "pnpm run /^build:/",
36
35
  "test": "cd ../..; jest --passWithNoTests plugins/rdf",
37
36
  "prebuild": "pnpm clean",
38
- "build:esm": "tsc -p tsconfig.build.esm.json",
37
+ "build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
39
38
  "clean": "rimraf esm *.tsbuildinfo"
40
39
  },
41
40
  "types": "esm/index.d.ts",
42
41
  "exports": {
43
- ".": {
44
- "types": "./esm/index.d.ts",
45
- "import": "./esm/index.js"
46
- }
42
+ ".": "./esm/index.js"
47
43
  }
48
44
  }