@_linked/server 2.24.11 → 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 CHANGED
@@ -1,5 +1,26 @@
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
+
18
+ ## 2.25.0
19
+
20
+ ### Minor Changes
21
+
22
+ - [#177](https://github.com/linked-fw/server/pull/177) [`781e5d0`](https://github.com/linked-fw/server/commit/781e5d0f41a8fa7a2e0984f9740003968511c759) Thanks [@flyon](https://github.com/flyon)! - GatewayStore: an IDataset that forwards Linked Queries to a gateway. Exported at `@_linked/server/shapes/quadstores/GatewayStore` so an app can select it from `linked.backend.datasets.json`; it posts each query as DSL-JSON with a bearer credential and throws `GatewayUnreachableError` from `init()` when the gateway does not answer.
23
+
3
24
  ## 2.24.11
4
25
 
5
26
  ### Patch Changes
@@ -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>;
@@ -30,22 +30,25 @@ export default class LincdServerBackendProvider extends BackendProvider {
30
30
  });
31
31
  }
32
32
  }
33
- // ── BackendAPIStore query execution ──────────────────────────────────────
34
- //
35
- // These answer the frontend `BackendAPIStore`, which calls
36
- // `Server.call(packageName, {method}, json)` — the PACKAGE form, so the request
37
- // arrives here on the generic provider rather than through Shape resolution.
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
- // They used to live on `BackendAPIStoreProvider`, a `ShapeProvider`, whose methods
40
- // took the store as a first argument and never read it. Reaching that provider meant
41
- // the transport had to carry a `shapeURI` and an `instanceNode`, and the server had to
42
- // rebuild the store with `new (providerShapeClass)({id})` — instantiating a Shape only
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 (core 2.10.0 contract flip), so the client ships
46
- // `toJSON()` and `fromJSON()` rehydrates it here into a live, closed query.
47
- // `LinkedStorage` then routes it by the QUERY's shape — which is, and always was, what
48
- // decides the target dataset. The store instance never participated.
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),
@@ -1 +1 @@
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 // ── BackendAPIStore query execution ──────────────────────────────────────\n //\n // These answer the frontend `BackendAPIStore`, which calls\n // `Server.call(packageName, {method}, json)` — the PACKAGE form, so the request\n // arrives here on the generic provider rather than through Shape resolution.\n //\n // They used to live on `BackendAPIStoreProvider`, a `ShapeProvider`, whose methods\n // took the store as a first argument and never read it. Reaching that provider meant\n // the transport had to carry a `shapeURI` and an `instanceNode`, and the server had to\n // rebuild the store with `new (providerShapeClass)({id})` — instantiating a Shape only\n // to discard it. Addressing by package removes all of that.\n //\n // A query cannot cross the wire live (core 2.10.0 contract flip), so the client ships\n // `toJSON()` and `fromJSON()` rehydrates it here into a live, closed query.\n // `LinkedStorage` then routes it by the QUERY's shape — which is, and always was, what\n // decides the target dataset. The store instance never participated.\n\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"]}
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"]}
@@ -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/BackendAPIStore.js';
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/BackendAPIStore.js';
6
+ import './shapes/quadstores/RemoteDataset.js';
7
7
  import './shapes/filestores/LocalFileStore.js';
8
8
  //THEN COMPONENTS
9
9
  import './utils/accessUrl.js';
