@_linked/server 2.25.0 → 3.1.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/CHANGELOG.md +21 -0
- package/lib/esm/backend.d.ts +8 -0
- package/lib/esm/backend.js +30 -20
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/index.d.ts +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/shapes/LincdAPI.js +5 -5
- package/lib/esm/shapes/LincdAPI.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +100 -0
- package/lib/esm/shapes/quadstores/RemoteDataset.js +188 -0
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -0
- package/lib/esm/utils/queryPlane.d.ts +32 -6
- package/lib/esm/utils/queryPlane.js +53 -13
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +90 -48
- package/lib/esm/shapes/quadstores/BackendAPIStore.d.ts +0 -58
- package/lib/esm/shapes/quadstores/BackendAPIStore.js +0 -71
- package/lib/esm/shapes/quadstores/BackendAPIStore.js.map +0 -1
- package/lib/esm/shapes/quadstores/GatewayStore.d.ts +0 -85
- package/lib/esm/shapes/quadstores/GatewayStore.js +0 -143
- package/lib/esm/shapes/quadstores/GatewayStore.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#181](https://github.com/linked-fw/server/pull/181) [`059dd58`](https://github.com/linked-fw/server/commit/059dd584bc95efac30ba5e9831b69a9931236690) Thanks [@flyon](https://github.com/flyon)! - Query-plane shape catalog hook: `setQueryCatalogResolver(resolver)` in `@_linked/server/utils/queryPlane` lets the host supply the `ShapeCatalog` each generic query request (`/call/@_linked/server/*Query`, `/api/*`) is decoded in. The access check and the store lower the query in that catalog. A resolver that throws a `ServerCallError` answers with its status; any other throw answers 500 `Shape catalog unavailable`. Without a resolver, behaviour is unchanged. Requires `@_linked/core` 2.35.0.
|
|
8
|
+
|
|
9
|
+
## 3.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- [#179](https://github.com/linked-fw/server/pull/179) [`0a032fb`](https://github.com/linked-fw/server/commit/0a032fbc830fd001ccb27330072090019259c206) Thanks [@flyon](https://github.com/flyon)! - `BackendAPIStore` is renamed `RemoteDataset` and gains a `url`/`token` form for a remote data server; `GatewayStore` is removed; new public `ping` method.
|
|
14
|
+
|
|
15
|
+
Migration:
|
|
16
|
+
|
|
17
|
+
- **`BackendAPIStore` users:** replace the import path `@_linked/server/shapes/quadstores/BackendAPIStore` with `@_linked/server/shapes/quadstores/RemoteDataset` and the class name `BackendAPIStore` with `RemoteDataset` (in code and in the `store` of `linked.{frontend,backend}.datasets.json`). Without `url` the behaviour is unchanged; there is no alias.
|
|
18
|
+
- **`GatewayStore` users:** point `RemoteDataset` at the server's `/call` base instead: config `{ "url": "<base>", "token": "<bearer token>" }` (was `{ endpoint, credential }`). It posts `{"args": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with `Authorization: Bearer <token>`; the `{v, op, query}` envelope, `/query` and `/health` routes and the `fetch` config override are gone.
|
|
19
|
+
- **Config:** `RemoteDatasetConfig` is `{ url?, token?, name?, id? }`. A `url` that is present but empty never falls back to the no-url form.
|
|
20
|
+
- **`init()`:** with `url`, it posts to `{url}/call/@_linked/server/ping` (memoized on success) and throws a typed error. Await it before registering the store.
|
|
21
|
+
- **Errors:** `GatewayStoreError` / `GatewayUnreachableError` are replaced by `RemoteDatasetError` (`url`, `status`, optional `code`) and its subclasses `RemoteDatasetUnreachableError` (no answer, empty url, or nothing at the url speaks the wire) and `RemoteDatasetUnauthorizedError` (401 or 403).
|
|
22
|
+
- **`ping`:** `POST /call/@_linked/server/ping` is a new `public` method that answers `{ "ok": true }`.
|
|
23
|
+
|
|
3
24
|
## 2.25.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/lib/esm/backend.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
|
|
2
2
|
export default class LincdServerBackendProvider extends BackendProvider {
|
|
3
3
|
setupBeforeControllers(): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Reachability check for a `RemoteDataset` pointed at this server: reads nothing, changes
|
|
6
|
+
* nothing. A plain linked server has no token concept, so it always answers `{ok: true}`;
|
|
7
|
+
* a host that serves the wire with tokens answers 401 for a refused one.
|
|
8
|
+
*/
|
|
9
|
+
ping(): Promise<{
|
|
10
|
+
ok: true;
|
|
11
|
+
}>;
|
|
4
12
|
selectQuery(json: any): Promise<unknown>;
|
|
5
13
|
askQuery(json: any): Promise<boolean>;
|
|
6
14
|
updateQuery(json: any): Promise<unknown>;
|
package/lib/esm/backend.js
CHANGED
|
@@ -12,7 +12,7 @@ import { callable } from '@_linked/server-utils/utils/callable';
|
|
|
12
12
|
import { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';
|
|
13
13
|
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
14
14
|
import { LocalFileStore } from './shapes/filestores/LocalFileStore.js';
|
|
15
|
-
import { authorizeGenericQuery, toQueryBuilder } from './utils/queryPlane.js';
|
|
15
|
+
import { authorizeGenericQuery, resolveQueryCatalog, toQueryBuilder, } from './utils/queryPlane.js';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import fs from 'fs/promises';
|
|
18
18
|
export default class LincdServerBackendProvider extends BackendProvider {
|
|
@@ -30,22 +30,26 @@ export default class LincdServerBackendProvider extends BackendProvider {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Reachability check for a `RemoteDataset` pointed at this server: reads nothing, changes
|
|
35
|
+
* nothing. A plain linked server has no token concept, so it always answers `{ok: true}`;
|
|
36
|
+
* a host that serves the wire with tokens answers 401 for a refused one.
|
|
37
|
+
*/
|
|
38
|
+
async ping() {
|
|
39
|
+
return { ok: true };
|
|
40
|
+
}
|
|
41
|
+
// ── Generic query execution ──────────────────────────────────────────────
|
|
38
42
|
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// the
|
|
42
|
-
//
|
|
43
|
-
// to discard it. Addressing by package removes all of that.
|
|
43
|
+
// These answer `RemoteDataset`: a browser's store calls
|
|
44
|
+
// `Server.call(packageName, {method}, json)` (the PACKAGE form), and a remote app's store
|
|
45
|
+
// posts the same `{args}` body to `{url}/call/@_linked/server/<method>`. Either way the
|
|
46
|
+
// request arrives here on the generic provider rather than through Shape resolution.
|
|
44
47
|
//
|
|
45
|
-
// A query cannot cross the wire live
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
48
|
+
// A query cannot cross the wire live, so the client ships `toJSON()` and `fromJSON()`
|
|
49
|
+
// rehydrates it here into a live, closed query. `LinkedStorage` then routes it by the
|
|
50
|
+
// QUERY's shape, which is what decides the target dataset; the store instance never
|
|
51
|
+
// participates. The host's catalog resolver (`setQueryCatalogResolver`) picks the shape
|
|
52
|
+
// catalog the JSON is decoded in; the builder keeps it for both lowerings.
|
|
49
53
|
//
|
|
50
54
|
// Every query is checked first against the access rules of the stores it
|
|
51
55
|
// maps to, declared in the app's storage config (`withAccess`; a store with
|
|
@@ -54,31 +58,37 @@ export default class LincdServerBackendProvider extends BackendProvider {
|
|
|
54
58
|
// 'public' because that check, not the RPC layer, decides whether a session
|
|
55
59
|
// is required.
|
|
56
60
|
async selectQuery(json) {
|
|
57
|
-
const query = toQueryBuilder(json);
|
|
61
|
+
const query = toQueryBuilder(json, await resolveQueryCatalog(json));
|
|
58
62
|
await authorizeGenericQuery('select', json, query, 'selectQuery');
|
|
59
63
|
return LinkedStorage.selectQuery(query);
|
|
60
64
|
}
|
|
61
65
|
async askQuery(json) {
|
|
62
|
-
const query = toQueryBuilder(json);
|
|
66
|
+
const query = toQueryBuilder(json, await resolveQueryCatalog(json));
|
|
63
67
|
await authorizeGenericQuery('ask', json, query, 'askQuery');
|
|
64
68
|
return LinkedStorage.askQuery(query);
|
|
65
69
|
}
|
|
66
70
|
async updateQuery(json) {
|
|
67
|
-
const query = toQueryBuilder(json);
|
|
71
|
+
const query = toQueryBuilder(json, await resolveQueryCatalog(json));
|
|
68
72
|
await authorizeGenericQuery('update', json, query, 'updateQuery');
|
|
69
73
|
return LinkedStorage.updateQuery(query);
|
|
70
74
|
}
|
|
71
75
|
async createQuery(json) {
|
|
72
|
-
const query = toQueryBuilder(json);
|
|
76
|
+
const query = toQueryBuilder(json, await resolveQueryCatalog(json));
|
|
73
77
|
await authorizeGenericQuery('create', json, query, 'createQuery');
|
|
74
78
|
return LinkedStorage.createQuery(query);
|
|
75
79
|
}
|
|
76
80
|
async deleteQuery(json) {
|
|
77
|
-
const query = toQueryBuilder(json);
|
|
81
|
+
const query = toQueryBuilder(json, await resolveQueryCatalog(json));
|
|
78
82
|
await authorizeGenericQuery('delete', json, query, 'deleteQuery');
|
|
79
83
|
return LinkedStorage.deleteQuery(query);
|
|
80
84
|
}
|
|
81
85
|
}
|
|
86
|
+
__decorate([
|
|
87
|
+
callable('public'),
|
|
88
|
+
__metadata("design:type", Function),
|
|
89
|
+
__metadata("design:paramtypes", []),
|
|
90
|
+
__metadata("design:returntype", Promise)
|
|
91
|
+
], LincdServerBackendProvider.prototype, "ping", null);
|
|
82
92
|
__decorate([
|
|
83
93
|
callable('public'),
|
|
84
94
|
__metadata("design:type", Function),
|
package/lib/esm/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,MAAM,sCAAsC,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { callable } from '@_linked/server-utils/utils/callable';\nimport { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { LocalFileStore } from './shapes/filestores/LocalFileStore.js';\nimport {\n authorizeGenericQuery,\n resolveQueryCatalog,\n toQueryBuilder,\n} from './utils/queryPlane.js';\nimport path from 'path';\nimport fs from 'fs/promises';\n\nexport default class LincdServerBackendProvider extends BackendProvider {\n async setupBeforeControllers() {\n if (!LinkedFileStorage.getDefaultStore()) {\n LinkedFileStorage.setDefaultStore(\n new LocalFileStore(process.env.NODE_ENV + '-filestore')\n );\n }\n let fileSystemUploadPath = path.join('data', 'filestores');\n try {\n await fs.access(fileSystemUploadPath);\n } catch (err) {\n await fs.mkdir(path.join(fileSystemUploadPath, 'filestores'), {\n recursive: true,\n });\n }\n }\n\n /**\n * Reachability check for a `RemoteDataset` pointed at this server: reads nothing, changes\n * nothing. A plain linked server has no token concept, so it always answers `{ok: true}`;\n * a host that serves the wire with tokens answers 401 for a refused one.\n */\n @callable('public')\n async ping(): Promise<{ ok: true }> {\n return { ok: true };\n }\n\n // ── Generic query execution ──────────────────────────────────────────────\n //\n // These answer `RemoteDataset`: a browser's store calls\n // `Server.call(packageName, {method}, json)` (the PACKAGE form), and a remote app's store\n // posts the same `{args}` body to `{url}/call/@_linked/server/<method>`. Either way the\n // request arrives here on the generic provider rather than through Shape resolution.\n //\n // A query cannot cross the wire live, so the client ships `toJSON()` and `fromJSON()`\n // rehydrates it here into a live, closed query. `LinkedStorage` then routes it by the\n // QUERY's shape, which is what decides the target dataset; the store instance never\n // participates. The host's catalog resolver (`setQueryCatalogResolver`) picks the shape\n // catalog the JSON is decoded in; the builder keeps it for both lowerings.\n //\n // Every query is checked first against the access rules of the stores it\n // maps to, declared in the app's storage config (`withAccess`; a store with\n // no rule requires a session): see utils/queryPlane. INTERIM model, to be\n // rethought. The store runs the builder that was checked. They are declared\n // 'public' because that check, not the RPC layer, decides whether a session\n // is required.\n\n @callable('public')\n async selectQuery(json: any) {\n const query = toQueryBuilder(json, await resolveQueryCatalog(json));\n await authorizeGenericQuery('select', json, query, 'selectQuery');\n return LinkedStorage.selectQuery(query);\n }\n\n @callable('public')\n async askQuery(json: any) {\n const query = toQueryBuilder(json, await resolveQueryCatalog(json));\n await authorizeGenericQuery('ask', json, query, 'askQuery');\n return LinkedStorage.askQuery(query);\n }\n\n @callable('public')\n async updateQuery(json: any) {\n const query = toQueryBuilder(json, await resolveQueryCatalog(json));\n await authorizeGenericQuery('update', json, query, 'updateQuery');\n return LinkedStorage.updateQuery(query);\n }\n\n @callable('public')\n async createQuery(json: any) {\n const query = toQueryBuilder(json, await resolveQueryCatalog(json));\n await authorizeGenericQuery('create', json, query, 'createQuery');\n return LinkedStorage.createQuery(query);\n }\n\n @callable('public')\n async deleteQuery(json: any) {\n const query = toQueryBuilder(json, await resolveQueryCatalog(json));\n await authorizeGenericQuery('delete', json, query, 'deleteQuery');\n return LinkedStorage.deleteQuery(query);\n }\n}\n"]}
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import './types.js';
|
|
2
2
|
import './ontologies/server.register.js';
|
|
3
3
|
import './shapes/index.js';
|
|
4
|
-
import './shapes/quadstores/
|
|
4
|
+
import './shapes/quadstores/RemoteDataset.js';
|
|
5
5
|
import './shapes/filestores/LocalFileStore.js';
|
|
6
6
|
import './utils/accessUrl.js';
|
|
7
7
|
export { FatalStartupError, isFatalError } from './utils/fatalError.js';
|
package/lib/esm/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import './ontologies/server.register.js';
|
|
|
3
3
|
//SHAPES FIRST
|
|
4
4
|
import './shapes/index.js';
|
|
5
5
|
// Not shapes (plain store classes); kept so the entry's module graph is unchanged.
|
|
6
|
-
import './shapes/quadstores/
|
|
6
|
+
import './shapes/quadstores/RemoteDataset.js';
|
|
7
7
|
import './shapes/filestores/LocalFileStore.js';
|
|
8
8
|
//THEN COMPONENTS
|
|
9
9
|
import './utils/accessUrl.js';
|
package/lib/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AACpB,OAAO,iCAAiC,CAAC;AAEzC,cAAc;AACd,OAAO,mBAAmB,CAAC;AAC3B,mFAAmF;AACnF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AACpB,OAAO,iCAAiC,CAAC;AAEzC,cAAc;AACd,OAAO,mBAAmB,CAAC;AAC3B,mFAAmF;AACnF,OAAO,sCAAsC,CAAC;AAC9C,OAAO,uCAAuC,CAAC;AAE/C,iBAAiB;AACjB,OAAO,sBAAsB,CAAC;AAE9B,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["import './types.js';\nimport './ontologies/server.register.js';\n\n//SHAPES FIRST\nimport './shapes/index.js';\n// Not shapes (plain store classes); kept so the entry's module graph is unchanged.\nimport './shapes/quadstores/RemoteDataset.js';\nimport './shapes/filestores/LocalFileStore.js';\n\n//THEN COMPONENTS\nimport './utils/accessUrl.js';\n\n// The fatal-error contract a provider uses to refuse to let the server start.\nexport { FatalStartupError, isFatalError } from './utils/fatalError.js';\nexport type { FatalError } from './utils/fatalError.js';\n\n//TYPES — re-exported from @_linked/server-utils (canonical location)\nexport type {\n RouteConfig,\n RoutesConfig,\n RoutesModule,\n} from '@_linked/server-utils/types/RouteConfig';\n"]}
|
|
@@ -10,7 +10,7 @@ import { server } from '../ontologies/server.js';
|
|
|
10
10
|
import { JSONParser } from '@_linked/server-utils/utils/JSONParser';
|
|
11
11
|
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
12
12
|
import { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';
|
|
13
|
-
import { authorizeGenericQuery, authorizeRawQuery, toQueryBuilder, } from '../utils/queryPlane.js';
|
|
13
|
+
import { authorizeGenericQuery, authorizeRawQuery, resolveQueryCatalog, toQueryBuilder, } from '../utils/queryPlane.js';
|
|
14
14
|
import { getRpcExposureMode } from '../utils/rpcExposure.js';
|
|
15
15
|
/** An error carrying an HTTP status, such as a `ServerCallError` (401/403). */
|
|
16
16
|
function isStatusError(error) {
|
|
@@ -56,22 +56,22 @@ let LincdAPI = class LincdAPI extends Shape {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
async post_select({ query }) {
|
|
59
|
-
const builder = toQueryBuilder(query);
|
|
59
|
+
const builder = toQueryBuilder(query, await resolveQueryCatalog(query));
|
|
60
60
|
await authorizeGenericQuery('select', query, builder, 'api/select');
|
|
61
61
|
return LinkedStorage.selectQuery(builder);
|
|
62
62
|
}
|
|
63
63
|
async post_create({ query }) {
|
|
64
|
-
const builder = toQueryBuilder(query);
|
|
64
|
+
const builder = toQueryBuilder(query, await resolveQueryCatalog(query));
|
|
65
65
|
await authorizeGenericQuery('create', query, builder, 'api/create');
|
|
66
66
|
return LinkedStorage.createQuery(builder);
|
|
67
67
|
}
|
|
68
68
|
async post_update({ query }) {
|
|
69
|
-
const builder = toQueryBuilder(query);
|
|
69
|
+
const builder = toQueryBuilder(query, await resolveQueryCatalog(query));
|
|
70
70
|
await authorizeGenericQuery('update', query, builder, 'api/update');
|
|
71
71
|
return LinkedStorage.updateQuery(builder);
|
|
72
72
|
}
|
|
73
73
|
async post_delete({ query }) {
|
|
74
|
-
const builder = toQueryBuilder(query);
|
|
74
|
+
const builder = toQueryBuilder(query, await resolveQueryCatalog(query));
|
|
75
75
|
await authorizeGenericQuery('delete', query, builder, 'api/delete');
|
|
76
76
|
return LinkedStorage.deleteQuery(builder);
|
|
77
77
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LincdAPI.js","sourceRoot":"","sources":["../../../src/shapes/LincdAPI.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"LincdAPI.js","sourceRoot":"","sources":["../../../src/shapes/LincdAPI.ts"],"names":[],"mappings":"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","sourcesContent":["import { Shape } from '@_linked/core/shapes/Shape';\nimport { linkedShape } from '../package.js';\nimport { server } from '../ontologies/server.js';\nimport { JSONParser } from '@_linked/server-utils/utils/JSONParser';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';\nimport { SparqlDataset as SPARQLStore } from '@_linked/core/sparql/SparqlDataset';\nimport {\n authorizeGenericQuery,\n authorizeRawQuery,\n resolveQueryCatalog,\n toQueryBuilder,\n} from '../utils/queryPlane.js';\nimport { getRpcExposureMode } from '../utils/rpcExposure.js';\n\n/** An error carrying an HTTP status, such as a `ServerCallError` (401/403). */\nfunction isStatusError(error: any): error is { status: number; message: string } {\n return (\n !!error &&\n typeof error.status === 'number' &&\n error.status >= 400 &&\n error.status < 600 &&\n error.name === 'ServerCallError'\n );\n}\n\n// Named explicitly: tsc emits `let X = class X`, and any later esbuild pass over that JS (Vite's\n// SSR `define` replacement runs one on every file mentioning a defined `process.env.*`) renames\n// the inner binding to `X2`, which would otherwise become this shape's IRI.\n@linkedShape({name: 'LincdAPI'})\nexport class LincdAPI extends Shape {\n static targetClass = server.LincdAPI;\n\n async process(\n request,\n response,\n httpMethod: 'get' | 'post' | 'put' | 'delete'\n ) {\n let { method, action } = request.params;\n const body = JSONParser.parseObject<any>(request.body, {\n shapeClasses: getRpcExposureMode() === 'enforce' ? 'reject' : 'warn',\n });\n\n //replace - with _ in action names\n let localMethod = httpMethod + '_' + method.replace(/-/g, '_');\n if (typeof this[localMethod] !== 'function') {\n throw new Error('Unknown method: ' + localMethod);\n }\n\n // The query routes (`post_*`) check access themselves, through\n // utils/queryPlane.\n try {\n const result = await this[localMethod](body, request, response);\n if (typeof result !== 'undefined' || !response.headersSent) {\n const jsonObject = JSONWriter.toJsObject(result);\n response.json(jsonObject);\n }\n } catch (error) {\n if (!response.headersSent && isStatusError(error)) {\n response.status(error.status).json({ error: error.message });\n return;\n }\n console.error(`Error while processing ${localMethod}:`, error);\n if (!response.headersSent) {\n const message =\n error instanceof Error ? error.message : 'Unknown server error';\n response.status(500).json({ error: message });\n }\n }\n }\n\n async post_select({ query }) {\n const builder = toQueryBuilder(query, await resolveQueryCatalog(query));\n await authorizeGenericQuery('select', query, builder, 'api/select');\n return LinkedStorage.selectQuery(builder);\n }\n async post_create({ query }) {\n const builder = toQueryBuilder(query, await resolveQueryCatalog(query));\n await authorizeGenericQuery('create', query, builder, 'api/create');\n return LinkedStorage.createQuery(builder);\n }\n async post_update({ query }) {\n const builder = toQueryBuilder(query, await resolveQueryCatalog(query));\n await authorizeGenericQuery('update', query, builder, 'api/update');\n return LinkedStorage.updateQuery(builder);\n }\n async post_delete({ query }) {\n const builder = toQueryBuilder(query, await resolveQueryCatalog(query));\n await authorizeGenericQuery('delete', query, builder, 'api/delete');\n return LinkedStorage.deleteQuery(builder);\n }\n\n async post_select_raw({ query }) {\n // A raw SPARQL string cannot be analysed for the shapes it touches: the\n // `rawQueries` setting decides (a session by default, or off).\n await authorizeRawQuery(query, 'api/select-raw');\n this.checkRawQuerySupport();\n return (LinkedStorage.getDefaultDataset() as unknown as SPARQLStore).rawQuery(\n query\n );\n }\n\n private checkRawQuerySupport() {\n if (!(LinkedStorage.getDefaultDataset() as unknown as SPARQLStore).rawQuery) {\n throw new Error(\n `Default store (${\n Object.getPrototypeOf(LinkedStorage.getDefaultDataset()).constructor\n .name\n }) does not support raw SPARQL queries`\n );\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { IDataset } from '@_linked/core/interfaces/IDataset';
|
|
2
|
+
import type { SelectQuery } from '@_linked/core/queries/SelectQuery';
|
|
3
|
+
import type { CountQuery } from '@_linked/core/queries/CountQuery';
|
|
4
|
+
import type { AskQuery } from '@_linked/core/queries/AskQuery';
|
|
5
|
+
import type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';
|
|
6
|
+
import type { CreateQuery } from '@_linked/core/queries/CreateQuery';
|
|
7
|
+
import type { DeleteQuery, DeleteResponse } from '@_linked/core/queries/DeleteQuery';
|
|
8
|
+
import type { SelectResult, UpdateResult, CreateResult } from '@_linked/core/queries/IntermediateRepresentation';
|
|
9
|
+
/**
|
|
10
|
+
* Constructor argument for `new RemoteDataset(config)`: the `config` object of a
|
|
11
|
+
* `linked.{frontend,backend}.datasets.json` entry, passed verbatim.
|
|
12
|
+
*/
|
|
13
|
+
export interface RemoteDatasetConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Base URL of the data server, e.g. `http://localhost:4040/gateway`; routes are appended
|
|
16
|
+
* to it. Without it, queries go to this process's own backend through `Server.call`.
|
|
17
|
+
* Present but empty is an error, never the no-url form.
|
|
18
|
+
*/
|
|
19
|
+
url?: string;
|
|
20
|
+
/** Sent as `Authorization: Bearer <token>` when set and `url` is set. */
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Or pass a fully-qualified URI directly. */
|
|
25
|
+
id?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP
|
|
29
|
+
* answer came back); `code` is the error body's `code` when the server sent one.
|
|
30
|
+
*/
|
|
31
|
+
export declare class RemoteDatasetError extends Error {
|
|
32
|
+
/** The data server base url ('' when the configured url is empty). */
|
|
33
|
+
readonly url: string;
|
|
34
|
+
/** The HTTP status; 0 = no HTTP answer. */
|
|
35
|
+
readonly status: number;
|
|
36
|
+
/** The error body's `code`, when the server sent one. */
|
|
37
|
+
readonly code?: string;
|
|
38
|
+
constructor(message: string, url: string, status: number, code?: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an
|
|
42
|
+
* answer that is not the server's. A boot that gets this should stop.
|
|
43
|
+
*/
|
|
44
|
+
export declare class RemoteDatasetUnreachableError extends RemoteDatasetError {
|
|
45
|
+
}
|
|
46
|
+
/** The data server answered 401 or 403: it refused this dataset's token. */
|
|
47
|
+
export declare class RemoteDatasetUnauthorizedError extends RemoteDatasetError {
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An {@link IDataset} whose queries are answered by a linked server.
|
|
51
|
+
*
|
|
52
|
+
* - **Without `url`** the server is this app's own backend: every query goes through
|
|
53
|
+
* `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),
|
|
54
|
+
* and on a backend `Server`'s local shortcut answers it in process. This is the store a
|
|
55
|
+
* browser uses to talk to its backend.
|
|
56
|
+
* - **With `url`** the server is a remote data server: every query posts
|
|
57
|
+
* `{"args": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer
|
|
58
|
+
* `token`, through `fetch`, never through `Server` (whose local shortcut would answer the
|
|
59
|
+
* app's own provider) and never retried (a retried write could apply twice). `init()` asks
|
|
60
|
+
* the server's `ping` and throws {@link RemoteDatasetUnreachableError} or
|
|
61
|
+
* {@link RemoteDatasetUnauthorizedError} when it cannot be used.
|
|
62
|
+
*
|
|
63
|
+
* A store is not a Shape: it addresses the backend by package name, so nothing about the
|
|
64
|
+
* store instance crosses the wire. Query routing is decided by the query's shape.
|
|
65
|
+
*
|
|
66
|
+
* `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must
|
|
67
|
+
* fail loudly awaits `init()` itself first; a successful `init()` is memoized.
|
|
68
|
+
*/
|
|
69
|
+
export declare class RemoteDataset implements IDataset {
|
|
70
|
+
/**
|
|
71
|
+
* Kept so the config form is still accepted and a store can be named; it is not an
|
|
72
|
+
* addressing mechanism, nothing transmits or resolves it.
|
|
73
|
+
*/
|
|
74
|
+
readonly id?: string;
|
|
75
|
+
/** The data server base url, trailing slashes trimmed; undefined in the no-url form. */
|
|
76
|
+
readonly url?: string;
|
|
77
|
+
private readonly token?;
|
|
78
|
+
private initialized?;
|
|
79
|
+
constructor(config?: RemoteDatasetConfig | string);
|
|
80
|
+
init(): Promise<void>;
|
|
81
|
+
selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number>;
|
|
82
|
+
askQuery(query: AskQuery): Promise<boolean>;
|
|
83
|
+
updateQuery(query: UpdateQuery): Promise<UpdateResult>;
|
|
84
|
+
createQuery(query: CreateQuery): Promise<CreateResult>;
|
|
85
|
+
deleteQuery(query: DeleteQuery): Promise<DeleteResponse>;
|
|
86
|
+
private call;
|
|
87
|
+
/**
|
|
88
|
+
* Opts in to `rejectOnError`, so a failed call (HTTP error status, or no
|
|
89
|
+
* provider on the backend) rejects with a `ServerCallError` carrying the
|
|
90
|
+
* status and the server's message instead of reading as an empty result.
|
|
91
|
+
* Whatever a successful call returns, `undefined` included, resolves as is.
|
|
92
|
+
*/
|
|
93
|
+
private callBackend;
|
|
94
|
+
/**
|
|
95
|
+
* The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed
|
|
96
|
+
* body (`null` is a valid result: a method that returned `undefined`). Messages name the
|
|
97
|
+
* url and the method, never the token.
|
|
98
|
+
*/
|
|
99
|
+
private post;
|
|
100
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { packageName } from '../../package.js';
|
|
2
|
+
import { Server } from '@_linked/server-utils/utils/Server';
|
|
3
|
+
/**
|
|
4
|
+
* A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP
|
|
5
|
+
* answer came back); `code` is the error body's `code` when the server sent one.
|
|
6
|
+
*/
|
|
7
|
+
export class RemoteDatasetError extends Error {
|
|
8
|
+
constructor(message, url, status, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = new.target.name;
|
|
11
|
+
this.url = url;
|
|
12
|
+
this.status = status;
|
|
13
|
+
if (code !== undefined)
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an
|
|
19
|
+
* answer that is not the server's. A boot that gets this should stop.
|
|
20
|
+
*/
|
|
21
|
+
export class RemoteDatasetUnreachableError extends RemoteDatasetError {
|
|
22
|
+
}
|
|
23
|
+
/** The data server answered 401 or 403: it refused this dataset's token. */
|
|
24
|
+
export class RemoteDatasetUnauthorizedError extends RemoteDatasetError {
|
|
25
|
+
}
|
|
26
|
+
/** On `ping`, these statuses mean nothing at that url serves the query wire. */
|
|
27
|
+
const PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);
|
|
28
|
+
/**
|
|
29
|
+
* An {@link IDataset} whose queries are answered by a linked server.
|
|
30
|
+
*
|
|
31
|
+
* - **Without `url`** the server is this app's own backend: every query goes through
|
|
32
|
+
* `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),
|
|
33
|
+
* and on a backend `Server`'s local shortcut answers it in process. This is the store a
|
|
34
|
+
* browser uses to talk to its backend.
|
|
35
|
+
* - **With `url`** the server is a remote data server: every query posts
|
|
36
|
+
* `{"args": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer
|
|
37
|
+
* `token`, through `fetch`, never through `Server` (whose local shortcut would answer the
|
|
38
|
+
* app's own provider) and never retried (a retried write could apply twice). `init()` asks
|
|
39
|
+
* the server's `ping` and throws {@link RemoteDatasetUnreachableError} or
|
|
40
|
+
* {@link RemoteDatasetUnauthorizedError} when it cannot be used.
|
|
41
|
+
*
|
|
42
|
+
* A store is not a Shape: it addresses the backend by package name, so nothing about the
|
|
43
|
+
* store instance crosses the wire. Query routing is decided by the query's shape.
|
|
44
|
+
*
|
|
45
|
+
* `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must
|
|
46
|
+
* fail loudly awaits `init()` itself first; a successful `init()` is memoized.
|
|
47
|
+
*/
|
|
48
|
+
export class RemoteDataset {
|
|
49
|
+
constructor(config) {
|
|
50
|
+
if (!config)
|
|
51
|
+
return;
|
|
52
|
+
if (typeof config === 'string') {
|
|
53
|
+
this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof config.url === 'string') {
|
|
57
|
+
this.url = config.url.trim().replace(/\/+$/, '');
|
|
58
|
+
const token = typeof config.token === 'string' ? config.token.trim() : '';
|
|
59
|
+
if (token)
|
|
60
|
+
this.token = token;
|
|
61
|
+
}
|
|
62
|
+
if (config.id) {
|
|
63
|
+
this.id = config.id;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (config.name) {
|
|
67
|
+
this.id = `${process.env.DATA_ROOT}/backend-api-store/${config.name}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
init() {
|
|
71
|
+
// No initialization needed when queries are routed to the app's own backend.
|
|
72
|
+
if (this.url === undefined)
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
if (!this.initialized) {
|
|
75
|
+
this.initialized = this.post('ping', []).then(() => undefined, (err) => {
|
|
76
|
+
// Not memoized: the next init() asks again.
|
|
77
|
+
this.initialized = undefined;
|
|
78
|
+
throw err;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return this.initialized;
|
|
82
|
+
}
|
|
83
|
+
// Queries serialize to DSL-JSON for the wire: the live (closed) query can't cross a call
|
|
84
|
+
// as-is, so we ship `toJSON()` and the server's generic provider rehydrates it with
|
|
85
|
+
// `fromJSON()`.
|
|
86
|
+
selectQuery(query) {
|
|
87
|
+
return this.call('selectQuery', query.toJSON());
|
|
88
|
+
}
|
|
89
|
+
askQuery(query) {
|
|
90
|
+
return this.call('askQuery', query.toJSON());
|
|
91
|
+
}
|
|
92
|
+
updateQuery(query) {
|
|
93
|
+
return this.call('updateQuery', query.toJSON());
|
|
94
|
+
}
|
|
95
|
+
createQuery(query) {
|
|
96
|
+
return this.call('createQuery', query.toJSON());
|
|
97
|
+
}
|
|
98
|
+
deleteQuery(query) {
|
|
99
|
+
return this.call('deleteQuery', query.toJSON());
|
|
100
|
+
}
|
|
101
|
+
call(method, json) {
|
|
102
|
+
if (this.url === undefined)
|
|
103
|
+
return this.callBackend(method, json);
|
|
104
|
+
return this.post(method, [json]);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Opts in to `rejectOnError`, so a failed call (HTTP error status, or no
|
|
108
|
+
* provider on the backend) rejects with a `ServerCallError` carrying the
|
|
109
|
+
* status and the server's message instead of reading as an empty result.
|
|
110
|
+
* Whatever a successful call returns, `undefined` included, resolves as is.
|
|
111
|
+
*/
|
|
112
|
+
callBackend(method, json) {
|
|
113
|
+
// The PACKAGE form, not the shape form. It posts to
|
|
114
|
+
// `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,
|
|
115
|
+
// no `instanceNode` — and the backend answers from this package's generic
|
|
116
|
+
// provider without resolving or instantiating any Shape.
|
|
117
|
+
return Server.call(packageName, { method, rejectOnError: true }, json);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed
|
|
121
|
+
* body (`null` is a valid result: a method that returned `undefined`). Messages name the
|
|
122
|
+
* url and the method, never the token.
|
|
123
|
+
*/
|
|
124
|
+
async post(method, args) {
|
|
125
|
+
var _a;
|
|
126
|
+
const base = (_a = this.url) !== null && _a !== void 0 ? _a : '';
|
|
127
|
+
if (!base) {
|
|
128
|
+
throw new RemoteDatasetUnreachableError(`RemoteDataset: no data server url is set (the configured url is empty); cannot ${method}`, '', 0);
|
|
129
|
+
}
|
|
130
|
+
const target = `${base}/call/${packageName}/${method}`;
|
|
131
|
+
const headers = {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
Accept: 'application/json',
|
|
134
|
+
};
|
|
135
|
+
if (this.token)
|
|
136
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
137
|
+
let response;
|
|
138
|
+
try {
|
|
139
|
+
response = await globalThis.fetch(target, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers,
|
|
142
|
+
body: JSON.stringify({ args }),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
147
|
+
throw new RemoteDatasetUnreachableError(`RemoteDataset: cannot reach the data server at ${base} (${method}): ${cause}`, base, 0);
|
|
148
|
+
}
|
|
149
|
+
const text = await response.text();
|
|
150
|
+
let body = undefined;
|
|
151
|
+
let isJson = false;
|
|
152
|
+
if (text) {
|
|
153
|
+
try {
|
|
154
|
+
body = JSON.parse(text);
|
|
155
|
+
isJson = true;
|
|
156
|
+
}
|
|
157
|
+
catch (_b) {
|
|
158
|
+
body = text;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (response.ok) {
|
|
162
|
+
if (isJson)
|
|
163
|
+
return body;
|
|
164
|
+
if (method === 'ping') {
|
|
165
|
+
throw new RemoteDatasetUnreachableError(`RemoteDataset: the data server at ${base} answered ping with a body that is not JSON`, base, response.status);
|
|
166
|
+
}
|
|
167
|
+
if (!text)
|
|
168
|
+
return undefined;
|
|
169
|
+
throw new RemoteDatasetError(`RemoteDataset: the data server at ${base} answered ${method} with a body that is not JSON`, base, response.status);
|
|
170
|
+
}
|
|
171
|
+
const record = body && typeof body === 'object' ? body : undefined;
|
|
172
|
+
const code = typeof (record === null || record === void 0 ? void 0 : record.code) === 'string' ? record.code : undefined;
|
|
173
|
+
const detail = typeof (record === null || record === void 0 ? void 0 : record.error) === 'string'
|
|
174
|
+
? record.error
|
|
175
|
+
: typeof body === 'string' && body
|
|
176
|
+
? body
|
|
177
|
+
: response.statusText;
|
|
178
|
+
const message = `RemoteDataset: the data server at ${base} answered ${method} with ${response.status}${code ? ` (${code})` : ''}${detail ? `: ${detail}` : ''}`;
|
|
179
|
+
if (response.status === 401 || response.status === 403) {
|
|
180
|
+
throw new RemoteDatasetUnauthorizedError(message, base, response.status, code);
|
|
181
|
+
}
|
|
182
|
+
if (method === 'ping' && (PING_UNREACHABLE_STATUSES.has(response.status) || !isJson)) {
|
|
183
|
+
throw new RemoteDatasetUnreachableError(message, base, response.status, code);
|
|
184
|
+
}
|
|
185
|
+
throw new RemoteDatasetError(message, base, response.status, code);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=RemoteDataset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteDataset.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/RemoteDataset.ts"],"names":[],"mappings":"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","sourcesContent":["import { packageName } from '../../package.js';\nimport { Server } from '@_linked/server-utils/utils/Server';\nimport type { IDataset } from '@_linked/core/interfaces/IDataset';\nimport type { SelectQuery } from '@_linked/core/queries/SelectQuery';\nimport type { CountQuery } from '@_linked/core/queries/CountQuery';\nimport type { AskQuery } from '@_linked/core/queries/AskQuery';\nimport type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';\nimport type { CreateQuery } from '@_linked/core/queries/CreateQuery';\nimport type {\n DeleteQuery,\n DeleteResponse,\n} from '@_linked/core/queries/DeleteQuery';\nimport type {\n SelectResult,\n UpdateResult,\n CreateResult,\n} from '@_linked/core/queries/IntermediateRepresentation';\n\n/**\n * Constructor argument for `new RemoteDataset(config)`: the `config` object of a\n * `linked.{frontend,backend}.datasets.json` entry, passed verbatim.\n */\nexport interface RemoteDatasetConfig {\n /**\n * Base URL of the data server, e.g. `http://localhost:4040/gateway`; routes are appended\n * to it. Without it, queries go to this process's own backend through `Server.call`.\n * Present but empty is an error, never the no-url form.\n */\n url?: string;\n /** Sent as `Authorization: Bearer <token>` when set and `url` is set. */\n token?: string;\n /** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */\n name?: string;\n /** Or pass a fully-qualified URI directly. */\n id?: string;\n}\n\n/**\n * A failed call to the data server at `url`. `status` is the HTTP status (0 when no HTTP\n * answer came back); `code` is the error body's `code` when the server sent one.\n */\nexport class RemoteDatasetError extends Error {\n /** The data server base url ('' when the configured url is empty). */\n readonly url: string;\n /** The HTTP status; 0 = no HTTP answer. */\n readonly status: number;\n /** The error body's `code`, when the server sent one. */\n readonly code?: string;\n\n constructor(message: string, url: string, status: number, code?: string) {\n super(message);\n this.name = new.target.name;\n this.url = url;\n this.status = status;\n if (code !== undefined) this.code = code;\n }\n}\n\n/**\n * Nothing at `url` speaks the query wire: no HTTP answer, an empty url, or (on `ping`) an\n * answer that is not the server's. A boot that gets this should stop.\n */\nexport class RemoteDatasetUnreachableError extends RemoteDatasetError {}\n\n/** The data server answered 401 or 403: it refused this dataset's token. */\nexport class RemoteDatasetUnauthorizedError extends RemoteDatasetError {}\n\ntype Method =\n | 'ping'\n | 'selectQuery'\n | 'askQuery'\n | 'updateQuery'\n | 'createQuery'\n | 'deleteQuery';\n\n/** On `ping`, these statuses mean nothing at that url serves the query wire. */\nconst PING_UNREACHABLE_STATUSES = new Set([404, 501, 502, 503, 504]);\n\n/**\n * An {@link IDataset} whose queries are answered by a linked server.\n *\n * - **Without `url`** the server is this app's own backend: every query goes through\n * `Server.call`'s package form (`/call/@_linked/server/<method>` with a body of `{args}`),\n * and on a backend `Server`'s local shortcut answers it in process. This is the store a\n * browser uses to talk to its backend.\n * - **With `url`** the server is a remote data server: every query posts\n * `{\"args\": [query.toJSON()]}` to `{url}/call/@_linked/server/<method>` with the bearer\n * `token`, through `fetch`, never through `Server` (whose local shortcut would answer the\n * app's own provider) and never retried (a retried write could apply twice). `init()` asks\n * the server's `ping` and throws {@link RemoteDatasetUnreachableError} or\n * {@link RemoteDatasetUnauthorizedError} when it cannot be used.\n *\n * A store is not a Shape: it addresses the backend by package name, so nothing about the\n * store instance crosses the wire. Query routing is decided by the query's shape.\n *\n * `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must\n * fail loudly awaits `init()` itself first; a successful `init()` is memoized.\n */\nexport class RemoteDataset implements IDataset {\n /**\n * Kept so the config form is still accepted and a store can be named; it is not an\n * addressing mechanism, nothing transmits or resolves it.\n */\n readonly id?: string;\n /** The data server base url, trailing slashes trimmed; undefined in the no-url form. */\n readonly url?: string;\n private readonly token?: string;\n private initialized?: Promise<void>;\n\n constructor(config?: RemoteDatasetConfig | string) {\n if (!config) return;\n if (typeof config === 'string') {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;\n return;\n }\n if (typeof config.url === 'string') {\n this.url = config.url.trim().replace(/\\/+$/, '');\n const token = typeof config.token === 'string' ? config.token.trim() : '';\n if (token) this.token = token;\n }\n if (config.id) {\n this.id = config.id;\n return;\n }\n if (config.name) {\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${config.name}`;\n }\n }\n\n init(): Promise<void> {\n // No initialization needed when queries are routed to the app's own backend.\n if (this.url === undefined) return Promise.resolve();\n if (!this.initialized) {\n this.initialized = this.post('ping', []).then(\n () => undefined,\n (err) => {\n // Not memoized: the next init() asks again.\n this.initialized = undefined;\n throw err;\n },\n );\n }\n return this.initialized;\n }\n\n // Queries serialize to DSL-JSON for the wire: the live (closed) query can't cross a call\n // as-is, so we ship `toJSON()` and the server's generic provider rehydrates it with\n // `fromJSON()`.\n selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.call('selectQuery', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.call('askQuery', query.toJSON());\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.call('updateQuery', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.call('createQuery', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.call('deleteQuery', query.toJSON());\n }\n\n private call<T>(method: Method, json: unknown): Promise<T> {\n if (this.url === undefined) return this.callBackend(method, json);\n return this.post(method, [json]) as Promise<T>;\n }\n\n /**\n * Opts in to `rejectOnError`, so a failed call (HTTP error status, or no\n * provider on the backend) rejects with a `ServerCallError` carrying the\n * status and the server's message instead of reading as an empty result.\n * Whatever a successful call returns, `undefined` included, resolves as is.\n */\n private callBackend<T>(method: string, json: unknown): Promise<T> {\n // The PACKAGE form, not the shape form. It posts to\n // `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,\n // no `instanceNode` — and the backend answers from this package's generic\n // provider without resolving or instantiating any Shape.\n return Server.call(packageName, { method, rejectOnError: true }, json);\n }\n\n /**\n * The url form: one POST to `{url}/call/@_linked/server/<method>`. Resolves the parsed\n * body (`null` is a valid result: a method that returned `undefined`). Messages name the\n * url and the method, never the token.\n */\n private async post(method: Method, args: unknown[]): Promise<unknown> {\n const base = this.url ?? '';\n if (!base) {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: no data server url is set (the configured url is empty); cannot ${method}`,\n '',\n 0,\n );\n }\n const target = `${base}/call/${packageName}/${method}`;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n if (this.token) headers.Authorization = `Bearer ${this.token}`;\n\n let response: Response;\n try {\n response = await globalThis.fetch(target, {\n method: 'POST',\n headers,\n body: JSON.stringify({ args }),\n });\n } catch (err) {\n const cause = err instanceof Error ? err.message : String(err);\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: cannot reach the data server at ${base} (${method}): ${cause}`,\n base,\n 0,\n );\n }\n\n const text = await response.text();\n let body: unknown = undefined;\n let isJson = false;\n if (text) {\n try {\n body = JSON.parse(text);\n isJson = true;\n } catch {\n body = text;\n }\n }\n\n if (response.ok) {\n if (isJson) return body;\n if (method === 'ping') {\n throw new RemoteDatasetUnreachableError(\n `RemoteDataset: the data server at ${base} answered ping with a body that is not JSON`,\n base,\n response.status,\n );\n }\n if (!text) return undefined;\n throw new RemoteDatasetError(\n `RemoteDataset: the data server at ${base} answered ${method} with a body that is not JSON`,\n base,\n response.status,\n );\n }\n\n const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : undefined;\n const code = typeof record?.code === 'string' ? record.code : undefined;\n const detail =\n typeof record?.error === 'string'\n ? record.error\n : typeof body === 'string' && body\n ? body\n : response.statusText;\n const message = `RemoteDataset: the data server at ${base} answered ${method} with ${response.status}${\n code ? ` (${code})` : ''\n }${detail ? `: ${detail}` : ''}`;\n\n if (response.status === 401 || response.status === 403) {\n throw new RemoteDatasetUnauthorizedError(message, base, response.status, code);\n }\n if (method === 'ping' && (PING_UNREACHABLE_STATUSES.has(response.status) || !isJson)) {\n throw new RemoteDatasetUnreachableError(message, base, response.status, code);\n }\n throw new RemoteDatasetError(message, base, response.status, code);\n }\n}\n"]}
|