@_linked/server 3.12.1 → 3.14.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 +72 -0
- package/lib/esm/backend.d.ts +23 -5
- package/lib/esm/backend.js +116 -25
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/shapes/LincdAPI.d.ts +1 -1
- package/lib/esm/shapes/LincdAPI.js +43 -7
- package/lib/esm/shapes/LincdAPI.js.map +1 -1
- package/lib/esm/shapes/LinkedServer.d.ts +59 -39
- package/lib/esm/shapes/LinkedServer.js +158 -93
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +52 -20
- package/lib/esm/shapes/quadstores/RemoteDataset.js +134 -32
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +23 -51
- package/lib/esm/utils/adminChannel.js +50 -38
- package/lib/esm/utils/adminChannel.js.map +1 -1
- package/lib/esm/utils/effectiveRoutes.d.ts +6 -0
- package/lib/esm/utils/effectiveRoutes.js +2 -1
- package/lib/esm/utils/effectiveRoutes.js.map +1 -1
- package/lib/esm/utils/forwardedTransactions.d.ts +46 -0
- package/lib/esm/utils/forwardedTransactions.js +155 -0
- package/lib/esm/utils/forwardedTransactions.js.map +1 -0
- package/lib/esm/utils/graphTargets.d.ts +42 -17
- package/lib/esm/utils/graphTargets.js +62 -69
- package/lib/esm/utils/graphTargets.js.map +1 -1
- package/lib/esm/utils/linkedDependencies.d.ts +53 -7
- package/lib/esm/utils/linkedDependencies.js +130 -12
- package/lib/esm/utils/linkedDependencies.js.map +1 -1
- package/lib/esm/utils/ontologySync.d.ts +55 -31
- package/lib/esm/utils/ontologySync.js +94 -42
- package/lib/esm/utils/ontologySync.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +13 -0
- package/lib/esm/utils/queryPlane.js +49 -2
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/lib/esm/utils/reloadSource.js +1 -1
- package/lib/esm/utils/reloadSource.js.map +1 -1
- package/lib/esm/utils/serverTransactions.d.ts +22 -0
- package/lib/esm/utils/serverTransactions.js +81 -0
- package/lib/esm/utils/serverTransactions.js.map +1 -0
- package/lib/esm/utils/syncShapes.d.ts +123 -6
- package/lib/esm/utils/syncShapes.js +166 -35
- package/lib/esm/utils/syncShapes.js.map +1 -1
- package/package.json +4 -4
- package/readme.md +158 -38
|
@@ -51,6 +51,19 @@ export declare function resolveQueryCatalog(query: unknown): Promise<ShapeCatalo
|
|
|
51
51
|
* from the same builder.
|
|
52
52
|
*/
|
|
53
53
|
export declare function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any;
|
|
54
|
+
/** The access rule a read is checked under: a select and a count under `select`, an ask under `ask`. */
|
|
55
|
+
export type ReadOperation = Extract<QueryOperation, 'select' | 'ask'>;
|
|
56
|
+
/**
|
|
57
|
+
* Decode a body posted to `readQuery` into the builder the store runs, with the access rule it is
|
|
58
|
+
* checked under. The body's kind is read before it is decoded (so a body `fromJSON` cannot decode
|
|
59
|
+
* is refused as what it is, with 400), the builder is decoded in `catalog` as
|
|
60
|
+
* {@link toQueryBuilder} does, and the decoded builder must be of the kind the body said (else
|
|
61
|
+
* 400). The rule follows that kind: a select body is never checked as an ask, nor the reverse.
|
|
62
|
+
*/
|
|
63
|
+
export declare function toReadQuery(json: unknown, catalog?: ShapeCatalog): {
|
|
64
|
+
query: any;
|
|
65
|
+
operation: ReadOperation;
|
|
66
|
+
};
|
|
54
67
|
/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */
|
|
55
68
|
export declare function lowerForAccessCheck(builder: any): unknown;
|
|
56
69
|
/**
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authorization for the generic query plane: the endpoints that run a query the
|
|
3
|
-
* client built (`LincdServerBackendProvider
|
|
4
|
-
* `/call/@_linked/server
|
|
3
|
+
* client built (`LincdServerBackendProvider`'s `readQuery`, `createQuery`, `updateQuery`,
|
|
4
|
+
* `deleteQuery` behind `/call/@_linked/server/<method>`, and `LincdAPI` `post_*` behind `/api/*`).
|
|
5
|
+
*
|
|
6
|
+
* Every read (select, count, ask) arrives on `readQuery`: {@link toReadQuery} tells its kind from
|
|
7
|
+
* the body before decoding it, and the kind (never the endpoint) picks the access rule the read is
|
|
8
|
+
* checked under.
|
|
5
9
|
*
|
|
6
10
|
* The rules are declared per store in the app's storage config
|
|
7
11
|
* (`linked.backend.storage.js`) with `withAccess` from
|
|
@@ -25,6 +29,7 @@
|
|
|
25
29
|
import { fromJSON } from '@_linked/core';
|
|
26
30
|
import { isGraphQueryError, parseGraphQuery, } from '@_linked/core/queries/graphQuery';
|
|
27
31
|
import { lower } from '@_linked/core/queries/lower';
|
|
32
|
+
import { readKindOf, readKindOfJSON } from '@_linked/core/queries/readQuery';
|
|
28
33
|
import { checkQueryAccess, } from '@_linked/server-utils/utils/QueryAccess';
|
|
29
34
|
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
30
35
|
import { getRawQueriesMode } from './rawQueries.js';
|
|
@@ -139,6 +144,48 @@ export function toQueryBuilder(query, catalog) {
|
|
|
139
144
|
throw new ServerCallError(400, 'Invalid query');
|
|
140
145
|
}
|
|
141
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The kind of a read body: `readKindOf` of a live builder (backend code), else `readKindOfJSON` of
|
|
149
|
+
* the wire form. Anything that is not a read answers 400: a write, a graph query (it goes to
|
|
150
|
+
* `graphQuery`), an unknown `op` or `kind`, a body that is not an object. So are triples and terms
|
|
151
|
+
* reads, which this server does not answer.
|
|
152
|
+
*/
|
|
153
|
+
function readBodyKind(json) {
|
|
154
|
+
let kind;
|
|
155
|
+
try {
|
|
156
|
+
kind = isLiveBuilder(json) ? readKindOf(json) : readKindOfJSON(json);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// Core's message quotes the client's `op` or `kind`: capped, so a body is never echoed whole.
|
|
160
|
+
const message = error instanceof Error ? error.message : 'Invalid query';
|
|
161
|
+
throw new ServerCallError(400, message.length > 200 ? `${message.slice(0, 200)}…` : message);
|
|
162
|
+
}
|
|
163
|
+
if (kind === 'triples' || kind === 'triples_terms') {
|
|
164
|
+
throw new ServerCallError(400, `This server's readQuery does not answer ${kind === 'triples' ? 'triples' : 'terms'} reads`);
|
|
165
|
+
}
|
|
166
|
+
return kind;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Decode a body posted to `readQuery` into the builder the store runs, with the access rule it is
|
|
170
|
+
* checked under. The body's kind is read before it is decoded (so a body `fromJSON` cannot decode
|
|
171
|
+
* is refused as what it is, with 400), the builder is decoded in `catalog` as
|
|
172
|
+
* {@link toQueryBuilder} does, and the decoded builder must be of the kind the body said (else
|
|
173
|
+
* 400). The rule follows that kind: a select body is never checked as an ask, nor the reverse.
|
|
174
|
+
*/
|
|
175
|
+
export function toReadQuery(json, catalog) {
|
|
176
|
+
const kind = readBodyKind(json);
|
|
177
|
+
const query = toQueryBuilder(json, catalog);
|
|
178
|
+
let decoded;
|
|
179
|
+
try {
|
|
180
|
+
decoded = readKindOf(query);
|
|
181
|
+
}
|
|
182
|
+
catch (_a) {
|
|
183
|
+
decoded = undefined;
|
|
184
|
+
}
|
|
185
|
+
if (decoded !== kind)
|
|
186
|
+
throw new ServerCallError(400, 'Invalid query');
|
|
187
|
+
return { query, operation: kind === 'ask' ? 'ask' : 'select' };
|
|
188
|
+
}
|
|
142
189
|
/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */
|
|
143
190
|
export function lowerForAccessCheck(builder) {
|
|
144
191
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * Graph queries (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`) arrive as plain\n * data on `/call/@_linked/server/graphQuery`: {@link toGraphQuery} checks the JSON, the store's\n * `graph` rule decides ({@link authorizeGraphQuery}), and a refusal of the store goes back with\n * its code ({@link graphQueryCallError}).\n *\n * The host can supply the shape catalog a request's query is decoded in\n * (`setQueryCatalogResolver`), so one server can resolve the same shape IRI\n * differently per request (for example per project branch).\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport {\n isGraphQueryError,\n parseGraphQuery,\n type GraphQuery,\n type GraphQueryErrorCode,\n} from '@_linked/core/queries/graphQuery';\nimport { lower } from '@_linked/core/queries/lower';\nimport type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * Supplies the shape catalog a generic query request is decoded in, from the\n * DSL-JSON the client sent (the request context is available through the\n * host's own means, e.g. `getCallContext`). `undefined` means the global\n * shape registry.\n */\nexport type QueryCatalogResolver = (\n query: unknown\n) => ShapeCatalog | undefined | Promise<ShapeCatalog | undefined>;\n\n/**\n * The host's resolver, shared by every copy of this module in the process: the host sets it\n * through one copy, and the query endpoint served by another must use it rather than decode in\n * the global registry. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst queryPlaneState = sharedState('@_linked/server', 'queryPlane', () => ({\n catalogResolver: undefined as QueryCatalogResolver | undefined,\n}));\n\n/**\n * Set (or, with `undefined`, clear) the host's catalog resolver for the\n * generic query endpoints. Without one, every query decodes in the global\n * shape registry.\n */\nexport function setQueryCatalogResolver(resolver: QueryCatalogResolver | undefined): void {\n queryPlaneState.catalogResolver = resolver;\n}\n\n/** A live builder passed by backend code (never a plain object from JSON). */\nfunction isLiveBuilder(query: any): boolean {\n return (\n !!query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n );\n}\n\n/**\n * The catalog `query` is decoded in: the host resolver's answer, or\n * `undefined` (the global registry) without a resolver. A live builder already\n * carries its catalog, so the resolver is not asked for it.\n *\n * A `ServerCallError` the resolver throws is passed on as is (the host chose\n * the status); any other failure is logged and answers 500.\n */\nexport async function resolveQueryCatalog(query: unknown): Promise<ShapeCatalog | undefined> {\n const resolver = queryPlaneState.catalogResolver;\n if (!resolver || isLiveBuilder(query)) return undefined;\n try {\n return await resolver(query);\n } catch (error) {\n if (ServerCallError.is(error)) throw error;\n console.error('[queryPlane] the shape catalog resolver failed:', error);\n throw new ServerCallError(500, 'Shape catalog unavailable');\n }\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent, decoded in\n * `catalog` (`undefined`: the global registry, whatever catalog is active;\n * see `resolveQueryCatalog`). The builder keeps that catalog, so\n * the access-check lowering and the store's lowering both resolve against it.\n * The result is what the store runs, so the access check and the store see\n * the same query. A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and its shape\n * catalog: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any {\n if (isLiveBuilder(query)) return query;\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query as any, { catalog });\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/**\n * The graph query `json` describes, checked with core's `parseGraphQuery`; `undefined` when it is\n * not a valid graph query (the access check then refuses it with 400).\n */\nexport function toGraphQuery(json: unknown): GraphQuery | undefined {\n try {\n return parseGraphQuery(json);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check a graph query against the `graph` rule of the store `LinkedStorage.graphQuery` routes its\n * record class to (`QueryAccess`, operation `graph`): no rule, or `'none'`, refuses it (403, 401\n * without a session); a malformed one answers 400. `ir` is {@link toGraphQuery}'s result. No\n * catalog is resolved for a graph query (its record shapes are core's built-ins), so the check\n * resolves the store in the global registry, where `LinkedStorage.graphQuery` then runs it.\n */\nexport async function authorizeGraphQuery(\n query: unknown,\n ir: GraphQuery | undefined,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation: 'graph',\n query,\n ir,\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/**\n * The HTTP status each graph query refusal is answered with. A code this map does not know (a\n * newer core than the one this server was built against) is answered with 400, never 500.\n */\nconst GRAPH_REFUSAL_STATUS: Record<GraphQueryErrorCode, number> = {\n 'unsupported-query-kind': 501,\n 'graph-name-read-only': 400,\n 'graph-not-registered': 409,\n 'graph-exists': 409,\n 'graph-not-routed': 400,\n};\n\n/**\n * A store's graph query refusal (`GraphQueryError`) as the answer the caller gets: a\n * `ServerCallError` with the refusal's status and message and its `code`, which the call route\n * sends as `{error, code}` (`RemoteDataset` turns it back into a `GraphQueryError`). Any other\n * error is returned as is.\n */\nexport function graphQueryCallError(err: unknown): unknown {\n if (!isGraphQueryError(err)) return err;\n const code: string = err.code;\n const status = Object.prototype.hasOwnProperty.call(GRAPH_REFUSAL_STATUS, code)\n ? GRAPH_REFUSAL_STATUS[code as GraphQueryErrorCode]\n : 400;\n return Object.assign(new ServerCallError(status, err.message, { cause: err }), { code: err.code });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider`'s `readQuery`, `createQuery`, `updateQuery`,\n * `deleteQuery` behind `/call/@_linked/server/<method>`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * Every read (select, count, ask) arrives on `readQuery`: {@link toReadQuery} tells its kind from\n * the body before decoding it, and the kind (never the endpoint) picks the access rule the read is\n * checked under.\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * Graph queries (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`) arrive as plain\n * data on `/call/@_linked/server/graphQuery`: {@link toGraphQuery} checks the JSON, the store's\n * `graph` rule decides ({@link authorizeGraphQuery}), and a refusal of the store goes back with\n * its code ({@link graphQueryCallError}).\n *\n * The host can supply the shape catalog a request's query is decoded in\n * (`setQueryCatalogResolver`), so one server can resolve the same shape IRI\n * differently per request (for example per project branch).\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport {\n isGraphQueryError,\n parseGraphQuery,\n type GraphQuery,\n type GraphQueryErrorCode,\n} from '@_linked/core/queries/graphQuery';\nimport { lower } from '@_linked/core/queries/lower';\nimport { readKindOf, readKindOfJSON, type ReadKind } from '@_linked/core/queries/readQuery';\nimport type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * Supplies the shape catalog a generic query request is decoded in, from the\n * DSL-JSON the client sent (the request context is available through the\n * host's own means, e.g. `getCallContext`). `undefined` means the global\n * shape registry.\n */\nexport type QueryCatalogResolver = (\n query: unknown\n) => ShapeCatalog | undefined | Promise<ShapeCatalog | undefined>;\n\n/**\n * The host's resolver, shared by every copy of this module in the process: the host sets it\n * through one copy, and the query endpoint served by another must use it rather than decode in\n * the global registry. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst queryPlaneState = sharedState('@_linked/server', 'queryPlane', () => ({\n catalogResolver: undefined as QueryCatalogResolver | undefined,\n}));\n\n/**\n * Set (or, with `undefined`, clear) the host's catalog resolver for the\n * generic query endpoints. Without one, every query decodes in the global\n * shape registry.\n */\nexport function setQueryCatalogResolver(resolver: QueryCatalogResolver | undefined): void {\n queryPlaneState.catalogResolver = resolver;\n}\n\n/** A live builder passed by backend code (never a plain object from JSON). */\nfunction isLiveBuilder(query: any): boolean {\n return (\n !!query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n );\n}\n\n/**\n * The catalog `query` is decoded in: the host resolver's answer, or\n * `undefined` (the global registry) without a resolver. A live builder already\n * carries its catalog, so the resolver is not asked for it.\n *\n * A `ServerCallError` the resolver throws is passed on as is (the host chose\n * the status); any other failure is logged and answers 500.\n */\nexport async function resolveQueryCatalog(query: unknown): Promise<ShapeCatalog | undefined> {\n const resolver = queryPlaneState.catalogResolver;\n if (!resolver || isLiveBuilder(query)) return undefined;\n try {\n return await resolver(query);\n } catch (error) {\n if (ServerCallError.is(error)) throw error;\n console.error('[queryPlane] the shape catalog resolver failed:', error);\n throw new ServerCallError(500, 'Shape catalog unavailable');\n }\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent, decoded in\n * `catalog` (`undefined`: the global registry, whatever catalog is active;\n * see `resolveQueryCatalog`). The builder keeps that catalog, so\n * the access-check lowering and the store's lowering both resolve against it.\n * The result is what the store runs, so the access check and the store see\n * the same query. A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and its shape\n * catalog: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any {\n if (isLiveBuilder(query)) return query;\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query as any, { catalog });\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The access rule a read is checked under: a select and a count under `select`, an ask under `ask`. */\nexport type ReadOperation = Extract<QueryOperation, 'select' | 'ask'>;\n\n/**\n * The kind of a read body: `readKindOf` of a live builder (backend code), else `readKindOfJSON` of\n * the wire form. Anything that is not a read answers 400: a write, a graph query (it goes to\n * `graphQuery`), an unknown `op` or `kind`, a body that is not an object. So are triples and terms\n * reads, which this server does not answer.\n */\nfunction readBodyKind(json: unknown): ReadKind {\n let kind: ReadKind;\n try {\n kind = isLiveBuilder(json) ? readKindOf(json as any) : readKindOfJSON(json);\n } catch (error) {\n // Core's message quotes the client's `op` or `kind`: capped, so a body is never echoed whole.\n const message = error instanceof Error ? error.message : 'Invalid query';\n throw new ServerCallError(400, message.length > 200 ? `${message.slice(0, 200)}…` : message);\n }\n if (kind === 'triples' || kind === 'triples_terms') {\n throw new ServerCallError(\n 400,\n `This server's readQuery does not answer ${kind === 'triples' ? 'triples' : 'terms'} reads`\n );\n }\n return kind;\n}\n\n/**\n * Decode a body posted to `readQuery` into the builder the store runs, with the access rule it is\n * checked under. The body's kind is read before it is decoded (so a body `fromJSON` cannot decode\n * is refused as what it is, with 400), the builder is decoded in `catalog` as\n * {@link toQueryBuilder} does, and the decoded builder must be of the kind the body said (else\n * 400). The rule follows that kind: a select body is never checked as an ask, nor the reverse.\n */\nexport function toReadQuery(\n json: unknown,\n catalog?: ShapeCatalog\n): { query: any; operation: ReadOperation } {\n const kind = readBodyKind(json);\n const query = toQueryBuilder(json, catalog);\n let decoded: ReadKind | undefined;\n try {\n decoded = readKindOf(query);\n } catch {\n decoded = undefined;\n }\n if (decoded !== kind) throw new ServerCallError(400, 'Invalid query');\n return { query, operation: kind === 'ask' ? 'ask' : 'select' };\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/**\n * The graph query `json` describes, checked with core's `parseGraphQuery`; `undefined` when it is\n * not a valid graph query (the access check then refuses it with 400).\n */\nexport function toGraphQuery(json: unknown): GraphQuery | undefined {\n try {\n return parseGraphQuery(json);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check a graph query against the `graph` rule of the store `LinkedStorage.graphQuery` routes its\n * record class to (`QueryAccess`, operation `graph`): no rule, or `'none'`, refuses it (403, 401\n * without a session); a malformed one answers 400. `ir` is {@link toGraphQuery}'s result. No\n * catalog is resolved for a graph query (its record shapes are core's built-ins), so the check\n * resolves the store in the global registry, where `LinkedStorage.graphQuery` then runs it.\n */\nexport async function authorizeGraphQuery(\n query: unknown,\n ir: GraphQuery | undefined,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation: 'graph',\n query,\n ir,\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/**\n * The HTTP status each graph query refusal is answered with. A code this map does not know (a\n * newer core than the one this server was built against) is answered with 400, never 500.\n */\nconst GRAPH_REFUSAL_STATUS: Record<GraphQueryErrorCode, number> = {\n 'unsupported-query-kind': 501,\n 'graph-name-read-only': 400,\n 'graph-not-registered': 409,\n 'graph-exists': 409,\n 'graph-not-routed': 400,\n};\n\n/**\n * A store's graph query refusal (`GraphQueryError`) as the answer the caller gets: a\n * `ServerCallError` with the refusal's status and message and its `code`, which the call route\n * sends as `{error, code}` (`RemoteDataset` turns it back into a `GraphQueryError`). Any other\n * error is returned as is.\n */\nexport function graphQueryCallError(err: unknown): unknown {\n if (!isGraphQueryError(err)) return err;\n const code: string = err.code;\n const status = Object.prototype.hasOwnProperty.call(GRAPH_REFUSAL_STATUS, code)\n ? GRAPH_REFUSAL_STATUS[code as GraphQueryErrorCode]\n : 400;\n return Object.assign(new ServerCallError(status, err.message, { cause: err }), { code: err.code });\n}\n"]}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `ssrLoadModule` of the package reads them from disk again.
|
|
4
4
|
*
|
|
5
5
|
* Vite's watcher does this too when a file changes, but asynchronously: a host
|
|
6
|
-
* that has just written a file and asks the app
|
|
6
|
+
* that has just written a file and asks the app for its graph content cannot rely
|
|
7
7
|
* on the watcher having seen it. Invalidating a module also invalidates its
|
|
8
8
|
* importers, so a changed shapes barrel reaches the package entry that imports it,
|
|
9
9
|
* and a new shape module the barrel now imports is loaded with it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reloadSource.js","sourceRoot":"","sources":["../../../src/utils/reloadSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAQ7B;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAwB,EACxB,KAAe;IAEf,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,SAAS;QAC5D,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC","sourcesContent":["/**\n * Mark a package's source modules stale in Vite's module graph, so the next\n * `ssrLoadModule` of the package reads them from disk again.\n *\n * Vite's watcher does this too when a file changes, but asynchronously: a host\n * that has just written a file and asks the app
|
|
1
|
+
{"version":3,"file":"reloadSource.js","sourceRoot":"","sources":["../../../src/utils/reloadSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAQ7B;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAwB,EACxB,KAAe;IAEf,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,SAAS;QAC5D,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC","sourcesContent":["/**\n * Mark a package's source modules stale in Vite's module graph, so the next\n * `ssrLoadModule` of the package reads them from disk again.\n *\n * Vite's watcher does this too when a file changes, but asynchronously: a host\n * that has just written a file and asks the app for its graph content cannot rely\n * on the watcher having seen it. Invalidating a module also invalidates its\n * importers, so a changed shapes barrel reaches the package entry that imports it,\n * and a new shape module the barrel now imports is loaded with it.\n */\nimport * as path from 'path';\n\n/** The subset of Vite's (back-compat) module graph this module uses. */\nexport interface SourceModuleGraph {\n idToModuleMap: Map<string, { file?: string | null }>;\n onFileChange(file: string): void;\n}\n\n/**\n * Invalidate every module whose file lies under one of `roots` and outside any\n * `node_modules` directory. Returns the files it invalidated.\n */\nexport function invalidateSourceUnder(\n graph: SourceModuleGraph,\n roots: string[]\n): string[] {\n const prefixes = roots.map((r) => path.resolve(r) + path.sep);\n const files = new Set<string>();\n for (const mod of graph.idToModuleMap.values()) {\n const file = mod?.file;\n if (!file) continue;\n if (file.split(path.sep).includes('node_modules')) continue;\n if (prefixes.some((p) => file.startsWith(p))) files.add(file);\n }\n for (const file of files) graph.onFileChange(file);\n return [...files];\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ForwardedTransactions } from './forwardedTransactions.js';
|
|
2
|
+
/** The transactions this server holds for its callers (see `utils/forwardedTransactions`). */
|
|
3
|
+
export declare const transactions: ForwardedTransactions;
|
|
4
|
+
/**
|
|
5
|
+
* Whether every dataset `LinkedStorage` routes to holds store transactions: only then does this
|
|
6
|
+
* server begin one, so no query of a transaction can reach a store that would run it outside.
|
|
7
|
+
*/
|
|
8
|
+
export declare function storesHoldTransactions(): boolean;
|
|
9
|
+
export declare function assertStoresHoldTransactions(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Who a transaction is bound to: the session's user account in an HTTP call (a session is
|
|
12
|
+
* required, as for a write without an access rule), `system` for work with no request behind it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function transactionCaller(): string;
|
|
15
|
+
/** A transaction refusal as the answer the caller gets (`{error, code}`); anything else as is. */
|
|
16
|
+
export declare function transactionCallError(err: unknown): unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Roll back the transaction a refused request names (`txId` in its body), when it is the
|
|
19
|
+
* caller's own: a query refused before it ran in its transaction must not leave it open.
|
|
20
|
+
* Nothing happens for anyone else's, or an unknown one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function abandonTransactionOf(json: unknown): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store transactions this server's generic query plane holds (see `forwardedTransactions`
|
|
3
|
+
* and the provider in `backend.ts`): one registry per process, who a transaction is bound to,
|
|
4
|
+
* and how its refusals are answered. Server-only.
|
|
5
|
+
*/
|
|
6
|
+
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
7
|
+
import { supportsTransactions } from '@_linked/core/utils/Transaction';
|
|
8
|
+
import { isLinkedError } from '@_linked/core/utils/linkedError';
|
|
9
|
+
import { getCallContext } from '@_linked/server-utils/utils/CallContext';
|
|
10
|
+
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
11
|
+
import { ForwardedTransactions, TransactionNotYoursError, TransactionUnknownError, TransactionUnsupportedError, } from './forwardedTransactions.js';
|
|
12
|
+
/** The transactions this server holds for its callers (see `utils/forwardedTransactions`). */
|
|
13
|
+
export const transactions = new ForwardedTransactions();
|
|
14
|
+
/**
|
|
15
|
+
* Whether every dataset `LinkedStorage` routes to holds store transactions: only then does this
|
|
16
|
+
* server begin one, so no query of a transaction can reach a store that would run it outside.
|
|
17
|
+
*/
|
|
18
|
+
export function storesHoldTransactions() {
|
|
19
|
+
const datasets = [...LinkedStorage.getDatasets()];
|
|
20
|
+
if (!datasets.length)
|
|
21
|
+
return false;
|
|
22
|
+
return datasets.every((dataset) => {
|
|
23
|
+
try {
|
|
24
|
+
return supportsTransactions(dataset);
|
|
25
|
+
}
|
|
26
|
+
catch (_a) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function assertStoresHoldTransactions() {
|
|
32
|
+
if (storesHoldTransactions())
|
|
33
|
+
return;
|
|
34
|
+
throw new TransactionUnsupportedError("The stores of this server do not all declare 'transactions' in `supports`, so it holds no " +
|
|
35
|
+
'transactions; a query of one is refused rather than run outside it.');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Who a transaction is bound to: the session's user account in an HTTP call (a session is
|
|
39
|
+
* required, as for a write without an access rule), `system` for work with no request behind it.
|
|
40
|
+
*/
|
|
41
|
+
export function transactionCaller() {
|
|
42
|
+
var _a, _b, _c;
|
|
43
|
+
const ctx = getCallContext();
|
|
44
|
+
if (!ctx || ctx.kind !== 'http')
|
|
45
|
+
return 'system';
|
|
46
|
+
const id = (_c = (_b = (_a = ctx.request) === null || _a === void 0 ? void 0 : _a.linkedAuth) === null || _b === void 0 ? void 0 : _b.userAccount) === null || _c === void 0 ? void 0 : _c.id;
|
|
47
|
+
if (typeof id !== 'string' || !id)
|
|
48
|
+
throw new ServerCallError(401, 'Authentication required');
|
|
49
|
+
return `user:${id}`;
|
|
50
|
+
}
|
|
51
|
+
/** A transaction refusal as the answer the caller gets (`{error, code}`); anything else as is. */
|
|
52
|
+
export function transactionCallError(err) {
|
|
53
|
+
const status = isLinkedError(err, TransactionUnsupportedError)
|
|
54
|
+
? 501
|
|
55
|
+
: isLinkedError(err, TransactionUnknownError)
|
|
56
|
+
? 409
|
|
57
|
+
: isLinkedError(err, TransactionNotYoursError)
|
|
58
|
+
? 403
|
|
59
|
+
: undefined;
|
|
60
|
+
if (status === undefined)
|
|
61
|
+
return err;
|
|
62
|
+
const error = err;
|
|
63
|
+
return Object.assign(new ServerCallError(status, error.message, { cause: err }), { code: error.code });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Roll back the transaction a refused request names (`txId` in its body), when it is the
|
|
67
|
+
* caller's own: a query refused before it ran in its transaction must not leave it open.
|
|
68
|
+
* Nothing happens for anyone else's, or an unknown one.
|
|
69
|
+
*/
|
|
70
|
+
export async function abandonTransactionOf(json) {
|
|
71
|
+
const txId = json === null || json === void 0 ? void 0 : json.txId;
|
|
72
|
+
if (typeof txId !== 'string' || !txId)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
await transactions.rollback(txId, transactionCaller());
|
|
76
|
+
}
|
|
77
|
+
catch (_a) {
|
|
78
|
+
// another caller's, or no session: not ours to end
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=serverTransactions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverTransactions.js","sourceRoot":"","sources":["../../../src/utils/serverTransactions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The store transactions this server's generic query plane holds (see `forwardedTransactions`\n * and the provider in `backend.ts`): one registry per process, who a transaction is bound to,\n * and how its refusals are answered. Server-only.\n */\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { supportsTransactions } from '@_linked/core/utils/Transaction';\nimport { isLinkedError } from '@_linked/core/utils/linkedError';\nimport { getCallContext } from '@_linked/server-utils/utils/CallContext';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport {\n ForwardedTransactions,\n TransactionNotYoursError,\n TransactionUnknownError,\n TransactionUnsupportedError,\n} from './forwardedTransactions.js';\n\n/** The transactions this server holds for its callers (see `utils/forwardedTransactions`). */\nexport const transactions = new ForwardedTransactions();\n\n/**\n * Whether every dataset `LinkedStorage` routes to holds store transactions: only then does this\n * server begin one, so no query of a transaction can reach a store that would run it outside.\n */\nexport function storesHoldTransactions(): boolean {\n const datasets = [...LinkedStorage.getDatasets()];\n if (!datasets.length) return false;\n return datasets.every((dataset) => {\n try {\n return supportsTransactions(dataset);\n } catch {\n return false;\n }\n });\n}\n\nexport function assertStoresHoldTransactions(): void {\n if (storesHoldTransactions()) return;\n throw new TransactionUnsupportedError(\n \"The stores of this server do not all declare 'transactions' in `supports`, so it holds no \" +\n 'transactions; a query of one is refused rather than run outside it.'\n );\n}\n\n/**\n * Who a transaction is bound to: the session's user account in an HTTP call (a session is\n * required, as for a write without an access rule), `system` for work with no request behind it.\n */\nexport function transactionCaller(): string {\n const ctx = getCallContext();\n if (!ctx || ctx.kind !== 'http') return 'system';\n const id = ctx.request?.linkedAuth?.userAccount?.id;\n if (typeof id !== 'string' || !id) throw new ServerCallError(401, 'Authentication required');\n return `user:${id}`;\n}\n\n/** A transaction refusal as the answer the caller gets (`{error, code}`); anything else as is. */\nexport function transactionCallError(err: unknown): unknown {\n const status = isLinkedError(err, TransactionUnsupportedError)\n ? 501\n : isLinkedError(err, TransactionUnknownError)\n ? 409\n : isLinkedError(err, TransactionNotYoursError)\n ? 403\n : undefined;\n if (status === undefined) return err;\n const error = err as Error & { code: string };\n return Object.assign(new ServerCallError(status, error.message, { cause: err }), { code: error.code });\n}\n\n/**\n * Roll back the transaction a refused request names (`txId` in its body), when it is the\n * caller's own: a query refused before it ran in its transaction must not leave it open.\n * Nothing happens for anyone else's, or an unknown one.\n */\nexport async function abandonTransactionOf(json: unknown): Promise<void> {\n const txId = (json as { txId?: unknown } | null)?.txId;\n if (typeof txId !== 'string' || !txId) return;\n try {\n await transactions.rollback(txId, transactionCaller());\n } catch {\n // another caller's, or no session: not ours to end\n }\n}\n\n"]}
|
|
@@ -36,6 +36,30 @@ export interface ShapeGraphCore {
|
|
|
36
36
|
package?: string;
|
|
37
37
|
};
|
|
38
38
|
}): Promise<ShapeGraphSyncResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Core's routed sync (`@_linked/core/shapes/syncTargets`): the same options without a dataset;
|
|
41
|
+
* each package's graph goes where the routing needs it. Optional so a test double can leave it out.
|
|
42
|
+
*/
|
|
43
|
+
syncShapeGraphsRouted?(options: {
|
|
44
|
+
orphanScope: BootOrphanScope;
|
|
45
|
+
packages?: string[];
|
|
46
|
+
turtleFor: (packageName: string) => Promise<string | undefined>;
|
|
47
|
+
appShapes?: {
|
|
48
|
+
graph: string;
|
|
49
|
+
package?: string;
|
|
50
|
+
};
|
|
51
|
+
}): Promise<RoutedSyncReport<ShapeGraphSyncResult>>;
|
|
52
|
+
/**
|
|
53
|
+
* Core's `graphContent` (`@_linked/core/shapes/syncTargets`): what the `'package'`-mode syncs
|
|
54
|
+
* would write, writing nothing. Optional so a test double can leave it out.
|
|
55
|
+
*/
|
|
56
|
+
graphContent?(options: {
|
|
57
|
+
packages?: string[];
|
|
58
|
+
turtleFor: (packageName: string) => Promise<string | undefined>;
|
|
59
|
+
appShapes?: {
|
|
60
|
+
graph: string;
|
|
61
|
+
};
|
|
62
|
+
}): Promise<GraphContents>;
|
|
39
63
|
/** Every package with registered shapes (core counted for its built-ins). */
|
|
40
64
|
packagesWithShapes(): string[];
|
|
41
65
|
/** A package's shape graph IRI (`ShapesGraph.iriFor`). */
|
|
@@ -83,7 +107,7 @@ export declare function appShapesGraph(env?: Record<string, string | undefined>)
|
|
|
83
107
|
export declare function viteShapesTurtleGenerator(vite: {
|
|
84
108
|
ssrLoadModule?: (url: string) => Promise<unknown>;
|
|
85
109
|
} | undefined | null): ((packageName: string) => Promise<string | undefined>) | undefined;
|
|
86
|
-
/** What one shape sync did, as the boot logs it
|
|
110
|
+
/** What one shape sync did, as the boot logs it. */
|
|
87
111
|
export interface ShapeGraphsReport {
|
|
88
112
|
/** Packages whose shape graph was written, and those left as they were. */
|
|
89
113
|
written: string[];
|
|
@@ -115,11 +139,12 @@ export interface ShapeGraphsReport {
|
|
|
115
139
|
fallback?: true;
|
|
116
140
|
}
|
|
117
141
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* `shapes.ttl` and the default-graph shapes removed. Throws when the sync fails.
|
|
142
|
+
* The head of a sync's log line: the packages written, then how many were unchanged (`skipped`
|
|
143
|
+
* in the syncs' results): `written a, b; 17 unchanged`, `written a, b`, or `19 unchanged`.
|
|
121
144
|
*/
|
|
122
|
-
export declare function
|
|
145
|
+
export declare function writtenAndUnchanged(written: string[], unchanged: string[]): string;
|
|
146
|
+
/** The options of {@link syncShapeGraphsInto} and {@link syncRoutedShapeGraphs}. */
|
|
147
|
+
export interface ShapeGraphSyncOptions {
|
|
123
148
|
core: ShapeGraphCore;
|
|
124
149
|
orphanScope: BootOrphanScope;
|
|
125
150
|
/** Whether a package is the app or one of its workspace packages. */
|
|
@@ -140,7 +165,69 @@ export declare function syncShapeGraphsInto(ds: unknown, { core, orphanScope, is
|
|
|
140
165
|
read?: (packageDir: string | undefined) => string | undefined;
|
|
141
166
|
log?: (message: string) => void;
|
|
142
167
|
warn?: (message: string, err?: unknown) => void;
|
|
143
|
-
}
|
|
168
|
+
}
|
|
169
|
+
/** Where each registered package's Turtle comes from, and the sync options that follow from it. */
|
|
170
|
+
export interface ShapeTurtleSources {
|
|
171
|
+
sources: Map<string, ShapeTurtleSource>;
|
|
172
|
+
/** Installed packages without `shapes.ttl`: no shape graph, left out of the run. */
|
|
173
|
+
noFile: string[];
|
|
174
|
+
/** The package subset to sync: set only when some package has no Turtle file. */
|
|
175
|
+
packages?: string[];
|
|
176
|
+
turtleFor: (packageName: string) => Promise<string | undefined>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Where each registered package's Turtle comes from ({@link shapeTurtleSource}) and the
|
|
180
|
+
* `turtleFor` the syncs read it through. Shared by the syncs and the graph content endpoint, so
|
|
181
|
+
* both see the same bytes.
|
|
182
|
+
*/
|
|
183
|
+
export declare function shapeTurtleSources({ core, isOwn, dev, packageDirOf, generate, read, warn, }: Pick<ShapeGraphSyncOptions, 'core' | 'isOwn' | 'dev' | 'packageDirOf' | 'generate' | 'read' | 'warn'>): ShapeTurtleSources;
|
|
184
|
+
/**
|
|
185
|
+
* Sync the registered shapes into `ds`, one shape graph per package (see the module doc), and log
|
|
186
|
+
* one line naming what was written and skipped, the graphs written, the packages without
|
|
187
|
+
* `shapes.ttl` and the default-graph shapes removed. Throws when the sync fails. The server's boot
|
|
188
|
+
* follows the routing instead ({@link syncRoutedShapeGraphs}); this syncs one given dataset.
|
|
189
|
+
*/
|
|
190
|
+
export declare function syncShapeGraphsInto(ds: unknown, options: ShapeGraphSyncOptions): Promise<ShapeGraphsReport>;
|
|
191
|
+
/** A package graph a routed sync dropped or found left behind (core's `RoutedGraph`). */
|
|
192
|
+
export interface RoutedGraphReport {
|
|
193
|
+
dataset: string;
|
|
194
|
+
package: string;
|
|
195
|
+
graph: string;
|
|
196
|
+
}
|
|
197
|
+
/** What core's routed syncs report (`@_linked/core/shapes/syncTargets`). */
|
|
198
|
+
export interface RoutedSyncReport<R> {
|
|
199
|
+
targets: Array<{
|
|
200
|
+
dataset: string;
|
|
201
|
+
result: R;
|
|
202
|
+
}>;
|
|
203
|
+
pruned: RoutedGraphReport[];
|
|
204
|
+
leftovers: RoutedGraphReport[];
|
|
205
|
+
failed: Array<{
|
|
206
|
+
dataset: string;
|
|
207
|
+
error: string;
|
|
208
|
+
}>;
|
|
209
|
+
}
|
|
210
|
+
/** The routed part of a shape sync's report: per target dataset, and what routing left behind. */
|
|
211
|
+
export interface RoutedShapeGraphsReport extends ShapeGraphsReport {
|
|
212
|
+
/** Each dataset the shapes went to (the default one first) and what the sync did there. */
|
|
213
|
+
datasets: Array<{
|
|
214
|
+
dataset: string;
|
|
215
|
+
} & ShapeGraphsReport>;
|
|
216
|
+
/** Package shape graphs dropped from the default dataset: routing sends them elsewhere now. */
|
|
217
|
+
prunedGraphs: RoutedGraphReport[];
|
|
218
|
+
/** Package shape graphs left in another dataset routing no longer sends them to (reported, never dropped). */
|
|
219
|
+
leftovers: RoutedGraphReport[];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Sync the registered shapes where the routing needs them (core's `syncShapeGraphsRouted`): each
|
|
223
|
+
* package's shape graph into every dataset its shapes and the shape shapes route to, the app graph
|
|
224
|
+
* into the datasets of the shapes declaring `contains` properties, and the graphs routing no longer
|
|
225
|
+
* sends to the default dataset dropped there. Logs one line per dataset (as
|
|
226
|
+
* {@link syncShapeGraphsInto} does, naming the dataset when there are several) and one per graph
|
|
227
|
+
* dropped or left behind. The answer's top-level fields join the datasets'. Throws, after the other
|
|
228
|
+
* datasets synced, when one failed, naming each.
|
|
229
|
+
*/
|
|
230
|
+
export declare function syncRoutedShapeGraphs(options: ShapeGraphSyncOptions): Promise<RoutedShapeGraphsReport>;
|
|
144
231
|
/**
|
|
145
232
|
* @deprecated The default-graph shape sync's callback type; the server now syncs shape graphs with
|
|
146
233
|
* {@link syncShapeGraphsInto}. Kept so code built against an earlier server still compiles.
|
|
@@ -161,3 +248,33 @@ export declare function materializeShapes(ds: unknown, syncShapes: SyncShapesFn,
|
|
|
161
248
|
count: number;
|
|
162
249
|
orphanScope: BootOrphanScope;
|
|
163
250
|
}>;
|
|
251
|
+
/** One graph's content as the `'package'`-mode sync writes and records it (core's `GraphContentPart`). */
|
|
252
|
+
export interface GraphContentPart {
|
|
253
|
+
graph: string;
|
|
254
|
+
mediaType: 'text/turtle' | 'application/n-triples';
|
|
255
|
+
text: string;
|
|
256
|
+
hash: string;
|
|
257
|
+
sources: string[];
|
|
258
|
+
}
|
|
259
|
+
/** What core's `graphContent` answers. */
|
|
260
|
+
export interface GraphContents {
|
|
261
|
+
packages: Array<{
|
|
262
|
+
name: string;
|
|
263
|
+
shapes?: GraphContentPart;
|
|
264
|
+
ontology?: GraphContentPart;
|
|
265
|
+
}>;
|
|
266
|
+
appGraph?: GraphContentPart | null;
|
|
267
|
+
withoutTurtle: string[];
|
|
268
|
+
unreadable: string[];
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The content of the app's graphs as the `'package'`-mode syncs would write them, writing nothing
|
|
272
|
+
* (core's `graphContent`), with the server's choice of each package's Turtle
|
|
273
|
+
* ({@link shapeTurtleSources}): an installed package without `shapes.ttl` gets no shape graph here
|
|
274
|
+
* either (it is listed in `withoutTurtle`), exactly as the sync leaves it out. `packages` limits
|
|
275
|
+
* the answer; without it every registered package with shapes or ontology data. The app graph is
|
|
276
|
+
* answered when `appGraph` (`{DATA_ROOT}/graph/shapes`) is given.
|
|
277
|
+
*/
|
|
278
|
+
export declare function graphContentOf(options: Pick<ShapeGraphSyncOptions, 'core' | 'isOwn' | 'dev' | 'packageDirOf' | 'generate' | 'read' | 'warn' | 'appGraph'> & {
|
|
279
|
+
packages?: string[];
|
|
280
|
+
}): Promise<GraphContents>;
|