@data-fair/dev-server 2.5.1 → 2.6.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/README.md CHANGED
@@ -51,12 +51,23 @@ balise `<meta name="application-name">` de l'`index.html` local et de la version
51
51
  du `package.json`.
52
52
 
53
53
  Une application de base réservée à une organisation (`privateAccess`) est
54
- invisible à une requête anonyme : sans elle la liste est vide, quand bien même
55
- les applications qui l'utilisent seraient publiques. Le paramètre
56
- `privateAccess` la rend visible, mais data-fair répond 401 sans
57
- authentification — il n'est donc envoyé que si `DATAFAIR_API_KEY` est
58
- renseignée. Une clé d'API se crée depuis le compte ou l'organisation sur le
59
- data-fair distant, et se met dans le `.env` de l'application :
54
+ invisible à une requête anonyme. Le paramètre `privateAccess` la rend visible,
55
+ mais data-fair répond 401 sans authentification — il n'est donc envoyé que si
56
+ `DATAFAIR_API_KEY` est renseignée.
57
+
58
+ Ne pas la trouver n'arrête pas la recherche pour autant : les applications qui
59
+ tournent dessus sont souvent publiques, et il suffit de connaître son URL pour
60
+ les lister. Les URL des applications de base publiées suivent deux conventions
61
+ (`<data-fair>/apps/<slug>/<mineure>/` et
62
+ `https://cdn.jsdelivr.net/npm/@scope/<paquet>@<mineure>/dist/`), et le filtre
63
+ de `/applications` est une égalité stricte : une URL devinée ne peut que ne
64
+ rien renvoyer, jamais renvoyer autre chose. Les candidates sont donc essayées
65
+ en une requête, ce qui suffit à lister les configurations publiques d'une
66
+ application de base privée, sans aucune clé.
67
+
68
+ Une clé reste nécessaire pour une application, elle, privée. Elle se crée
69
+ depuis le compte ou l'organisation sur le data-fair distant, et se met dans le
70
+ `.env` de l'application :
60
71
 
