@_linked/server 2.25.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/lib/esm/backend.d.ts +8 -0
- package/lib/esm/backend.js +23 -14
- 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/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/package.json +1 -1
- package/readme.md +65 -51
- 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,20 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
Migration:
|
|
10
|
+
|
|
11
|
+
- **`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.
|
|
12
|
+
- **`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.
|
|
13
|
+
- **Config:** `RemoteDatasetConfig` is `{ url?, token?, name?, id? }`. A `url` that is present but empty never falls back to the no-url form.
|
|
14
|
+
- **`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.
|
|
15
|
+
- **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).
|
|
16
|
+
- **`ping`:** `POST /call/@_linked/server/ping` is a new `public` method that answers `{ "ok": true }`.
|
|
17
|
+
|
|
3
18
|
## 2.25.0
|
|
4
19
|
|
|
5
20
|
### 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
|
@@ -30,22 +30,25 @@ 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.
|
|
49
52
|
//
|
|
50
53
|
// Every query is checked first against the access rules of the stores it
|
|
51
54
|
// maps to, declared in the app's storage config (`withAccess`; a store with
|
|
@@ -79,6 +82,12 @@ export default class LincdServerBackendProvider extends BackendProvider {
|
|
|
79
82
|
return LinkedStorage.deleteQuery(query);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
85
|
+
__decorate([
|
|
86
|
+
callable('public'),
|
|
87
|
+
__metadata("design:type", Function),
|
|
88
|
+
__metadata("design:paramtypes", []),
|
|
89
|
+
__metadata("design:returntype", Promise)
|
|
90
|
+
], LincdServerBackendProvider.prototype, "ping", null);
|
|
82
91
|
__decorate([
|
|
83
92
|
callable('public'),
|
|
84
93
|
__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,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,eAAe;IACrE,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,EAAE,CAAC;YACzC,iBAAiB,CAAC,eAAe,CAC/B,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,YAAY,CAAC,CACxD,CAAC;QACJ,CAAC;QACD,IAAI,oBAAoB,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EAAE;gBAC5D,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,
|
|
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 { authorizeGenericQuery, toQueryBuilder } 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.\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);\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);\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);\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);\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);\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"]}
|
|
@@ -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"]}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -20,8 +20,9 @@ If you want to further adjust the functionality of LincdServer yourself, either
|
|
|
20
20
|
- [Gotchas](#gotchas)
|
|
21
21
|
- [Error semantics](#error-semantics)
|
|
22
22
|
- [Query stores](#query-stores)
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
23
|
+
- [Without `url`](#without-url)
|
|
24
|
+
- [With `url`](#with-url)
|
|
25
|
+
- [`ping`](#ping)
|
|
25
26
|
- [API-only servers (`server.apiOnly`)](#api-only-servers-serverapionly)
|
|
26
27
|
- [Provider lifecycle and HMR](#provider-lifecycle-and-hmr)
|
|
27
28
|
- [When a provider throws, and refusing to start](#when-a-provider-throws-and-refusing-to-start)
|
|
@@ -233,73 +234,86 @@ What the caller sees depends on how it calls:
|
|
|
233
234
|
- **`Server.call` on the backend.** Here it calls the `LinkedServer` directly.
|
|
234
235
|
- A provider method that throws always rejects.
|
|
235
236
|
- An unmatched call resolves `undefined` by default and rejects with a 501 `ServerCallError` when `rejectOnError` is set.
|
|
236
|
-
- **`
|
|
237
|
+
- **`RemoteDataset` without `url`.** It always uses `rejectOnError`, so a failed query rejects with the status and message. A successful call resolves with whatever the provider returned, `undefined` included.
|
|
237
238
|
|
|
238
239
|
## Query stores
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
returns;
|
|
241
|
+
`RemoteDataset` is an `IDataset` whose Linked Queries are answered by a linked server instead of
|
|
242
|
+
by the store itself. It ships `query.toJSON()` and resolves with the plain JSON the server
|
|
243
|
+
returns; it offers no raw SPARQL (`rawQuery`), dataset administration or change feed.
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
Export path `@_linked/server/shapes/quadstores/RemoteDataset` (named export `RemoteDataset`, so
|
|
246
|
+
`loadStores` resolves it by the last path segment). One class, two forms:
|
|
247
|
+
|
|
248
|
+
| | no `url` | with `url` |
|
|
245
249
|
| --- | --- | --- |
|
|
246
|
-
|
|
|
247
|
-
| Sends
|
|
248
|
-
| Authenticated by | the user's session |
|
|
249
|
-
|
|
|
250
|
+
| Answered by | this app's own backend | a remote data server, e.g. a host's gateway |
|
|
251
|
+
| Sends through | `Server.call`'s package form (`/call/@_linked/server/<method>`); on a backend, `Server`'s local shortcut | `globalThis.fetch` to `{url}/call/@_linked/server/<method>`, never `Server` |
|
|
252
|
+
| Authenticated by | the user's session | `Authorization: Bearer <token>` |
|
|
253
|
+
| `init()` | resolves at once | posts `ping`, throws a typed error when the server cannot be used |
|
|
254
|
+
| Typical config | `linked.frontend.datasets.json` | `linked.backend.datasets.json` |
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
export interface RemoteDatasetConfig {
|
|
258
|
+
url?: string; // data server base, e.g. http://localhost:4040/gateway; routes are appended
|
|
259
|
+
token?: string; // sent as Authorization: Bearer <token> when set and url is set
|
|
260
|
+
name?: string; // id = ${DATA_ROOT}/backend-api-store/<name>; does not affect routing
|
|
261
|
+
id?: string;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
250
264
|
|
|
251
|
-
|
|
265
|
+
`new RemoteDataset('main')` is the `name` form.
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
`linked.frontend.datasets.json`; `config` may carry a `name` or `id`, neither of which affects
|
|
255
|
-
routing. Its error semantics are under [Error semantics](#error-semantics).
|
|
267
|
+
### Without `url`
|
|
256
268
|
|
|
257
|
-
|
|
269
|
+
Every query is `Server.call(packageName, { method, rejectOnError: true }, query.toJSON())`, and
|
|
270
|
+
this package's generic provider answers it from the backend's `LinkedStorage`. Its error
|
|
271
|
+
semantics are under [Error semantics](#error-semantics). `token` is ignored.
|
|
258
272
|
|
|
259
|
-
|
|
260
|
-
`loadStores` resolves it by the last path segment). It is for a backend whose data lives behind
|
|
261
|
-
a gateway run by the host that started it — for example an app process started by Create Now,
|
|
262
|
-
whose queries go to CN's gateway so that branch forks, authorization and audit apply to app
|
|
263
|
-
traffic.
|
|
273
|
+
### With `url`
|
|
264
274
|
|
|
265
275
|
```json
|
|
266
276
|
{
|
|
267
277
|
"datasets": {
|
|
268
|
-
"
|
|
269
|
-
"store": "@_linked/server/shapes/quadstores/
|
|
270
|
-
"config": { "
|
|
278
|
+
"appData": {
|
|
279
|
+
"store": "@_linked/server/shapes/quadstores/RemoteDataset",
|
|
280
|
+
"config": { "url": "${DATA_SERVER_URL:-}", "token": "${DATA_SERVER_TOKEN:-}" }
|
|
271
281
|
}
|
|
272
282
|
}
|
|
273
283
|
}
|
|
274
284
|
```
|
|
275
285
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
-
|
|
285
|
-
the
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
286
|
+
- Each method posts `{"args": [<query.toJSON()>]}` to `{url}/call/@_linked/server/<method>`
|
|
287
|
+
(`selectQuery`, which also carries counts, `askQuery`, `updateQuery`, `createQuery`,
|
|
288
|
+
`deleteQuery`) with `Content-Type` and `Accept` set to `application/json` and, when `token` is
|
|
289
|
+
non-empty, `Authorization: Bearer <token>`. It resolves the parsed body; `null` is a valid
|
|
290
|
+
result. No other headers are sent and nothing is retried, so a write is never applied twice.
|
|
291
|
+
- Trailing slashes are trimmed from `url`. A `url` that is present but empty (an unset
|
|
292
|
+
`${DATA_SERVER_URL:-}`) never falls back to the no-url form: `init()` and every method throw
|
|
293
|
+
`RemoteDatasetUnreachableError` with `url: ''`.
|
|
294
|
+
- `init()` posts `{"args": []}` to `{url}/call/@_linked/server/ping`. A success is memoized; after
|
|
295
|
+
a failure the next `init()` asks again. `LinkedStorage.setDefaultDataset` does not await
|
|
296
|
+
`init()`, so a boot that must fail loudly awaits it itself before registering the store.
|
|
297
|
+
- Errors, each carrying `url` (the base), `status` (0 when no HTTP answer came back) and `code`
|
|
298
|
+
(the error body's `code`, when the server sent one). Messages name the url and the method,
|
|
299
|
+
never the token.
|
|
300
|
+
|
|
301
|
+
| Answer | Error |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| no HTTP answer | `RemoteDatasetUnreachableError` (status 0) |
|
|
304
|
+
| 401 or 403 | `RemoteDatasetUnauthorizedError` |
|
|
305
|
+
| on `ping` only: 404, 501, 502, 503, 504, or a body that is not JSON | `RemoteDatasetUnreachableError` |
|
|
306
|
+
| any other non-2xx | `RemoteDatasetError` |
|
|
307
|
+
|
|
308
|
+
`RemoteDatasetUnreachableError` and `RemoteDatasetUnauthorizedError` extend
|
|
309
|
+
`RemoteDatasetError`, which extends `Error`; match them with `instanceof`.
|
|
310
|
+
|
|
311
|
+
### `ping`
|
|
312
|
+
|
|
313
|
+
`POST /call/@_linked/server/ping` (a `public` method of this package's generic provider) reads
|
|
314
|
+
nothing, changes nothing and answers `{ "ok": true }`. A plain linked server has no token
|
|
315
|
+
concept; a host that serves the wire with tokens checks the token itself and answers 401 for a
|
|
316
|
+
refused one.
|
|
303
317
|
|
|
304
318
|
## API-only servers (`server.apiOnly`)
|
|
305
319
|
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { IDataset } from '@_linked/core/interfaces/IDataset';
|
|
2
|
-
import type { SelectQuery } from '@_linked/core/queries/SelectQuery';
|
|
3
|
-
import type { AskQuery } from '@_linked/core/queries/AskQuery';
|
|
4
|
-
import type { UpdateQuery } from '@_linked/core/queries/UpdateQuery';
|
|
5
|
-
import type { CreateQuery } from '@_linked/core/queries/CreateQuery';
|
|
6
|
-
import type { DeleteQuery, DeleteResponse } from '@_linked/core/queries/DeleteQuery';
|
|
7
|
-
import type { SelectResult, UpdateResult, CreateResult } from '@_linked/core/queries/IntermediateRepresentation';
|
|
8
|
-
/**
|
|
9
|
-
* Constructor argument for `new BackendAPIStore(config)`. Per the
|
|
10
|
-
* docs/backlog/016-ejection-export-flow.md spec — a single JSON object
|
|
11
|
-
* passed verbatim from linked.{frontend,backend}.datasets.json's `config`.
|
|
12
|
-
*/
|
|
13
|
-
export interface BackendAPIStoreConfig {
|
|
14
|
-
/** Stable name; becomes the suffix of the store URI: ${DATA_ROOT}/backend-api-store/<name>. */
|
|
15
|
-
name?: string;
|
|
16
|
-
/** Or pass a fully-qualified URI directly. */
|
|
17
|
-
id?: string;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Frontend-side store that routes all queries to the backend via `Server.call()`.
|
|
21
|
-
* The backend's generic provider for this package executes them against the real store.
|
|
22
|
-
*
|
|
23
|
-
* A store is not a Shape. This class used to `extends Shape` and carry `@linkedShape`
|
|
24
|
-
* with a `targetClass`, purely so that `Server.call(this, …)` could address it: the
|
|
25
|
-
* transport derived the target from `shape instanceof Shape && shape.id` (the instance)
|
|
26
|
-
* and `shapeClass.shape.id` (the class). Neither carried any meaning — the backend
|
|
27
|
-
* provider never read the store it was handed, and query routing is decided by the
|
|
28
|
-
* QUERY's shape, not the store's.
|
|
29
|
-
*
|
|
30
|
-
* It now uses `Server.call`'s package form instead, which addresses the backend by
|
|
31
|
-
* package name and sends no shape fields at all. That removes the last reason for this
|
|
32
|
-
* class to be a Shape, and with it the `new (providerShapeClass)({id})` the backend had
|
|
33
|
-
* to perform to rebuild a store instance nobody used.
|
|
34
|
-
*/
|
|
35
|
-
export declare class BackendAPIStore implements IDataset {
|
|
36
|
-
/**
|
|
37
|
-
* Kept so the config form is still accepted and a store can be named, but it is no
|
|
38
|
-
* longer an addressing mechanism — nothing transmits or resolves it. Retained because
|
|
39
|
-
* `linked.{frontend,backend}.datasets.json` may still pass `name`/`id`.
|
|
40
|
-
*/
|
|
41
|
-
readonly id?: string;
|
|
42
|
-
constructor(config?: BackendAPIStoreConfig | string | {
|
|
43
|
-
id?: string;
|
|
44
|
-
});
|
|
45
|
-
init(): Promise<void>;
|
|
46
|
-
selectQuery(query: SelectQuery): Promise<SelectResult>;
|
|
47
|
-
askQuery(query: AskQuery): Promise<boolean>;
|
|
48
|
-
updateQuery(query: UpdateQuery): Promise<UpdateResult>;
|
|
49
|
-
createQuery(query: CreateQuery): Promise<CreateResult>;
|
|
50
|
-
deleteQuery(query: DeleteQuery): Promise<DeleteResponse>;
|
|
51
|
-
/**
|
|
52
|
-
* Opts in to `rejectOnError`, so a failed call (HTTP error status, or no
|
|
53
|
-
* provider on the backend) rejects with a `ServerCallError` carrying the
|
|
54
|
-
* status and the server's message instead of reading as an empty result.
|
|
55
|
-
* Whatever a successful call returns, `undefined` included, resolves as is.
|
|
56
|
-
*/
|
|
57
|
-
private callBackend;
|
|
58
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { packageName } from '../../package.js';
|
|
2
|
-
import { Server } from '@_linked/server-utils/utils/Server';
|
|
3
|
-
/**
|
|
4
|
-
* Frontend-side store that routes all queries to the backend via `Server.call()`.
|
|
5
|
-
* The backend's generic provider for this package executes them against the real store.
|
|
6
|
-
*
|
|
7
|
-
* A store is not a Shape. This class used to `extends Shape` and carry `@linkedShape`
|
|
8
|
-
* with a `targetClass`, purely so that `Server.call(this, …)` could address it: the
|
|
9
|
-
* transport derived the target from `shape instanceof Shape && shape.id` (the instance)
|
|
10
|
-
* and `shapeClass.shape.id` (the class). Neither carried any meaning — the backend
|
|
11
|
-
* provider never read the store it was handed, and query routing is decided by the
|
|
12
|
-
* QUERY's shape, not the store's.
|
|
13
|
-
*
|
|
14
|
-
* It now uses `Server.call`'s package form instead, which addresses the backend by
|
|
15
|
-
* package name and sends no shape fields at all. That removes the last reason for this
|
|
16
|
-
* class to be a Shape, and with it the `new (providerShapeClass)({id})` the backend had
|
|
17
|
-
* to perform to rebuild a store instance nobody used.
|
|
18
|
-
*/
|
|
19
|
-
export class BackendAPIStore {
|
|
20
|
-
constructor(config) {
|
|
21
|
-
if (!config)
|
|
22
|
-
return;
|
|
23
|
-
if (typeof config === 'string') {
|
|
24
|
-
this.id = `${process.env.DATA_ROOT}/backend-api-store/${config}`;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (config.id) {
|
|
28
|
-
this.id = config.id;
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (config.name) {
|
|
32
|
-
const name = config.name;
|
|
33
|
-
this.id = `${process.env.DATA_ROOT}/backend-api-store/${name}`;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async init() {
|
|
37
|
-
// No initialization needed — queries are routed to the backend
|
|
38
|
-
}
|
|
39
|
-
// Queries serialize to DSL-JSON for the wire (core 2.10.0 contract flip): the
|
|
40
|
-
// live (closed) query can't cross Server.call as-is, so we ship `toJSON()` and
|
|
41
|
-
// the BackendAPIStoreProvider rehydrates with `fromJSON()` on the backend.
|
|
42
|
-
selectQuery(query) {
|
|
43
|
-
return this.callBackend('selectQuery', query.toJSON());
|
|
44
|
-
}
|
|
45
|
-
askQuery(query) {
|
|
46
|
-
return this.callBackend('askQuery', query.toJSON());
|
|
47
|
-
}
|
|
48
|
-
updateQuery(query) {
|
|
49
|
-
return this.callBackend('updateQuery', query.toJSON());
|
|
50
|
-
}
|
|
51
|
-
createQuery(query) {
|
|
52
|
-
return this.callBackend('createQuery', query.toJSON());
|
|
53
|
-
}
|
|
54
|
-
deleteQuery(query) {
|
|
55
|
-
return this.callBackend('deleteQuery', query.toJSON());
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Opts in to `rejectOnError`, so a failed call (HTTP error status, or no
|
|
59
|
-
* provider on the backend) rejects with a `ServerCallError` carrying the
|
|
60
|
-
* status and the server's message instead of reading as an empty result.
|
|
61
|
-
* Whatever a successful call returns, `undefined` included, resolves as is.
|
|
62
|
-
*/
|
|
63
|
-
callBackend(method, json) {
|
|
64
|
-
// The PACKAGE form, not the shape form. It posts to
|
|
65
|
-
// `/call/<packageName>/<method>` with a body of `{args}` only — no `shapeURI`,
|
|
66
|
-
// no `instanceNode` — and the backend answers from this package's generic
|
|
67
|
-
// provider without resolving or instantiating any Shape.
|
|
68
|
-
return Server.call(packageName, { method, rejectOnError: true }, json);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
//# sourceMappingURL=BackendAPIStore.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BackendAPIStore.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/BackendAPIStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AA4B5D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,eAAe;IAQ1B,YAAY,MAAyD;QACnE,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,sBAAsB,MAAM,EAAE,CAAC;YACjE,OAAO;QACT,CAAC;QACD,IAAK,MAAgC,CAAC,EAAE,EAAE,CAAC;YACzC,IAAI,CAAC,EAAE,GAAI,MAAgC,CAAC,EAAG,CAAC;YAChD,OAAO;QACT,CAAC;QACD,IAAK,MAAgC,CAAC,IAAI,EAAE,CAAC;YAC3C,MAAM,IAAI,GAAI,MAAgC,CAAC,IAAK,CAAC;YACrD,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,sBAAsB,IAAI,EAAE,CAAC;QACjE,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,+DAA+D;IACjE,CAAC;IAED,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,QAAQ,CAAC,KAAe;QACtB,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IAED;;;;;OAKG;IACK,WAAW,CAAI,MAAc,EAAE,IAAa;QAClD,oDAAoD;QACpD,+EAA+E;QAC/E,0EAA0E;QAC1E,yDAAyD;QACzD,OAAO,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;IACzE,CAAC;CACF","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 { 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 BackendAPIStore(config)`. Per the\n * docs/backlog/016-ejection-export-flow.md spec — a single JSON object\n * passed verbatim from linked.{frontend,backend}.datasets.json's `config`.\n */\nexport interface BackendAPIStoreConfig {\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 * Frontend-side store that routes all queries to the backend via `Server.call()`.\n * The backend's generic provider for this package executes them against the real store.\n *\n * A store is not a Shape. This class used to `extends Shape` and carry `@linkedShape`\n * with a `targetClass`, purely so that `Server.call(this, …)` could address it: the\n * transport derived the target from `shape instanceof Shape && shape.id` (the instance)\n * and `shapeClass.shape.id` (the class). Neither carried any meaning — the backend\n * provider never read the store it was handed, and query routing is decided by the\n * QUERY's shape, not the store's.\n *\n * It now uses `Server.call`'s package form instead, which addresses the backend by\n * package name and sends no shape fields at all. That removes the last reason for this\n * class to be a Shape, and with it the `new (providerShapeClass)({id})` the backend had\n * to perform to rebuild a store instance nobody used.\n */\nexport class BackendAPIStore implements IDataset {\n /**\n * Kept so the config form is still accepted and a store can be named, but it is no\n * longer an addressing mechanism — nothing transmits or resolves it. Retained because\n * `linked.{frontend,backend}.datasets.json` may still pass `name`/`id`.\n */\n readonly id?: string;\n\n constructor(config?: BackendAPIStoreConfig | string | { id?: 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 ((config as BackendAPIStoreConfig).id) {\n this.id = (config as BackendAPIStoreConfig).id!;\n return;\n }\n if ((config as BackendAPIStoreConfig).name) {\n const name = (config as BackendAPIStoreConfig).name!;\n this.id = `${process.env.DATA_ROOT}/backend-api-store/${name}`;\n }\n }\n\n async init(): Promise<void> {\n // No initialization needed — queries are routed to the backend\n }\n\n // Queries serialize to DSL-JSON for the wire (core 2.10.0 contract flip): the\n // live (closed) query can't cross Server.call as-is, so we ship `toJSON()` and\n // the BackendAPIStoreProvider rehydrates with `fromJSON()` on the backend.\n selectQuery(query: SelectQuery): Promise<SelectResult> {\n return this.callBackend('selectQuery', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.callBackend('askQuery', query.toJSON());\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.callBackend('updateQuery', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.callBackend('createQuery', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.callBackend('deleteQuery', query.toJSON());\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"]}
|
|
@@ -1,85 +0,0 @@
|
|
|
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 GatewayStore(config)` — the `config` object of a
|
|
11
|
-
* `linked.backend.datasets.json` entry, passed verbatim by `loadStores`.
|
|
12
|
-
*/
|
|
13
|
-
export interface GatewayStoreConfig {
|
|
14
|
-
/** Base URL of the gateway, e.g. `http://localhost:4040/gateway`. Routes are appended to it. */
|
|
15
|
-
endpoint: string;
|
|
16
|
-
/** The bearer credential the gateway issued to this process. */
|
|
17
|
-
credential: string;
|
|
18
|
-
/** Transport override; defaults to `globalThis.fetch`. */
|
|
19
|
-
fetch?: typeof fetch;
|
|
20
|
-
}
|
|
21
|
-
/** The op names on the wire; each is the `IDataset` method the store was called on. */
|
|
22
|
-
export type GatewayOp = 'select' | 'ask' | 'update' | 'create' | 'delete';
|
|
23
|
-
/** The wire version this store speaks. The gateway refuses any other with `code: "wire-version"`. */
|
|
24
|
-
export declare const GATEWAY_WIRE_VERSION = 1;
|
|
25
|
-
/**
|
|
26
|
-
* A non-2xx answer from the gateway. `code` is the body's `code` when the gateway sent one
|
|
27
|
-
* (`bad-request`, `wire-version`, `unknown-shape`, `refused`, `fork-failed`, `query-failed`),
|
|
28
|
-
* else a generic one; `body` is the parsed response when it was JSON.
|
|
29
|
-
*/
|
|
30
|
-
export declare class GatewayStoreError extends Error {
|
|
31
|
-
readonly status: number;
|
|
32
|
-
readonly code: string;
|
|
33
|
-
readonly body?: unknown;
|
|
34
|
-
constructor(message: string, status: number, code: string, body?: unknown);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* The gateway could not be used at all: the request never got an HTTP answer (network error,
|
|
38
|
-
* `status` 0), or the gateway refused the credential (401). Thrown by `init()` and by every
|
|
39
|
-
* query method; a process that gets this at boot should stop rather than carry on without data.
|
|
40
|
-
*/
|
|
41
|
-
export declare class GatewayUnreachableError extends GatewayStoreError {
|
|
42
|
-
}
|
|
43
|
-
/** What `GET {endpoint}/health` answers with a valid credential. */
|
|
44
|
-
export interface GatewayHealth {
|
|
45
|
-
projectId: string;
|
|
46
|
-
branchName: string;
|
|
47
|
-
dataset: string;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* An {@link IDataset} that forwards every Linked Query to a gateway over HTTP and returns
|
|
51
|
-
* the gateway's result as is.
|
|
52
|
-
*
|
|
53
|
-
* Each query method posts `{v, op, query: query.toJSON()}` to `POST {endpoint}/query` with
|
|
54
|
-
* `Authorization: Bearer <credential>` and resolves with the body's `result` — the same plain
|
|
55
|
-
* JSON rows `BackendAPIStore` returns. The gateway decides which dataset the query runs on
|
|
56
|
-
* from the credential; the store carries no project, branch or actor.
|
|
57
|
-
*
|
|
58
|
-
* Unlike `BackendAPIStore` it does not go through `Server.call` and has no dependency on
|
|
59
|
-
* the process's own HTTP server: it is meant for a backend whose data lives behind another
|
|
60
|
-
* service, selected from `linked.backend.datasets.json` by the host that started it.
|
|
61
|
-
*
|
|
62
|
-
* The store offers no raw SPARQL (`rawQuery`), no dataset administration and no change feed.
|
|
63
|
-
*/
|
|
64
|
-
export declare class GatewayStore implements IDataset {
|
|
65
|
-
readonly endpoint: string;
|
|
66
|
-
private readonly credential;
|
|
67
|
-
private readonly fetchImpl;
|
|
68
|
-
constructor(config: GatewayStoreConfig);
|
|
69
|
-
/**
|
|
70
|
-
* Checks the gateway answers for this credential: `GET {endpoint}/health`. Resolves with the
|
|
71
|
-
* gateway's answer; throws {@link GatewayUnreachableError} when the gateway cannot be reached
|
|
72
|
-
* or refuses the credential. Safe to call more than once; it changes nothing on either side.
|
|
73
|
-
*
|
|
74
|
-
* `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must
|
|
75
|
-
* fail loudly awaits `init()` itself before registering the store.
|
|
76
|
-
*/
|
|
77
|
-
init(): Promise<GatewayHealth>;
|
|
78
|
-
selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number>;
|
|
79
|
-
askQuery(query: AskQuery): Promise<boolean>;
|
|
80
|
-
updateQuery(query: UpdateQuery): Promise<UpdateResult>;
|
|
81
|
-
createQuery(query: CreateQuery): Promise<CreateResult>;
|
|
82
|
-
deleteQuery(query: DeleteQuery): Promise<DeleteResponse>;
|
|
83
|
-
private send;
|
|
84
|
-
private request;
|
|
85
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/** The wire version this store speaks. The gateway refuses any other with `code: "wire-version"`. */
|
|
2
|
-
export const GATEWAY_WIRE_VERSION = 1;
|
|
3
|
-
/**
|
|
4
|
-
* A non-2xx answer from the gateway. `code` is the body's `code` when the gateway sent one
|
|
5
|
-
* (`bad-request`, `wire-version`, `unknown-shape`, `refused`, `fork-failed`, `query-failed`),
|
|
6
|
-
* else a generic one; `body` is the parsed response when it was JSON.
|
|
7
|
-
*/
|
|
8
|
-
export class GatewayStoreError extends Error {
|
|
9
|
-
constructor(message, status, code, body) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = new.target.name;
|
|
12
|
-
this.status = status;
|
|
13
|
-
this.code = code;
|
|
14
|
-
this.body = body;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* The gateway could not be used at all: the request never got an HTTP answer (network error,
|
|
19
|
-
* `status` 0), or the gateway refused the credential (401). Thrown by `init()` and by every
|
|
20
|
-
* query method; a process that gets this at boot should stop rather than carry on without data.
|
|
21
|
-
*/
|
|
22
|
-
export class GatewayUnreachableError extends GatewayStoreError {
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* An {@link IDataset} that forwards every Linked Query to a gateway over HTTP and returns
|
|
26
|
-
* the gateway's result as is.
|
|
27
|
-
*
|
|
28
|
-
* Each query method posts `{v, op, query: query.toJSON()}` to `POST {endpoint}/query` with
|
|
29
|
-
* `Authorization: Bearer <credential>` and resolves with the body's `result` — the same plain
|
|
30
|
-
* JSON rows `BackendAPIStore` returns. The gateway decides which dataset the query runs on
|
|
31
|
-
* from the credential; the store carries no project, branch or actor.
|
|
32
|
-
*
|
|
33
|
-
* Unlike `BackendAPIStore` it does not go through `Server.call` and has no dependency on
|
|
34
|
-
* the process's own HTTP server: it is meant for a backend whose data lives behind another
|
|
35
|
-
* service, selected from `linked.backend.datasets.json` by the host that started it.
|
|
36
|
-
*
|
|
37
|
-
* The store offers no raw SPARQL (`rawQuery`), no dataset administration and no change feed.
|
|
38
|
-
*/
|
|
39
|
-
export class GatewayStore {
|
|
40
|
-
constructor(config) {
|
|
41
|
-
var _a;
|
|
42
|
-
const endpoint = typeof (config === null || config === void 0 ? void 0 : config.endpoint) === 'string' ? config.endpoint.trim() : '';
|
|
43
|
-
const credential = typeof (config === null || config === void 0 ? void 0 : config.credential) === 'string' ? config.credential.trim() : '';
|
|
44
|
-
if (!endpoint) {
|
|
45
|
-
throw new Error('GatewayStore: `endpoint` is empty. A gateway store needs the URL of the gateway ' +
|
|
46
|
-
'(the host that started this process sets it, e.g. through CN_EG_GATEWAY).');
|
|
47
|
-
}
|
|
48
|
-
if (!credential) {
|
|
49
|
-
throw new Error('GatewayStore: `credential` is empty. A gateway store needs the bearer credential ' +
|
|
50
|
-
'the gateway issued to this process (e.g. CN_APP_SERVICE_SECRET).');
|
|
51
|
-
}
|
|
52
|
-
const fetchImpl = (_a = config.fetch) !== null && _a !== void 0 ? _a : globalThis.fetch;
|
|
53
|
-
if (typeof fetchImpl !== 'function') {
|
|
54
|
-
throw new Error('GatewayStore: no `fetch` available; pass one in the config.');
|
|
55
|
-
}
|
|
56
|
-
this.endpoint = endpoint.replace(/\/+$/, '');
|
|
57
|
-
this.credential = credential;
|
|
58
|
-
this.fetchImpl = fetchImpl;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Checks the gateway answers for this credential: `GET {endpoint}/health`. Resolves with the
|
|
62
|
-
* gateway's answer; throws {@link GatewayUnreachableError} when the gateway cannot be reached
|
|
63
|
-
* or refuses the credential. Safe to call more than once; it changes nothing on either side.
|
|
64
|
-
*
|
|
65
|
-
* `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must
|
|
66
|
-
* fail loudly awaits `init()` itself before registering the store.
|
|
67
|
-
*/
|
|
68
|
-
async init() {
|
|
69
|
-
return (await this.request('GET', '/health'));
|
|
70
|
-
}
|
|
71
|
-
selectQuery(query) {
|
|
72
|
-
return this.send('select', query.toJSON());
|
|
73
|
-
}
|
|
74
|
-
askQuery(query) {
|
|
75
|
-
return this.send('ask', query.toJSON());
|
|
76
|
-
}
|
|
77
|
-
updateQuery(query) {
|
|
78
|
-
return this.send('update', query.toJSON());
|
|
79
|
-
}
|
|
80
|
-
createQuery(query) {
|
|
81
|
-
return this.send('create', query.toJSON());
|
|
82
|
-
}
|
|
83
|
-
deleteQuery(query) {
|
|
84
|
-
return this.send('delete', query.toJSON());
|
|
85
|
-
}
|
|
86
|
-
async send(op, query) {
|
|
87
|
-
const body = (await this.request('POST', '/query', {
|
|
88
|
-
v: GATEWAY_WIRE_VERSION,
|
|
89
|
-
op,
|
|
90
|
-
query,
|
|
91
|
-
}));
|
|
92
|
-
if (!body || typeof body !== 'object' || !('result' in body)) {
|
|
93
|
-
throw new GatewayStoreError(`GatewayStore: the gateway answered ${op} without a result`, 200, 'bad-response', body);
|
|
94
|
-
}
|
|
95
|
-
return body.result;
|
|
96
|
-
}
|
|
97
|
-
async request(method, route, json) {
|
|
98
|
-
const url = this.endpoint + route;
|
|
99
|
-
const headers = {
|
|
100
|
-
Authorization: `Bearer ${this.credential}`,
|
|
101
|
-
Accept: 'application/json',
|
|
102
|
-
};
|
|
103
|
-
const init = { method, headers };
|
|
104
|
-
if (json !== undefined) {
|
|
105
|
-
headers['Content-Type'] = 'application/json';
|
|
106
|
-
init.body = JSON.stringify(json);
|
|
107
|
-
}
|
|
108
|
-
let response;
|
|
109
|
-
try {
|
|
110
|
-
response = await this.fetchImpl(url, init);
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
const cause = err instanceof Error ? err.message : String(err);
|
|
114
|
-
throw new GatewayUnreachableError(`GatewayStore: cannot reach the gateway at ${url}: ${cause}`, 0, 'unreachable');
|
|
115
|
-
}
|
|
116
|
-
const text = await response.text();
|
|
117
|
-
let body = undefined;
|
|
118
|
-
if (text) {
|
|
119
|
-
try {
|
|
120
|
-
body = JSON.parse(text);
|
|
121
|
-
}
|
|
122
|
-
catch (_a) {
|
|
123
|
-
body = text;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (response.ok)
|
|
127
|
-
return body;
|
|
128
|
-
const code = body && typeof body === 'object' && typeof body.code === 'string'
|
|
129
|
-
? body.code
|
|
130
|
-
: `http-${response.status}`;
|
|
131
|
-
const detail = body && typeof body === 'object' && typeof body.error === 'string'
|
|
132
|
-
? body.error
|
|
133
|
-
: typeof body === 'string' && body
|
|
134
|
-
? body
|
|
135
|
-
: response.statusText;
|
|
136
|
-
const message = `GatewayStore: ${method} ${url} answered ${response.status} (${code})${detail ? `: ${detail}` : ''}`;
|
|
137
|
-
if (response.status === 401) {
|
|
138
|
-
throw new GatewayUnreachableError(message, 401, code, body);
|
|
139
|
-
}
|
|
140
|
-
throw new GatewayStoreError(message, response.status, code, body);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
//# sourceMappingURL=GatewayStore.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GatewayStore.js","sourceRoot":"","sources":["../../../../src/shapes/quadstores/GatewayStore.ts"],"names":[],"mappings":"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","sourcesContent":["import 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 GatewayStore(config)` — the `config` object of a\n * `linked.backend.datasets.json` entry, passed verbatim by `loadStores`.\n */\nexport interface GatewayStoreConfig {\n /** Base URL of the gateway, e.g. `http://localhost:4040/gateway`. Routes are appended to it. */\n endpoint: string;\n /** The bearer credential the gateway issued to this process. */\n credential: string;\n /** Transport override; defaults to `globalThis.fetch`. */\n fetch?: typeof fetch;\n}\n\n/** The op names on the wire; each is the `IDataset` method the store was called on. */\nexport type GatewayOp = 'select' | 'ask' | 'update' | 'create' | 'delete';\n\n/** The wire version this store speaks. The gateway refuses any other with `code: \"wire-version\"`. */\nexport const GATEWAY_WIRE_VERSION = 1;\n\n/**\n * A non-2xx answer from the gateway. `code` is the body's `code` when the gateway sent one\n * (`bad-request`, `wire-version`, `unknown-shape`, `refused`, `fork-failed`, `query-failed`),\n * else a generic one; `body` is the parsed response when it was JSON.\n */\nexport class GatewayStoreError extends Error {\n readonly status: number;\n readonly code: string;\n readonly body?: unknown;\n\n constructor(message: string, status: number, code: string, body?: unknown) {\n super(message);\n this.name = new.target.name;\n this.status = status;\n this.code = code;\n this.body = body;\n }\n}\n\n/**\n * The gateway could not be used at all: the request never got an HTTP answer (network error,\n * `status` 0), or the gateway refused the credential (401). Thrown by `init()` and by every\n * query method; a process that gets this at boot should stop rather than carry on without data.\n */\nexport class GatewayUnreachableError extends GatewayStoreError {}\n\n/** What `GET {endpoint}/health` answers with a valid credential. */\nexport interface GatewayHealth {\n projectId: string;\n branchName: string;\n dataset: string;\n}\n\n/**\n * An {@link IDataset} that forwards every Linked Query to a gateway over HTTP and returns\n * the gateway's result as is.\n *\n * Each query method posts `{v, op, query: query.toJSON()}` to `POST {endpoint}/query` with\n * `Authorization: Bearer <credential>` and resolves with the body's `result` — the same plain\n * JSON rows `BackendAPIStore` returns. The gateway decides which dataset the query runs on\n * from the credential; the store carries no project, branch or actor.\n *\n * Unlike `BackendAPIStore` it does not go through `Server.call` and has no dependency on\n * the process's own HTTP server: it is meant for a backend whose data lives behind another\n * service, selected from `linked.backend.datasets.json` by the host that started it.\n *\n * The store offers no raw SPARQL (`rawQuery`), no dataset administration and no change feed.\n */\nexport class GatewayStore implements IDataset {\n readonly endpoint: string;\n private readonly credential: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(config: GatewayStoreConfig) {\n const endpoint = typeof config?.endpoint === 'string' ? config.endpoint.trim() : '';\n const credential = typeof config?.credential === 'string' ? config.credential.trim() : '';\n if (!endpoint) {\n throw new Error(\n 'GatewayStore: `endpoint` is empty. A gateway store needs the URL of the gateway ' +\n '(the host that started this process sets it, e.g. through CN_EG_GATEWAY).',\n );\n }\n if (!credential) {\n throw new Error(\n 'GatewayStore: `credential` is empty. A gateway store needs the bearer credential ' +\n 'the gateway issued to this process (e.g. CN_APP_SERVICE_SECRET).',\n );\n }\n const fetchImpl = config.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error('GatewayStore: no `fetch` available; pass one in the config.');\n }\n this.endpoint = endpoint.replace(/\\/+$/, '');\n this.credential = credential;\n this.fetchImpl = fetchImpl;\n }\n\n /**\n * Checks the gateway answers for this credential: `GET {endpoint}/health`. Resolves with the\n * gateway's answer; throws {@link GatewayUnreachableError} when the gateway cannot be reached\n * or refuses the credential. Safe to call more than once; it changes nothing on either side.\n *\n * `LinkedStorage.setDefaultDataset` calls `init()` without awaiting it, so a boot that must\n * fail loudly awaits `init()` itself before registering the store.\n */\n async init(): Promise<GatewayHealth> {\n return (await this.request('GET', '/health')) as GatewayHealth;\n }\n\n selectQuery(query: SelectQuery | CountQuery): Promise<SelectResult | number> {\n return this.send('select', query.toJSON());\n }\n\n askQuery(query: AskQuery): Promise<boolean> {\n return this.send('ask', query.toJSON());\n }\n\n updateQuery(query: UpdateQuery): Promise<UpdateResult> {\n return this.send('update', query.toJSON());\n }\n\n createQuery(query: CreateQuery): Promise<CreateResult> {\n return this.send('create', query.toJSON());\n }\n\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n return this.send('delete', query.toJSON());\n }\n\n private async send<T>(op: GatewayOp, query: unknown): Promise<T> {\n const body = (await this.request('POST', '/query', {\n v: GATEWAY_WIRE_VERSION,\n op,\n query,\n })) as { result?: T } | null;\n if (!body || typeof body !== 'object' || !('result' in body)) {\n throw new GatewayStoreError(\n `GatewayStore: the gateway answered ${op} without a result`,\n 200,\n 'bad-response',\n body,\n );\n }\n return body.result as T;\n }\n\n private async request(method: 'GET' | 'POST', route: string, json?: unknown): Promise<unknown> {\n const url = this.endpoint + route;\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.credential}`,\n Accept: 'application/json',\n };\n const init: RequestInit = { method, headers };\n if (json !== undefined) {\n headers['Content-Type'] = 'application/json';\n init.body = JSON.stringify(json);\n }\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (err) {\n const cause = err instanceof Error ? err.message : String(err);\n throw new GatewayUnreachableError(\n `GatewayStore: cannot reach the gateway at ${url}: ${cause}`,\n 0,\n 'unreachable',\n );\n }\n\n const text = await response.text();\n let body: unknown = undefined;\n if (text) {\n try {\n body = JSON.parse(text);\n } catch {\n body = text;\n }\n }\n\n if (response.ok) return body;\n\n const code =\n body && typeof body === 'object' && typeof (body as any).code === 'string'\n ? ((body as any).code as string)\n : `http-${response.status}`;\n const detail =\n body && typeof body === 'object' && typeof (body as any).error === 'string'\n ? (body as any).error\n : typeof body === 'string' && body\n ? body\n : response.statusText;\n const message = `GatewayStore: ${method} ${url} answered ${response.status} (${code})${\n detail ? `: ${detail}` : ''\n }`;\n\n if (response.status === 401) {\n throw new GatewayUnreachableError(message, 401, code, body);\n }\n throw new GatewayStoreError(message, response.status, code, body);\n }\n}\n"]}
|