@jbrowse/plugin-rdf 4.3.0 → 5.0.0-beta.10

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;
@@ -1,8 +1,16 @@
1
1
  import { readConfObject } from '@jbrowse/core/configuration';
2
2
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
3
+ import { checkAbortSignal } from '@jbrowse/core/util/aborting';
3
4
  import { ObservableCreate } from '@jbrowse/core/util/rxjs';
4
5
  import SimpleFeature from '@jbrowse/core/util/simpleFeature';
5
- import format from 'string-template';
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,31 +43,37 @@ 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) {
42
51
  observer.next(feature);
43
52
  }
44
53
  observer.complete();
45
- }, opts.stopToken);
54
+ }, opts.signal);
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
+ checkAbortSignal(opts?.signal);
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
  },
67
+ signal: opts?.signal,
57
68
  });
69
+ if (!response.ok) {
70
+ throw new Error(`SPARQL query failed: ${response.status} ${response.statusText}`);
71
+ }
72
+ checkAbortSignal(opts?.signal);
58
73
  return response.json();
59
74
  }
60
75
  resultsToRefNames(response) {
61
- const rows = response.results.bindings || [];
76
+ const rows = response.results.bindings ?? [];
62
77
  const fields = response.head.vars;
63
78
  if (!fields.includes('refName')) {
64
79
  throw new Error('"refName" not found in refNamesQueryTemplate response');
@@ -66,7 +81,7 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
66
81
  return rows.map(row => row.refName.value);
67
82
  }
68
83
  resultsToFeatures(results, refName) {
69
- const rows = results.results.bindings || [];
84
+ const rows = results.results.bindings ?? [];
70
85
  const fields = results.head.vars;
71
86
  const requiredFields = ['start', 'end', 'uniqueId'];
72
87
  for (const requiredField of requiredFields) {
@@ -74,91 +89,59 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter {
74
89
  console.error(`Required field ${requiredField} missing from feature data`);
75
90
  }
76
91
  }
77
- const seenFeatures = {};
92
+ // Each row encodes a feature plus its sub_/sub_sub_ ancestors. Flatten
93
+ // every level into a single uniqueId->feature map and record each level's
94
+ // parent, then attach children to parents in a second pass. Building the
95
+ // full map first makes attachment independent of row/level ordering.
96
+ const featuresById = new Map();
97
+ const parentById = new Map();
78
98
  for (const row of rows) {
79
- const rawData = [{}];
80
- for (let field of fields) {
99
+ const levels = [{}];
100
+ for (const field of fields) {
81
101
  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;
102
+ let name = field;
103
+ let depth = 0;
104
+ while (name.startsWith('sub_')) {
105
+ name = name.slice(4);
106
+ depth += 1;
87
107
  }
88
- while (idx > rawData.length - 1) {
89
- rawData.push({});
108
+ while (depth > levels.length - 1) {
109
+ levels.push({});
90
110
  }
91
- rawData[idx][field] = value;
111
+ levels[depth][name] = row[field].value;
92
112
  }
93
113
  }
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;
114
+ for (const [depth, level] of levels.entries()) {
115
+ const { uniqueId, start, end, strand } = level;
116
+ featuresById.set(uniqueId, {
117
+ ...level,
118
+ uniqueId: uniqueId,
119
+ refName,
120
+ start: Number.parseInt(start, 10),
121
+ end: Number.parseInt(end, 10),
122
+ strand: Number.parseInt(strand, 10) || 0,
123
+ });
124
+ if (depth > 0) {
125
+ parentById.set(uniqueId, levels[depth - 1].uniqueId);
98
126
  }
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
127
  }
110
128
  }
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
- }
129
+ const roots = [];
130
+ for (const [uniqueId, feature] of featuresById) {
131
+ const pid = parentById.get(uniqueId);
132
+ const parent = pid ? featuresById.get(pid) : undefined;
133
+ if (parent) {
134
+ parent.subfeatures ??= [];
135
+ parent.subfeatures.push(feature);
136
+ }
137
+ else {
138
+ if (pid) {
139
+ console.error(`Could not find parentID ${pid}`);
154
140
  }
141
+ roots.push(feature);
155
142
  }
156
143
  }
157
- return Object.keys(seenFeatures).map(seenFeature => new SimpleFeature({
158
- ...seenFeatures[seenFeature].data,
159
- uniqueId: seenFeature,
160
- subfeatures: seenFeatures[seenFeature].data.subfeatures,
161
- }));
144
+ return roots.map(data => new SimpleFeature(data));
162
145
  }
163
146
  async hasDataForRefName(refName, opts = {}) {
164
147
  const refNames = await this.getRefNames(opts);
@@ -1,31 +1,59 @@
1
- declare const _default: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
2
- endpoint: {
3
- type: string;
4
- defaultValue: {
5
- uri: string;
6
- locationType: string;
7
- };
8
- description: 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
+ type: {
28
+ type: "string";
29
+ } & {
30
+ defaultValue: string;
31
+ };
32
+ readonly endpoint: {
33
+ type: "fileLocation";
34
+ } & {
35
+ defaultValue: unknown;
9
36
  };
10
- queryTemplate: {
11
- type: string;
37
+ readonly queryTemplate: {
38
+ type: "text";
39
+ } & {
12
40
  defaultValue: string;
13
- description: string;
14
41
  };
15
- refNamesQueryTemplate: {
16
- type: string;
42
+ readonly refNamesQueryTemplate: {
43
+ type: "text";
44
+ } & {
17
45
  defaultValue: string;
18
- description: string;
19
46
  };
20
- refNames: {
21
- type: string;
22
- defaultValue: never[];
23
- description: string;
47
+ readonly refNames: {
48
+ type: "stringArray";
49
+ } & {
50
+ defaultValue: unknown;
24
51
  };
25
- additionalQueryParams: {
26
- type: string;
27
- defaultValue: never[];
28
- description: string;
52
+ readonly additionalQueryParams: {
53
+ type: "stringArray";
54
+ } & {
55
+ defaultValue: unknown;
29
56
  };
30
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
31
- export default _default;
57
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
58
+ export default SPARQLAdapterConfigSchema;
59
+ 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.3.0",
3
+ "version": "5.0.0-beta.10",
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.3",
24
+ "@jbrowse/mobx-state-tree": "^6.6.1",
25
+ "mobx": "^7.0.5",
26
26
  "rxjs": "^7.8.2",
27
- "string-template": "^1.0.0",
28
- "@jbrowse/core": "^4.3.0"
27
+ "@jbrowse/core": "^5.0.0-beta.10"
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
  }