61
72
  ```
62
73
  DATAFAIR_API_KEY=xxx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/dev-server",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "A development server for optimal development experience of data-fair applications.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -2,7 +2,10 @@
2
2
  import config from './config.js';
3
3
  import uiConfig from './ui-config.js';
4
4
  import { localizeConfig } from './localize.js';
5
+ import { remoteFetch, remoteFetchBuffer } from './remote.js';
6
+ import { prepareConfig as prepareRemoteConfig } from './enrich.js';
5
7
  import { ATTACHMENTS_DIR, attachmentPath, copyAttachments, listAttachments } from './attachments.js';
8
+ import { candidateBaseAppUrls } from './base-app-urls.js';
6
9
  import { WebSocket, WebSocketServer } from 'ws';
7
10
  import { createServer } from 'node:http';
8
11
  import express from 'express';
@@ -150,13 +153,16 @@ const localAppInfo = async () => {
150
153
  debug('failed to fetch local app index.html', err);
151
154
  }
152
155
  let version;
156
+ let packageName;
153
157
  try {
154
- version = JSON.parse(readFileSync('package.json', 'utf8')).version;
158
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
159
+ version = pkg.version;
160
+ packageName = pkg.name;
155
161
  }
156
162
  catch (err) {
157
163
  debug('failed to read local package.json', err);
158
164
  }
159
- return { name, version };
165
+ return { name, version, packageName };
160
166
  };
161
167
  // The owner the dev-server works on behalf of, in the "type:id[:department]" form the
162
168
  // data-fair api expects for its owner and privateAccess filters.
@@ -167,76 +173,14 @@ const ownerFilter = () => {
167
173
  filter += ':' + owner.department;
168
174
  return filter;
169
175
  };
170
- // Fetch a resource from the remote data-fair api
171
- const remoteFetch = async (path) => {
172
- const res = await remoteFetchRaw(path);
173
- return res.json();
174
- };
175
- // Same, for an attachment: a file, never json, and never decoded as text — see the app proxy
176
- // below, where decoding a binary body as utf8 destroys it.
177
- const remoteFetchBuffer = async (path) => {
178
- const res = await remoteFetchRaw(path);
179
- return Buffer.from(await res.arrayBuffer());
180
- };
181
- const remoteFetchRaw = async (path) => {
182
- const res = await fetch(config.dataFair.url + '/api/v1' + path, {
183
- headers: config.dataFair.apiKey ? { 'x-apiKey': config.dataFair.apiKey } : {}
184
- });
185
- if (!res.ok)
186
- throw new Error(`error ${res.status} on remote data-fair: ${await res.text()}`);
187
- return res;
188
- };
189
- // Short-lived cache for the dataset enrichment below, so that every preview reload does not
190
- // hammer the remote data-fair API. Failures are cached too: a private dataset without an api
191
- // key would otherwise be re-fetched, and re-warned about, on every single reload.
192
- const datasetsCache = new Map();
193
- const DATASETS_CACHE_TTL = 30_000;
194
- // The exact set of properties data-fair injects in a configuration dataset entry. Sticking to
195
- // it matters: forwarding the whole remote dataset would let an application rely, in dev, on a
196
- // property that production never sends.
197
- const INJECTED_DATASET_PROPS = ['id', 'href', 'page', 'title', 'slug', 'finalizedAt', 'schema', 'userPermissions'];
198
- // Rebuild a configuration dataset entry the same way data-fair does in production
199
- // (refreshConfigDatasetsRefs in api/src/applications/utils.ts): the app stores only
200
- // minimal references, data-fair injects the full schema (with concepts), finalizedAt,
201
- // slug and userPermissions at request time. We reproduce that so applications
202
- // following the skill contract read window.APPLICATION.configuration.datasets
203
- // identically in dev and in prod.
204
- const enrichDataset = async (dataset) => {
205
- if (!dataset?.id)
206
- return dataset;
207
- const cached = datasetsCache.get(dataset.id);
208
- if (cached && Date.now() - cached.fetchedAt < DATASETS_CACHE_TTL) {
209
- return cached.data ? { ...dataset, ...cached.data } : dataset;
210
- }
211
- try {
212
- const fresh = await remoteFetch('/datasets/' + encodeURIComponent(dataset.id));
213
- const data = {};
214
- for (const prop of INJECTED_DATASET_PROPS) {
215
- if (fresh[prop] !== undefined)
216
- data[prop] = fresh[prop];
217
- }
218
- data.userPermissions = fresh.userPermissions ?? [];
219
- datasetsCache.set(dataset.id, { data, fetchedAt: Date.now() });
220
- return { ...dataset, ...data };
221
- }
222
- catch (err) {
223
- // a private dataset without an api key, or a network failure: keep the raw
224
- // configuration entry so the app still loads, and warn in the dev-server UI
225
- console.warn('[dev-server] failed to enrich dataset ' + dataset.id + ', keeping raw configuration entry', err);
226
- datasetsCache.set(dataset.id, { data: null, fetchedAt: Date.now() });
227
- return dataset;
228
- }
229
- };
230
- // Enrich the datasets from the remote data-fair, then rewrite every remote origin to ours.
231
- // The rewrite is applied even when there is no dataset to enrich: a configuration can carry
232
- // remote urls anywhere (logos, links, tileserver styles), not only in datasets[].href.
233
- const prepareConfig = async (configuration) => {
234
- const datasets = configuration?.datasets?.filter((d) => !!d);
235
- const enriched = datasets?.length
236
- ? { ...configuration, datasets: await Promise.all(datasets.map(enrichDataset)) }
237
- : configuration;
238
- return localizeConfig(enriched, new URL(config.dataFair.url).origin, `http://localhost:${config.port}`);
239
- };
176
+ // Enrich the datasets from the remote data-fair and rewrite every remote origin to ours
177
+ // (pure helpers in enrich.ts, wired here to our remote api and local origins).
178
+ const prepareConfig = (configuration) => prepareRemoteConfig(configuration, {
179
+ fetchJson: remoteFetch,
180
+ localize: localizeConfig,
181
+ remoteOrigin: new URL(config.dataFair.url).origin,
182
+ localOrigin: `http://localhost:${config.port}`
183
+ });
240
184
  // read the .dev-config.json file of the app under development
241
185
  const readDevConfig = () => existsSync('.dev-config.json') ? JSON.parse(readFileSync('.dev-config.json', 'utf8')) : {};
242
186
  // Reproduce the waiting strategy of the capture service (capture/api/utils/page.ts) so that a
@@ -368,7 +312,7 @@ const minorVersion = (version) => version.split('.').slice(0, 2).join('.');
368
312
  // list configurations of the app under development that exist on the remote data-fair
369
313
  // with the same minor version, useful to test quickly for regressions