@@ -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,wCAAwC,CAAC;AAChD,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/BackendAPIStore.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"]}
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":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AAoC5D;;;GAGG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAQ3C,YAAY,OAAe,EAAE,GAAW,EAAE,MAAc,EAAE,IAAa;QACrE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAC3C,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,6BAA8B,SAAQ,kBAAkB;CAAG;AAExE,4EAA4E;AAC5E,MAAM,OAAO,8BAA+B,SAAQ,kBAAkB;CAAG;AAUzE,gFAAgF;AAChF,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAErE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,aAAa;IAWxB,YAAY,MAAqC;QAC/C,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,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACjD,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1E,IAAI,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QAChC,CAAC;QACD,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,sBAAsB,MAAM,CAAC,IAAI,EAAE,CAAC;QACxE,CAAC;IACH,CAAC;IAED,IAAI;QACF,6EAA6E;QAC7E,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QACrD,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAC3C,GAAG,EAAE,CAAC,SAAS,EACf,CAAC,GAAG,EAAE,EAAE;gBACN,4CAA4C;gBAC5C,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;gBAC7B,MAAM,GAAG,CAAC;YACZ,CAAC,CACF,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,yFAAyF;IACzF,oFAAoF;IACpF,gBAAgB;IAChB,WAAW,CAAC,KAA+B;QACzC,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,QAAQ,CAAC,KAAe;QACtB,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,WAAW,CAAC,KAAkB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAEO,IAAI,CAAI,MAAc,EAAE,IAAa;QAC3C,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAe,CAAC;IACjD,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;IAED;;;;OAIG;IACK,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,IAAe;;QAChD,MAAM,IAAI,GAAG,MAAA,IAAI,CAAC,GAAG,mCAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,6BAA6B,CACrC,kFAAkF,MAAM,EAAE,EAC1F,EAAE,EACF,CAAC,CACF,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,IAAI,SAAS,WAAW,IAAI,MAAM,EAAE,CAAC;QACvD,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;SAC3B,CAAC;QACF,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,aAAa,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAC;QAE/D,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE;gBACxC,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;aAC/B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC/D,MAAM,IAAI,6BAA6B,CACrC,kDAAkD,IAAI,KAAK,MAAM,MAAM,KAAK,EAAE,EAC9E,IAAI,EACJ,CAAC,CACF,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,IAAI,GAAY,SAAS,CAAC;QAC9B,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACxB,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;YAAC,WAAM,CAAC;gBACP,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,IAAI,MAAM;gBAAE,OAAO,IAAI,CAAC;YACxB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACtB,MAAM,IAAI,6BAA6B,CACrC,qCAAqC,IAAI,6CAA6C,EACtF,IAAI,EACJ,QAAQ,CAAC,MAAM,CAChB,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,IAAI;gBAAE,OAAO,SAAS,CAAC;YAC5B,MAAM,IAAI,kBAAkB,CAC1B,qCAAqC,IAAI,aAAa,MAAM,+BAA+B,EAC3F,IAAI,EACJ,QAAQ,CAAC,MAAM,CAChB,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAgC,CAAC,CAAC,CAAC,SAAS,CAAC;QAChG,MAAM,IAAI,GAAG,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAA,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACxE,MAAM,MAAM,GACV,OAAO,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,CAAA,KAAK,QAAQ;YAC/B,CAAC,CAAC,MAAM,CAAC,KAAK;YACd,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI;gBAChC,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC;QAC5B,MAAM,OAAO,GAAG,qCAAqC,IAAI,aAAa,MAAM,SAAS,QAAQ,CAAC,MAAM,GAClG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EACxB,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAEjC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvD,MAAM,IAAI,8BAA8B,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACrF,MAAM,IAAI,6BAA6B,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,IAAI,kBAAkB,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrE,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 { 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
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/server.git"
12
12
  },
13
- "version": "2.24.11",
13
+ "version": "3.0.0",
14
14
  "packageManager": "npm@12.2.0",
15
15
  "linkedPackage": true,
16
16
  "type": "module",
package/readme.md CHANGED
@@ -19,6 +19,10 @@ If you want to further adjust the functionality of LincdServer yourself, either
19
19
  - [Generic backend providers](#generic-backend-providers)
20
20
  - [Gotchas](#gotchas)
21
21
  - [Error semantics](#error-semantics)
22
+ - [Query stores](#query-stores)
23
+ - [Without `url`](#without-url)
24
+ - [With `url`](#with-url)
25
+ - [`ping`](#ping)
22
26
  - [API-only servers (`server.apiOnly`)](#api-only-servers-serverapionly)
23
27
  - [Provider lifecycle and HMR](#provider-lifecycle-and-hmr)
24
28
  - [When a provider throws, and refusing to start](#when-a-provider-throws-and-refusing-to-start)
@@ -230,7 +234,86 @@ What the caller sees depends on how it calls:
230
234
  - **`Server.call` on the backend.** Here it calls the `LinkedServer` directly.
231
235
  - A provider method that throws always rejects.
232
236
  - An unmatched call resolves `undefined` by default and rejects with a 501 `ServerCallError` when `rejectOnError` is set.
233
- - **`BackendAPIStore`.** 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
+ - **`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.
238
+
239
+ ## Query stores
240
+
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.
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` |
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
+ ```
264
+
265
+ `new RemoteDataset('main')` is the `name` form.
266
+
267
+ ### Without `url`
268
+
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.
272
+
273
+ ### With `url`
274
+
275
+ ```json
276
+ {
277
+ "datasets": {
278
+ "appData": {
279
+ "store": "@_linked/server/shapes/quadstores/RemoteDataset",
280
+ "config": { "url": "${DATA_SERVER_URL:-}", "token": "${DATA_SERVER_TOKEN:-}" }
281
+ }
282
+ }
283
+ }
284
+ ```
285
+
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.
234
317
 
235
318
  ## API-only servers (`server.apiOnly`)
236
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"]}