370
314
  app.get('/configurations', async (req, res) => {
371
- const { name, version } = await localAppInfo();
315
+ const { name, version, packageName } = await localAppInfo();
372
316
  const response = { appName: name, localVersion: version, remoteUrl: config.dataFair.url };
373
317
  if (!name) {
374
318
  res.status(400).send({ ...response, error: 'The "application-name" meta tag was not found in the local app index.html' });
@@ -379,10 +323,9 @@ app.get('/configurations', async (req, res) => {
379
323
  return;
380
324
  }
381
325
  response.minorVersion = minorVersion(version);
382
- // a base application restricted to an organization is invisible to an anonymous request, and
383
- // without it the listing below finds nothing at all — the applications are then never reached,
384
- // however public they are. privateAccess is what makes it visible, and data-fair answers 401
385
- // to it without credentials, so it is only sent when an api key is configured.
326
+ // a base application restricted to an organization is invisible to an anonymous request.
327
+ // privateAccess is what makes it visible, and data-fair answers 401 to it without
328
+ // credentials, so it is only sent when an api key is configured.
386
329
  response.authenticated = !!config.dataFair.apiKey;
387
330
  try {
388
331
  let baseAppsQuery = '/base-applications?applicationName=' + encodeURIComponent(name) + '&size=1000&count=false';
@@ -390,16 +333,21 @@ app.get('/configurations', async (req, res) => {
390
333
  baseAppsQuery += '&privateAccess=' + encodeURIComponent(ownerFilter());
391
334
  const baseApps = await remoteFetch(baseAppsQuery);
392
335
  const matchingBaseApps = (baseApps.results ?? []).filter((b) => typeof b.version === 'string' && minorVersion(b.version) === response.minorVersion);
393
- // told apart from "the base application exists but carries no application": the UI has no
394
- // other way to explain an empty list, and a missing api key is by far the likeliest cause
395
- response.baseAppFound = matchingBaseApps.length > 0;
396
- response.results = [];
397
- for (const baseApp of matchingBaseApps) {
398
- const applications = await remoteFetch('/applications?base-application=' + encodeURIComponent(baseApp.url) + '&size=1000&count=false&select=id,title,owner');
399
- for (const application of applications.results ?? []) {
400
- response.results.push({ id: application.id, title: application.title, owner: application.owner, baseAppVersion: baseApp.version });
401
- }
402
- }
336
+ // Not finding the base application is not the end of the search: it may simply be private
337
+ // while the applications running on it are public, and those are reachable as soon as its
338
+ // url is known. Guessing the url costs nothing — the filter below is an exact match.
339
+ const versionByUrl = new Map(matchingBaseApps.map((b) => [b.url, b.version]));
340
+ const baseAppUrls = matchingBaseApps.length
341
+ ? [...versionByUrl.keys()]
342
+ : candidateBaseAppUrls(config.dataFair.url, name, packageName, response.minorVersion);
343
+ // one request for the whole set: the data-fair list filters accept comma separated values
344
+ const applications = await remoteFetch('/applications?base-application=' + encodeURIComponent(baseAppUrls.join(',')) + '&size=1000&count=false&select=id,title,owner,url');
345
+ response.results = (applications.results ?? []).map((application) => ({
346
+ id: application.id,
347
+ title: application.title,
348
+ owner: application.owner,
349
+ baseAppVersion: versionByUrl.get(application.url) ?? response.minorVersion
350
+ }));
403
351
  response.results.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
404
352
  res.send(response);
405
353
  }
@@ -412,13 +360,16 @@ app.get('/configurations', async (req, res) => {
412
360
  app.get('/configurations/:id', async (req, res) => {
413
361
  const id = encodeURIComponent(req.params.id);
414
362
  try {
415
- // Sent as-is, with its remote origins: the UI stores this straight into .dev-config.json,
416
- // which must stay portable. Origins are rewritten on the read path, in prepareConfig.
417
- const configuration = await remoteFetch('/applications/' + id + '/configuration');
418
- // The attachments come along: a configuration references them by name only, so without the
419
- // files a copied production configuration renders with every image broken. Fetched from the
420
- // application itself rather than from the configuration, which never lists them.
421
- const application = await remoteFetch('/applications/' + id + '?select=attachments');
363
+ // The configuration is sent as-is, with its remote origins: the UI stores it straight into
364
+ // .dev-config.json, which must stay portable. Origins are rewritten on the read path, in
365
+ // prepareConfig. The attachments come along, since a configuration references them by name
366
+ // only and without the files a copied production configuration renders with every image
367
+ // broken; they are read from the application, which is where data-fair lists them.
368
+ // Neither read depends on the other, so they leave together instead of one after the other.
369
+ const [configuration, application] = await Promise.all([
370
+ remoteFetch('/applications/' + id + '/configuration'),
371
+ remoteFetch('/applications/' + id + '?select=attachments')
372
+ ]);
422
373
  const attachments = await copyAttachments(application.attachments ?? [], (name) => remoteFetchBuffer('/applications/' + id + '/attachments/' + encodeURIComponent(name)));
423
374
  if (attachments.failed.length) {
424
375
  console.warn('[dev-server] failed to copy attachments ' + attachments.failed.join(', ') + ', images referencing them will be broken');
@@ -82,6 +82,11 @@ export const attachmentPath = (dir, name) => {
82
82
  return undefined;
83
83
  return filePath;
84
84
  };
85
+ // Attachments used to be downloaded one after the other, each waiting for a full round trip
86
+ // to the remote data-fair before the next one started, so an application carrying a dozen
87
+ // images cost a dozen round trips to copy. They go in parallel now, bounded so that a
88
+ // configuration with many attachments does not open as many connections at once.
89
+ const DOWNLOAD_CONCURRENCY = 5;
85
90
  // Replace the whole local attachments directory with the attachments of a remote application.
86
91
  // Replace, and not merge: copying a configuration replaces the current one entirely, so leaving
87
92
  // behind the images of the previously copied application would only produce a directory whose
@@ -93,32 +98,43 @@ export const attachmentPath = (dir, name) => {
93
98
  // where nothing downloads does empty the directory, which is the point: the configuration it
94
99
  // came with is being applied all the same, and its images are genuinely missing.
95
100
  export const copyAttachments = async (attachments, fetchAttachment, dir = ATTACHMENTS_DIR) => {
96
- const result = { copied: [], failed: [] };
97
- const downloaded = [];
98
- for (const attachment of attachments ?? []) {
99
- // a name with a path separator would write outside the directory, and data-fair has no
100
- // reason to send one: skip it rather than sanitize a name the configuration still refers to
101
- if (!attachment?.name || attachmentPath(dir, attachment.name) === undefined) {
102
- if (attachment?.name)
103
- result.failed.push(attachment.name);
104
- continue;
101
+ const entries = attachments ?? [];
102
+ // indexed rather than appended: downloads no longer finish in the order they started, and
103
+ // both lists below are read by a developer against the configuration they came from
104
+ const downloaded = new Array(entries.length);
105
+ const failures = new Array(entries.length);
106
+ let next = 0;
107
+ const worker = async () => {
108
+ while (next < entries.length) {
109
+ const index = next++;
110
+ const attachment = entries[index];
111
+ // a name with a path separator would write outside the directory, and data-fair has no
112
+ // reason to send one: skip it rather than sanitize a name the configuration still refers to
113
+ if (!attachment?.name || attachmentPath(dir, attachment.name) === undefined) {
114
+ if (attachment?.name)
115
+ failures[index] = attachment.name;
116
+ continue;
117
+ }
118
+ try {
119
+ downloaded[index] = { attachment: { ...attachment, name: attachment.name }, body: await fetchAttachment(attachment.name) };
120
+ }
121
+ catch (err) {
122
+ failures[index] = attachment.name;
123
+ }
105
124
  }
106
- try {
107
- downloaded.push({ attachment: { ...attachment, name: attachment.name }, body: await fetchAttachment(attachment.name) });
108
- }
109
- catch (err) {
110
- result.failed.push(attachment.name);
111
- }
112
- }
125
+ };
126
+ await Promise.all(Array.from({ length: Math.min(DOWNLOAD_CONCURRENCY, entries.length) }, worker));
127
+ const kept = downloaded.filter(entry => entry !== undefined);
128
+ const result = { copied: [], failed: failures.filter(name => name !== undefined) };
113
129
  // written aside then swapped in, so an interrupted copy leaves the previous directory intact
114
130
  const tmpDir = dir + '.tmp';
115
131
  rmSync(tmpDir, { recursive: true, force: true });
116
132
  mkdirSync(tmpDir, { recursive: true });
117
- for (const { attachment, body } of downloaded) {
133
+ for (const { attachment, body } of kept) {
118
134
  writeFileSync(join(tmpDir, attachment.name), body);
119
135
  result.copied.push(attachment.name);
120
136
  }
121
- writeFileSync(join(tmpDir, METADATA_FILE), JSON.stringify(downloaded.map(d => d.attachment), null, 2));
137
+ writeFileSync(join(tmpDir, METADATA_FILE), JSON.stringify(kept.map(d => d.attachment), null, 2));
122
138
  rmSync(dir, { recursive: true, force: true });
123
139
  renameSync(tmpDir, dir);
124
140
  return result;
@@ -0,0 +1 @@
1
+ export declare const candidateBaseAppUrls: (dataFairUrl: string, appName: string, packageName: string | undefined, minorVersion: string) => string[];
@@ -0,0 +1,37 @@
1
+ // Candidate urls of the base application under development.
2
+ //
3
+ // Discovering it by name is the authoritative path, but it only works for a base application
4
+ // the request is allowed to see: one restricted to an organization is invisible to an anonymous
5
+ // request, while the applications running on it are often public — which is precisely the case
6
+ // of an application developed for a single customer. The /applications listing filters on an
7
+ // exact url, so a wrong candidate returns nothing and a right one returns the applications: a
8
+ // guess can never produce a false match, and they all fit in a single request.
9
+ //
10
+ // The two conventions published base applications follow, as of today:
11
+ // https://koumoul.com/apps/<slug>/<minor>/
12
+ // https://cdn.jsdelivr.net/npm/@scope/<package>@<minor>/dist/
13
+ // The slug is the repository name, which is not always the application name — "Liste et fiches"
14
+ // is served under /apps/list-details/ — so both are tried, along with the package name stripped
15
+ // of the prefixes repositories use.
16
+ const slugs = (appName, packageName) => {
17
+ const candidates = [appName];
18
+ if (packageName) {
19
+ const unscoped = packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
20
+ candidates.push(unscoped, unscoped.replace(/^(app|data-fair)-/, ''));
21
+ }
22
+ return [...new Set(candidates.filter(Boolean))];
23
+ };
24
+ export const candidateBaseAppUrls = (dataFairUrl, appName, packageName, minorVersion) => {
25
+ const urls = [];
26
+ // the same origin as the data-fair the dev-server talks to: a base application is served
27
+ // beside it, never on some unrelated host
28
+ const origin = new URL(dataFairUrl).origin;
29
+ for (const slug of slugs(appName, packageName)) {
30
+ urls.push(`${origin}/apps/${slug}/${minorVersion}/`);
31
+ }
32
+ if (packageName) {
33
+ const scoped = packageName.startsWith('@') ? packageName : '@data-fair/' + packageName;
34
+ urls.push(`https://cdn.jsdelivr.net/npm/${scoped}@${minorVersion}/dist/`);
35
+ }
36
+ return urls;
37
+ };
@@ -0,0 +1,9 @@
1
+ export declare const INJECTED_DATASET_PROPS: readonly ["id", "href", "page", "title", "slug", "finalizedAt", "schema", "isRest", "userPermissions"];
2
+ export declare const enrichDataset: (dataset: any, fetchJson: (path: string) => Promise<any>) => Promise<any>;
3
+ export interface PrepareConfigDeps {
4
+ fetchJson: (path: string) => Promise<any>;
5
+ localize: (configuration: any, remoteOrigin: string, localOrigin: string) => any;
6
+ remoteOrigin: string;
7
+ localOrigin: string;
8
+ }
9
+ export declare const prepareConfig: (configuration: any, deps: PrepareConfigDeps) => Promise<any>;
package/src/enrich.js ADDED
@@ -0,0 +1,57 @@
1
+ // Rebuild the configuration dataset entries the same way data-fair does in production
2
+ // (refreshConfigDatasetsRefs in api/src/applications/utils.ts): an application stores only
3
+ // minimal dataset references, data-fair injects the full schema (with concepts), finalizedAt,
4
+ // slug, isRest and the per-request userPermissions at serving time. We reproduce that so
5
+ // applications following the skill contract read window.APPLICATION.configuration.datasets
6
+ // identically in dev and in prod.
7
+ //
8
+ // Pure module, no imports: it takes its remote accessor and origin rewrite as dependencies,
9
+ // wired in app.ts — so it can be unit tested without a build (test/enrich.test.ts).
10
+ // Short-lived cache for the dataset enrichment below, so that every preview reload does not
11
+ // hammer the remote data-fair API. Failures are cached too: a private dataset without an api
12
+ // key would otherwise be re-fetched, and re-warned about, on every single reload.
13
+ const datasetsCache = new Map();
14
+ const DATASETS_CACHE_TTL = 30_000;
15
+ // The set of properties data-fair injects in a configuration dataset entry. Sticking to it
16
+ // matters: forwarding the whole remote dataset would let an application rely, in dev, on a
17
+ // property that production never sends. data-fair refreshes the keys stored in the entry plus
18
+ // the `select` of the app's config schema dataset selector — this list covers that contract,
19
+ // including `isRest`, carried by the select of editing apps and used as the gate deciding
20
+ // whether rest line editing (POST/PATCH/DELETE /lines) is offered at all.
21
+ export const INJECTED_DATASET_PROPS = ['id', 'href', 'page', 'title', 'slug', 'finalizedAt', 'schema', 'isRest', 'userPermissions'];
22
+ export const enrichDataset = async (dataset, fetchJson) => {
23
+ if (!dataset?.id)
24
+ return dataset;
25
+ const cached = datasetsCache.get(dataset.id);
26
+ if (cached && Date.now() - cached.fetchedAt < DATASETS_CACHE_TTL) {
27
+ return cached.data ? { ...dataset, ...cached.data } : dataset;
28
+ }
29
+ try {
30
+ const fresh = await fetchJson('/datasets/' + encodeURIComponent(dataset.id));
31
+ const data = {};
32
+ for (const prop of INJECTED_DATASET_PROPS) {
33
+ if (fresh[prop] !== undefined)
34
+ data[prop] = fresh[prop];
35
+ }
36
+ data.userPermissions = fresh.userPermissions ?? [];
37
+ datasetsCache.set(dataset.id, { data, fetchedAt: Date.now() });
38
+ return { ...dataset, ...data };
39
+ }
40
+ catch (err) {
41
+ // a private dataset without an api key, or a network failure: keep the raw
42
+ // configuration entry so the app still loads, and warn in the dev-server UI
43
+ console.warn('[dev-server] failed to enrich dataset ' + dataset.id + ', keeping raw configuration entry', err);
44
+ datasetsCache.set(dataset.id, { data: null, fetchedAt: Date.now() });
45
+ return dataset;
46
+ }
47
+ };
48
+ // Enrich the datasets from the remote data-fair, then rewrite every remote origin to ours.
49
+ // The rewrite is applied even when there is no dataset to enrich: a configuration can carry
50
+ // remote urls anywhere (logos, links, tileserver styles), not only in datasets[].href.
51
+ export const prepareConfig = async (configuration, deps) => {
52
+ const datasets = configuration?.datasets?.filter((d) => !!d);
53
+ const enriched = datasets?.length
54
+ ? { ...configuration, datasets: await Promise.all(datasets.map(d => enrichDataset(d, deps.fetchJson))) }
55
+ : configuration;
56
+ return deps.localize(enriched, deps.remoteOrigin, deps.localOrigin);
57
+ };
@@ -0,0 +1,3 @@
1
+ export declare const remoteFetchRaw: (path: string) => Promise<Response>;
2
+ export declare const remoteFetch: (path: string) => Promise<any>;
3
+ export declare const remoteFetchBuffer: (path: string) => Promise<Buffer<ArrayBuffer>>;
package/src/remote.js ADDED
@@ -0,0 +1,22 @@
1
+ // Access to the remote data-fair api. Only JSON and buffer reads: every mutation goes
2
+ // through data-fair itself, the dev-server never writes on behalf of the developer.
3
+ import config from './config.js';
4
+ // Fetch a resource from the remote data-fair api
5
+ export const remoteFetchRaw = async (path) => {
6
+ const res = await fetch(config.dataFair.url + '/api/v1' + path, {
7
+ headers: config.dataFair.apiKey ? { 'x-apiKey': config.dataFair.apiKey } : {}
8
+ });
9
+ if (!res.ok)
10
+ throw new Error(`error ${res.status} on remote data-fair: ${await res.text()}`);
11
+ return res;
12
+ };
13
+ export const remoteFetch = async (path) => {
14
+ const res = await remoteFetchRaw(path);
15
+ return res.json();
16
+ };
17
+ // Same, for an attachment: a file, never json, and never decoded as text — see the app proxy
18
+ // below, where decoding a binary body as utf8 destroys it.
19
+ export const remoteFetchBuffer = async (path) => {
20
+ const res = await remoteFetchRaw(path);
21
+ return Buffer.from(await res.arrayBuffer());
22
+ };