@_linked/server 3.7.0 → 3.9.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 +99 -0
- package/lib/esm/backend.d.ts +9 -0
- package/lib/esm/backend.js +28 -1
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/data/shapes.ttl +26 -0
- package/lib/esm/shapes/LinkedServer.d.ts +23 -19
- package/lib/esm/shapes/LinkedServer.js +63 -33
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +41 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.js +152 -4
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +57 -4
- package/lib/esm/utils/adminChannel.js +125 -15
- package/lib/esm/utils/adminChannel.js.map +1 -1
- package/lib/esm/utils/effectiveRoutes.d.ts +26 -0
- package/lib/esm/utils/effectiveRoutes.js +40 -0
- package/lib/esm/utils/effectiveRoutes.js.map +1 -0
- package/lib/esm/utils/ontologySync.d.ts +24 -6
- package/lib/esm/utils/ontologySync.js +30 -9
- package/lib/esm/utils/ontologySync.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +21 -0
- package/lib/esm/utils/queryPlane.js +60 -0
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/lib/esm/utils/syncShapes.d.ts +136 -26
- package/lib/esm/utils/syncShapes.js +175 -26
- package/lib/esm/utils/syncShapes.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +40 -3
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* module turns the incoming query into the builder the store runs and the IR
|
|
11
11
|
* the rules read.
|
|
12
12
|
*
|
|
13
|
+
* Graph queries (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`) arrive as plain
|
|
14
|
+
* data on `/call/@_linked/server/graphQuery`: {@link toGraphQuery} checks the JSON, the store's
|
|
15
|
+
* `graph` rule decides ({@link authorizeGraphQuery}), and a refusal of the store goes back with
|
|
16
|
+
* its code ({@link graphQueryCallError}).
|
|
17
|
+
*
|
|
13
18
|
* The host can supply the shape catalog a request's query is decoded in
|
|
14
19
|
* (`setQueryCatalogResolver`), so one server can resolve the same shape IRI
|
|
15
20
|
* differently per request (for example per project branch).
|
|
@@ -18,6 +23,7 @@
|
|
|
18
23
|
* the ground up (see QueryAccess).
|
|
19
24
|
*/
|
|
20
25
|
import { fromJSON } from '@_linked/core';
|
|
26
|
+
import { isGraphQueryError, parseGraphQuery, } from '@_linked/core/queries/graphQuery';
|
|
21
27
|
import { lower } from '@_linked/core/queries/lower';
|
|
22
28
|
import { checkQueryAccess, } from '@_linked/server-utils/utils/QueryAccess';
|
|
23
29
|
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
@@ -174,4 +180,58 @@ export async function authorizeRawQuery(query, endpoint) {
|
|
|
174
180
|
endpoint,
|
|
175
181
|
});
|
|
176
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* The graph query `json` describes, checked with core's `parseGraphQuery`; `undefined` when it is
|
|
185
|
+
* not a valid graph query (the access check then refuses it with 400).
|
|
186
|
+
*/
|
|
187
|
+
export function toGraphQuery(json) {
|
|
188
|
+
try {
|
|
189
|
+
return parseGraphQuery(json);
|
|
190
|
+
}
|
|
191
|
+
catch (_a) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check a graph query against the `graph` rule of the store `LinkedStorage.graphQuery` routes its
|
|
197
|
+
* record class to (`QueryAccess`, operation `graph`): no rule, or `'none'`, refuses it (403, 401
|
|
198
|
+
* without a session); a malformed one answers 400. `ir` is {@link toGraphQuery}'s result. No
|
|
199
|
+
* catalog is resolved for a graph query (its record shapes are core's built-ins), so the check
|
|
200
|
+
* resolves the store in the global registry, where `LinkedStorage.graphQuery` then runs it.
|
|
201
|
+
*/
|
|
202
|
+
export async function authorizeGraphQuery(query, ir, endpoint) {
|
|
203
|
+
await checkQueryAccess({
|
|
204
|
+
operation: 'graph',
|
|
205
|
+
query,
|
|
206
|
+
ir,
|
|
207
|
+
mode: getRpcExposureMode(),
|
|
208
|
+
endpoint,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The HTTP status each graph query refusal is answered with. A code this map does not know (a
|
|
213
|
+
* newer core than the one this server was built against) is answered with 400, never 500.
|
|
214
|
+
*/
|
|
215
|
+
const GRAPH_REFUSAL_STATUS = {
|
|
216
|
+
'unsupported-query-kind': 501,
|
|
217
|
+
'graph-name-read-only': 400,
|
|
218
|
+
'graph-not-registered': 409,
|
|
219
|
+
'graph-exists': 409,
|
|
220
|
+
'graph-not-routed': 400,
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* A store's graph query refusal (`GraphQueryError`) as the answer the caller gets: a
|
|
224
|
+
* `ServerCallError` with the refusal's status and message and its `code`, which the call route
|
|
225
|
+
* sends as `{error, code}` (`RemoteDataset` turns it back into a `GraphQueryError`). Any other
|
|
226
|
+
* error is returned as is.
|
|
227
|
+
*/
|
|
228
|
+
export function graphQueryCallError(err) {
|
|
229
|
+
if (!isGraphQueryError(err))
|
|
230
|
+
return err;
|
|
231
|
+
const code = err.code;
|
|
232
|
+
const status = Object.prototype.hasOwnProperty.call(GRAPH_REFUSAL_STATUS, code)
|
|
233
|
+
? GRAPH_REFUSAL_STATUS[code]
|
|
234
|
+
: 400;
|
|
235
|
+
return Object.assign(new ServerCallError(status, err.message, { cause: err }), { code: err.code });
|
|
236
|
+
}
|
|
177
237
|
//# sourceMappingURL=queryPlane.js.map
|
|
@@ -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 * 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 { 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"]}
|
|
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,37 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Boot-time shape materialization: write the shapes this process registered into
|
|
3
|
-
* its app-data dataset as SHACL, via core `syncShapes`.
|
|
4
|
-
*
|
|
5
|
-
* Kept apart from `LinkedServer` so the orphan policy can be tested without
|
|
6
|
-
* booting a server.
|
|
7
|
-
*/
|
|
8
1
|
/** The orphan policies a booting server may run with. */
|
|
9
2
|
export type BootOrphanScope = 'all' | 'none';
|
|
10
|
-
/** The subset of core's `syncShapes` this module calls. */
|
|
11
|
-
export type SyncShapesFn = (ds: unknown, options: {
|
|
12
|
-
orphanScope: BootOrphanScope;
|
|
13
|
-
}) => Promise<Array<() => Promise<void>>>;
|
|
14
3
|
/**
|
|
15
|
-
* The orphan policy for
|
|
4
|
+
* The orphan policy for the shape sync.
|
|
16
5
|
*
|
|
17
|
-
* `'none'` unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true
|
|
6
|
+
* `'none'` unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, which drops the
|
|
7
|
+
* shape graph of every package that is no longer registered in this process (never the app's own
|
|
8
|
+
* graph).
|
|
18
9
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* all, and an app-data dataset also holds shapes other writers put there (Create
|
|
23
|
-
* Now's `bindShape` / `enableCapability`). With pruning on, an app whose backend
|
|
24
|
-
* imports no shapes deletes every NodeShape in its app-data on each boot.
|
|
25
|
-
* `'ownedNamespaces'` is deliberately not offered: a namespace is a whole
|
|
26
|
-
* package, so a deep import of one shape file still prunes the rest of it.
|
|
10
|
+
* The registry is routinely a subset of what the dataset holds: shapes are deep-importable one
|
|
11
|
+
* file at a time, an app's backend may import no shapes at all, and an app-data dataset also holds
|
|
12
|
+
* shapes other writers put there. So nothing is pruned by default.
|
|
27
13
|
*/
|
|
28
14
|
export declare function bootOrphanScope(env?: Record<string, string | undefined>): BootOrphanScope;
|
|
15
|
+
/** What core's `syncShapeGraphs` reports (`@_linked/core/shapes/syncShapeGraphs`). */
|
|
16
|
+
export interface ShapeGraphSyncResult {
|
|
17
|
+
written: string[];
|
|
18
|
+
skipped: string[];
|
|
19
|
+
pruned: string[];
|
|
20
|
+
cleaned: number;
|
|
21
|
+
appGraph?: 'written' | 'skipped' | 'dropped';
|
|
22
|
+
defaultGraph?: number;
|
|
23
|
+
}
|
|
24
|
+
/** The subset of core's shape graph API this module calls. */
|
|
25
|
+
export interface ShapeGraphCore {
|
|
26
|
+
syncShapeGraphs(ds: unknown, options: {
|
|
27
|
+
orphanScope: BootOrphanScope;
|
|
28
|
+
packages?: string[];
|
|
29
|
+
turtleFor: (packageName: string) => Promise<string | undefined>;
|
|
30
|
+
appShapes?: {
|
|
31
|
+
graph: string;
|
|
32
|
+
package?: string;
|
|
33
|
+
};
|
|
34
|
+
}): Promise<ShapeGraphSyncResult>;
|
|
35
|
+
/** Every package with registered shapes (core counted for its built-ins). */
|
|
36
|
+
packagesWithShapes(): string[];
|
|
37
|
+
/** A package's shape graph IRI (`ShapesGraph.iriFor`). */
|
|
38
|
+
shapesGraphFor(packageName: string): string;
|
|
39
|
+
}
|
|
40
|
+
/** Import core's shape graph sync. */
|
|
41
|
+
export declare function loadShapeGraphCore(): Promise<ShapeGraphCore>;
|
|
42
|
+
/** Where `linked build` writes a package's shapes, relative to the package root. */
|
|
43
|
+
export declare const SHAPES_TURTLE_FILE: string;
|
|
44
|
+
/** A package's built `shapes.ttl`, or undefined when the package root has none. */
|
|
45
|
+
export declare function readShapesTurtle(packageDir: string | undefined): string | undefined;
|
|
46
|
+
/** Where one package's Turtle comes from. */
|
|
47
|
+
export type ShapeTurtleSource = {
|
|
48
|
+
source: 'file';
|
|
49
|
+
text: string;
|
|
50
|
+
} | {
|
|
51
|
+
source: 'generated';
|
|
52
|
+
} | {
|
|
53
|
+
source: 'none';
|
|
54
|
+
};
|
|
29
55
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
* Where a registered package's Turtle comes from: in dev an own package is generated; otherwise
|
|
57
|
+
* its built file when it has one; else an own package is generated and an installed one has
|
|
58
|
+
* none (no shape graph, no generation).
|
|
59
|
+
*/
|
|
60
|
+
export declare function shapeTurtleSource(packageName: string, { own, dev, packageDir, read, }: {
|
|
61
|
+
/** Whether the package is the app or one of its workspace packages. */
|
|
62
|
+
own: boolean;
|
|
63
|
+
/** Whether the server runs with Vite (dev). */
|
|
64
|
+
dev: boolean;
|
|
65
|
+
/** The package root, when the server knows it. */
|
|
66
|
+
packageDir: string | undefined;
|
|
67
|
+
read?: (packageDir: string | undefined) => string | undefined;
|
|
68
|
+
}): ShapeTurtleSource;
|
|
69
|
+
/**
|
|
70
|
+
* The app's own shape graph, `{DATA_ROOT}/graph/shapes`, or undefined when `DATA_ROOT` is not
|
|
71
|
+
* an absolute IRI.
|
|
72
|
+
*/
|
|
73
|
+
export declare function appShapesGraph(env?: Record<string, string | undefined>): string | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* In dev, the generator for the app's own packages: core's `packageShapesTurtle` imported through
|
|
76
|
+
* Vite's `ssrLoadModule`, the core the SSR graph registered the source shapes on. Undefined
|
|
77
|
+
* without Vite.
|
|
78
|
+
*/
|
|
79
|
+
export declare function viteShapesTurtleGenerator(vite: {
|
|
80
|
+
ssrLoadModule?: (url: string) => Promise<unknown>;
|
|
81
|
+
} | undefined | null): ((packageName: string) => Promise<string | undefined>) | undefined;
|
|
82
|
+
/** What one shape sync did, as the boot logs it and `POST /admin/sync-shapes` reports it. */
|
|
83
|
+
export interface ShapeGraphsReport {
|
|
84
|
+
/** Packages whose shape graph was written, and those left as they were. */
|
|
85
|
+
written: string[];
|
|
86
|
+
skipped: string[];
|
|
87
|
+
/** Packages no longer registered whose shape graph was dropped (`orphanScope: 'all'`). */
|
|
88
|
+
pruned: string[];
|
|
89
|
+
/** The shape graphs written (package graphs, then the app graph), by IRI. */
|
|
90
|
+
graphs: string[];
|
|
91
|
+
/** The app's own shape graph and what the sync did with it; absent without `DATA_ROOT`. */
|
|
92
|
+
appGraph?: {
|
|
93
|
+
graph: string;
|
|
94
|
+
status: 'written' | 'skipped' | 'dropped';
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Registered packages with no Turtle, so with no shape graph: an installed package without
|
|
98
|
+
* `shapes.ttl`, or an own package whose shapes could not be generated.
|
|
99
|
+
*/
|
|
100
|
+
withoutTurtle: string[];
|
|
101
|
+
/** Shapes an earlier sync had written into the default graph, removed now. */
|
|
102
|
+
cleaned: number;
|
|
103
|
+
/** Set when the store has no graph queries: the default-graph sync's writes. */
|
|
104
|
+
defaultGraph?: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Sync the registered shapes into `ds`, one shape graph per package (see the module doc), and log
|
|
108
|
+
* one line naming what was written and skipped, the graphs written, the packages without
|
|
109
|
+
* `shapes.ttl` and the default-graph shapes removed. Throws when the sync fails.
|
|
110
|
+
*/
|
|
111
|
+
export declare function syncShapeGraphsInto(ds: unknown, { core, orphanScope, isOwn, dev, packageDirOf, generate, appGraph, appPackage, read, log, warn, }: {
|
|
112
|
+
core: ShapeGraphCore;
|
|
113
|
+
orphanScope: BootOrphanScope;
|
|
114
|
+
/** Whether a package is the app or one of its workspace packages. */
|
|
115
|
+
isOwn: (packageName: string) => boolean;
|
|
116
|
+
/** Whether the server runs with Vite (dev). */
|
|
117
|
+
dev: boolean;
|
|
118
|
+
/** A package's root directory, when the server knows it. */
|
|
119
|
+
packageDirOf: (packageName: string) => string | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Generates an own package's Turtle (in dev through Vite's loader); undefined, or a result of
|
|
122
|
+
* undefined, leaves the generation to the core that runs the sync.
|
|
123
|
+
*/
|
|
124
|
+
generate?: (packageName: string) => Promise<string | undefined>;
|
|
125
|
+
/** The app's own shape graph (`{DATA_ROOT}/graph/shapes`). */
|
|
126
|
+
appGraph?: string;
|
|
127
|
+
/** The app's package name, recorded on its shape graph's record. */
|
|
128
|
+
appPackage?: string;
|
|
129
|
+
read?: (packageDir: string | undefined) => string | undefined;
|
|
130
|
+
log?: (message: string) => void;
|
|
131
|
+
warn?: (message: string, err?: unknown) => void;
|
|
132
|
+
}): Promise<ShapeGraphsReport>;
|
|
133
|
+
/**
|
|
134
|
+
* @deprecated The default-graph shape sync's callback type; the server now syncs shape graphs with
|
|
135
|
+
* {@link syncShapeGraphsInto}. Kept so code built against an earlier server still compiles.
|
|
136
|
+
*/
|
|
137
|
+
export type SyncShapesFn = (ds: unknown, options: {
|
|
138
|
+
orphanScope: BootOrphanScope;
|
|
139
|
+
}) => Promise<Array<() => Promise<void>>>;
|
|
140
|
+
/**
|
|
141
|
+
* @deprecated Use {@link syncShapeGraphsInto}, which writes each package's shapes into its own
|
|
142
|
+
* shape graph. This runs the old default-graph sync: it plans every registered shape with
|
|
143
|
+
* `syncShapes` (the orphan policy from {@link bootOrphanScope}) and runs the writes in batches
|
|
144
|
+
* of 8. Kept so code built against an earlier server keeps working.
|
|
35
145
|
*/
|
|
36
146
|
export declare function materializeShapes(ds: unknown, syncShapes: SyncShapesFn, { env, log, }?: {
|
|
37
147
|
env?: Record<string, string | undefined>;
|
|
@@ -1,33 +1,187 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The shape sync, as the server runs it at boot and when the host asks for a re-sync
|
|
3
|
+
* (`POST /admin/sync-shapes`): core's `syncShapeGraphs` writes each package's shapes into its own
|
|
4
|
+
* named graph of the app's default dataset (`{package IRI}/graph/shapes`, a `ShapesGraph` record
|
|
5
|
+
* in the graph registry), skipped when the stored content hash matches, and the app's generated
|
|
6
|
+
* single-owner shapes into the app's own shape graph, `{DATA_ROOT}/graph/shapes`. Shapes an
|
|
7
|
+
* earlier sync wrote into the default graph are removed there once their package's graph holds
|
|
8
|
+
* them. A store without graph queries gets core's default-graph shape sync instead.
|
|
4
9
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
10
|
+
* Where a package's Turtle comes from ({@link shapeTurtleSource}):
|
|
11
|
+
* - an installed package: its built `lib/esm/data/shapes.ttl` (written by `linked build`); a
|
|
12
|
+
* package without that file gets no shape graph, and nothing is generated for it;
|
|
13
|
+
* - the app's own packages (the app and its workspace packages): in dev, generated in process
|
|
14
|
+
* from their source shapes, through Vite's loader, so the core the SSR graph registered the
|
|
15
|
+
* shapes on writes them (the same bytes as `packageShapesTurtle`, which `linked build` writes);
|
|
16
|
+
* in a built app, their `lib/esm/data/shapes.ttl` when there is one, else generated in process.
|
|
17
|
+
*
|
|
18
|
+
* Kept apart from `LinkedServer` so the choices and the log can be tested without booting a
|
|
19
|
+
* server.
|
|
7
20
|
*/
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
8
23
|
/**
|
|
9
|
-
* The orphan policy for
|
|
24
|
+
* The orphan policy for the shape sync.
|
|
10
25
|
*
|
|
11
|
-
* `'none'` unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true
|
|
26
|
+
* `'none'` unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, which drops the
|
|
27
|
+
* shape graph of every package that is no longer registered in this process (never the app's own
|
|
28
|
+
* graph).
|
|
12
29
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* all, and an app-data dataset also holds shapes other writers put there (Create
|
|
17
|
-
* Now's `bindShape` / `enableCapability`). With pruning on, an app whose backend
|
|
18
|
-
* imports no shapes deletes every NodeShape in its app-data on each boot.
|
|
19
|
-
* `'ownedNamespaces'` is deliberately not offered: a namespace is a whole
|
|
20
|
-
* package, so a deep import of one shape file still prunes the rest of it.
|
|
30
|
+
* The registry is routinely a subset of what the dataset holds: shapes are deep-importable one
|
|
31
|
+
* file at a time, an app's backend may import no shapes at all, and an app-data dataset also holds
|
|
32
|
+
* shapes other writers put there. So nothing is pruned by default.
|
|
21
33
|
*/
|
|
22
34
|
export function bootOrphanScope(env = process.env) {
|
|
23
35
|
return env.LINKED_SYNC_SHAPES_PRUNE_ORPHANS === 'true' ? 'all' : 'none';
|
|
24
36
|
}
|
|
37
|
+
/** Import core's shape graph sync. */
|
|
38
|
+
export async function loadShapeGraphCore() {
|
|
39
|
+
const sync = await import('@_linked/core/shapes/syncShapeGraphs');
|
|
40
|
+
const turtle = await import('@_linked/core/shapes/shapesToTurtle');
|
|
41
|
+
const graphs = await import('@_linked/core/shapes/NamedGraph');
|
|
42
|
+
return {
|
|
43
|
+
syncShapeGraphs: (ds, options) => sync.syncShapeGraphs(ds, options),
|
|
44
|
+
packagesWithShapes: () => turtle.packagesWithShapes(),
|
|
45
|
+
shapesGraphFor: (packageName) => graphs.ShapesGraph.iriFor(packageName),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Where `linked build` writes a package's shapes, relative to the package root. */
|
|
49
|
+
export const SHAPES_TURTLE_FILE = path.join('lib', 'esm', 'data', 'shapes.ttl');
|
|
50
|
+
/** A package's built `shapes.ttl`, or undefined when the package root has none. */
|
|
51
|
+
export function readShapesTurtle(packageDir) {
|
|
52
|
+
if (!packageDir)
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
return fs.readFileSync(path.join(packageDir, SHAPES_TURTLE_FILE), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
25
61
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
62
|
+
* Where a registered package's Turtle comes from: in dev an own package is generated; otherwise
|
|
63
|
+
* its built file when it has one; else an own package is generated and an installed one has
|
|
64
|
+
* none (no shape graph, no generation).
|
|
65
|
+
*/
|
|
66
|
+
export function shapeTurtleSource(packageName, { own, dev, packageDir, read = readShapesTurtle, }) {
|
|
67
|
+
if (own && dev)
|
|
68
|
+
return { source: 'generated' };
|
|
69
|
+
const text = read(packageDir);
|
|
70
|
+
if (text !== undefined)
|
|
71
|
+
return { source: 'file', text };
|
|
72
|
+
return own ? { source: 'generated' } : { source: 'none' };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The app's own shape graph, `{DATA_ROOT}/graph/shapes`, or undefined when `DATA_ROOT` is not
|
|
76
|
+
* an absolute IRI.
|
|
77
|
+
*/
|
|
78
|
+
export function appShapesGraph(env = process.env) {
|
|
79
|
+
var _a;
|
|
80
|
+
const root = (_a = env.DATA_ROOT) === null || _a === void 0 ? void 0 : _a.trim().replace(/\/+$/, '');
|
|
81
|
+
if (!root || !/^[a-z][a-z0-9+.-]*:\/\/[^/]/i.test(root))
|
|
82
|
+
return undefined;
|
|
83
|
+
return `${root}/graph/shapes`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* In dev, the generator for the app's own packages: core's `packageShapesTurtle` imported through
|
|
87
|
+
* Vite's `ssrLoadModule`, the core the SSR graph registered the source shapes on. Undefined
|
|
88
|
+
* without Vite.
|
|
89
|
+
*/
|
|
90
|
+
export function viteShapesTurtleGenerator(vite) {
|
|
91
|
+
const ssrLoadModule = vite === null || vite === void 0 ? void 0 : vite.ssrLoadModule;
|
|
92
|
+
if (typeof ssrLoadModule !== 'function')
|
|
93
|
+
return undefined;
|
|
94
|
+
return async (packageName) => {
|
|
95
|
+
const mod = (await ssrLoadModule.call(vite, '@_linked/core/shapes/shapesToTurtle'));
|
|
96
|
+
if (typeof (mod === null || mod === void 0 ? void 0 : mod.packageShapesTurtle) !== 'function') {
|
|
97
|
+
throw new Error('@_linked/core/shapes/shapesToTurtle has no packageShapesTurtle');
|
|
98
|
+
}
|
|
99
|
+
return mod.packageShapesTurtle(packageName);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const list = (names) => (names.length ? names.join(', ') : 'none');
|
|
103
|
+
/**
|
|
104
|
+
* Sync the registered shapes into `ds`, one shape graph per package (see the module doc), and log
|
|
105
|
+
* one line naming what was written and skipped, the graphs written, the packages without
|
|
106
|
+
* `shapes.ttl` and the default-graph shapes removed. Throws when the sync fails.
|
|
107
|
+
*/
|
|
108
|
+
export async function syncShapeGraphsInto(ds, { core, orphanScope, isOwn, dev, packageDirOf, generate, appGraph, appPackage, read = readShapesTurtle, log = console.log, warn = console.warn, }) {
|
|
109
|
+
const sources = new Map();
|
|
110
|
+
for (const name of core.packagesWithShapes()) {
|
|
111
|
+
sources.set(name, shapeTurtleSource(name, { own: isOwn(name), dev, packageDir: packageDirOf(name), read }));
|
|
112
|
+
}
|
|
113
|
+
const noFile = [...sources].filter(([, s]) => s.source === 'none').map(([name]) => name);
|
|
114
|
+
const turtleFor = async (name) => {
|
|
115
|
+
const source = sources.get(name);
|
|
116
|
+
if ((source === null || source === void 0 ? void 0 : source.source) === 'file')
|
|
117
|
+
return source.text;
|
|
118
|
+
if ((source === null || source === void 0 ? void 0 : source.source) !== 'generated' || !generate)
|
|
119
|
+
return undefined;
|
|
120
|
+
try {
|
|
121
|
+
return await generate(name);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
warn(`[LinkedServer] generating the shapes of ${name} through the dev loader failed; trying the server's core instead:`, err);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const result = await core.syncShapeGraphs(ds, {
|
|
129
|
+
orphanScope,
|
|
130
|
+
// A package without Turtle is left out: core would generate it otherwise.
|
|
131
|
+
...(noFile.length ? { packages: [...sources.keys()].filter((n) => !noFile.includes(n)) } : {}),
|
|
132
|
+
turtleFor,
|
|
133
|
+
...(appGraph ? { appShapes: { graph: appGraph, ...(appPackage ? { package: appPackage } : {}) } } : {}),
|
|
134
|
+
});
|
|
135
|
+
// A package core neither wrote nor skipped got no Turtle at all: generation produced nothing
|
|
136
|
+
// (in dev, the server's core may not hold the source shapes the dev loader registered). The
|
|
137
|
+
// default-graph sync (a store without graph queries) reports no packages, so it is not checked.
|
|
138
|
+
const covered = new Set([...result.written, ...result.skipped]);
|
|
139
|
+
const notGenerated = result.defaultGraph !== undefined
|
|
140
|
+
? []
|
|
141
|
+
: [...sources.keys()].filter((n) => !noFile.includes(n) && !covered.has(n));
|
|
142
|
+
if (notGenerated.length) {
|
|
143
|
+
warn(`[LinkedServer] no shapes could be generated for ${list(notGenerated.sort())}, so ` +
|
|
144
|
+
`${notGenerated.length === 1 ? 'it has' : 'they have'} no shape graph this sync`);
|
|
145
|
+
}
|
|
146
|
+
const withoutTurtle = [...noFile, ...notGenerated].sort();
|
|
147
|
+
const report = {
|
|
148
|
+
written: result.written,
|
|
149
|
+
skipped: result.skipped,
|
|
150
|
+
pruned: result.pruned,
|
|
151
|
+
graphs: result.written.map((name) => core.shapesGraphFor(name)),
|
|
152
|
+
withoutTurtle,
|
|
153
|
+
cleaned: result.cleaned,
|
|
154
|
+
};
|
|
155
|
+
if (appGraph && result.appGraph) {
|
|
156
|
+
report.appGraph = { graph: appGraph, status: result.appGraph };
|
|
157
|
+
if (result.appGraph === 'written')
|
|
158
|
+
report.graphs.push(appGraph);
|
|
159
|
+
}
|
|
160
|
+
if (result.defaultGraph !== undefined)
|
|
161
|
+
report.defaultGraph = result.defaultGraph;
|
|
162
|
+
if (report.defaultGraph !== undefined) {
|
|
163
|
+
log(`[LinkedServer] shape sync (default graph: the store has no graph queries): ` +
|
|
164
|
+
`${report.defaultGraph} shape(s) written`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
log(`[LinkedServer] shape graph sync: written ${list(report.written)}; skipped ${list(report.skipped)}` +
|
|
168
|
+
(report.appGraph ? `; app graph ${report.appGraph.status}` : '; no app graph (DATA_ROOT not set)') +
|
|
169
|
+
(report.graphs.length ? `; graphs written ${list(report.graphs)}` : '') +
|
|
170
|
+
(orphanScope === 'all' ? `; pruned ${list(report.pruned)}` : '') +
|
|
171
|
+
(report.cleaned ? `; ${report.cleaned} default-graph shape(s) removed` : '') +
|
|
172
|
+
(withoutTurtle.length ? `; no shapes.ttl (no shape graph): ${list(withoutTurtle)}` : ''));
|
|
173
|
+
}
|
|
174
|
+
if (orphanScope === 'all' && withoutTurtle.length) {
|
|
175
|
+
warn('[LinkedServer] orphaned shape graphs were not pruned: core prunes only when the sync covers ' +
|
|
176
|
+
`every registered package, and ${list(withoutTurtle)} has no Turtle`);
|
|
177
|
+
}
|
|
178
|
+
return report;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* @deprecated Use {@link syncShapeGraphsInto}, which writes each package's shapes into its own
|
|
182
|
+
* shape graph. This runs the old default-graph sync: it plans every registered shape with
|
|
183
|
+
* `syncShapes` (the orphan policy from {@link bootOrphanScope}) and runs the writes in batches
|
|
184
|
+
* of 8. Kept so code built against an earlier server keeps working.
|
|
31
185
|
*/
|
|
32
186
|
export async function materializeShapes(ds, syncShapes, { env = process.env, log = console.log, } = {}) {
|
|
33
187
|
const orphanScope = bootOrphanScope(env);
|
|
@@ -35,13 +189,8 @@ export async function materializeShapes(ds, syncShapes, { env = process.env, log
|
|
|
35
189
|
for (let i = 0; i < thunks.length; i += 8) {
|
|
36
190
|
await Promise.all(thunks.slice(i, i + 8).map((run) => run()));
|
|
37
191
|
}
|
|
38
|
-
log(orphanScope
|
|
39
|
-
|
|
40
|
-
`(LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true): ${thunks.length} operation(s), ` +
|
|
41
|
-
`store shapes not registered in this process were deleted`
|
|
42
|
-
: `[LinkedServer] synced shapes into app-data with orphanScope 'none': ` +
|
|
43
|
-
`${thunks.length} shape(s) written, no store shapes deleted ` +
|
|
44
|
-
`(set LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true to prune)`);
|
|
192
|
+
log(`[LinkedServer] synced shapes into the default graph with orphanScope '${orphanScope}': ` +
|
|
193
|
+
`${thunks.length} operation(s)`);
|
|
45
194
|
return { count: thunks.length, orphanScope };
|
|
46
195
|
}
|
|
47
196
|
//# sourceMappingURL=syncShapes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncShapes.js","sourceRoot":"","sources":["../../../src/utils/syncShapes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA0C,OAAO,CAAC,GAAG;IAErD,OAAO,GAAG,CAAC,gCAAgC,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAW,EACX,UAAwB,EACxB,EACE,GAAG,GAAG,OAAO,CAAC,GAAG,EACjB,GAAG,GAAG,OAAO,CAAC,GAAG,MAIf,EAAE;IAEN,MAAM,WAAW,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,EAAE,EAAE,EAAC,WAAW,EAAC,CAAC,CAAC;IACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,GAAG,CACD,WAAW,KAAK,KAAK;QACnB,CAAC,CAAC,oEAAoE;YAClE,4CAA4C,MAAM,CAAC,MAAM,iBAAiB;YAC1E,0DAA0D;QAC9D,CAAC,CAAC,sEAAsE;YACpE,GAAG,MAAM,CAAC,MAAM,6CAA6C;YAC7D,sDAAsD,CAC7D,CAAC;IACF,OAAO,EAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,EAAC,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * Boot-time shape materialization: write the shapes this process registered into\n * its app-data dataset as SHACL, via core `syncShapes`.\n *\n * Kept apart from `LinkedServer` so the orphan policy can be tested without\n * booting a server.\n */\n\n/** The orphan policies a booting server may run with. */\nexport type BootOrphanScope = 'all' | 'none';\n\n/** The subset of core's `syncShapes` this module calls. */\nexport type SyncShapesFn = (\n ds: unknown,\n options: {orphanScope: BootOrphanScope},\n) => Promise<Array<() => Promise<void>>>;\n\n/**\n * The orphan policy for boot-time sync.\n *\n * `'none'` unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`.\n *\n * Boot sync must not delete store shapes the process did not register, because\n * the registry is routinely a subset of what the dataset holds: shapes are\n * deep-importable one file at a time, an app's backend may import no shapes at\n * all, and an app-data dataset also holds shapes other writers put there (Create\n * Now's `bindShape` / `enableCapability`). With pruning on, an app whose backend\n * imports no shapes deletes every NodeShape in its app-data on each boot.\n * `'ownedNamespaces'` is deliberately not offered: a namespace is a whole\n * package, so a deep import of one shape file still prunes the rest of it.\n */\nexport function bootOrphanScope(\n env: Record<string, string | undefined> = process.env,\n): BootOrphanScope {\n return env.LINKED_SYNC_SHAPES_PRUNE_ORPHANS === 'true' ? 'all' : 'none';\n}\n\n/**\n * Plan and run a sync of every registered shape into `ds`, in batches of 8 so\n * Fuseki is not flooded. The orphan policy is always passed explicitly — an app\n * may be running a core whose `syncShapes` still defaults to pruning.\n *\n * Logs one line naming the policy that ran.\n */\nexport async function materializeShapes(\n ds: unknown,\n syncShapes: SyncShapesFn,\n {\n env = process.env,\n log = console.log,\n }: {\n env?: Record<string, string | undefined>;\n log?: (message: string) => void;\n } = {},\n): Promise<{count: number; orphanScope: BootOrphanScope}> {\n const orphanScope = bootOrphanScope(env);\n const thunks = await syncShapes(ds, {orphanScope});\n for (let i = 0; i < thunks.length; i += 8) {\n await Promise.all(thunks.slice(i, i + 8).map((run) => run()));\n }\n log(\n orphanScope === 'all'\n ? `[LinkedServer] synced shapes into app-data with orphanScope 'all' ` +\n `(LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true): ${thunks.length} operation(s), ` +\n `store shapes not registered in this process were deleted`\n : `[LinkedServer] synced shapes into app-data with orphanScope 'none': ` +\n `${thunks.length} shape(s) written, no store shapes deleted ` +\n `(set LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true to prune)`,\n );\n return {count: thunks.length, orphanScope};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"syncShapes.js","sourceRoot":"","sources":["../../../src/utils/syncShapes.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shape sync, as the server runs it at boot and when the host asks for a re-sync\n * (`POST /admin/sync-shapes`): core's `syncShapeGraphs` writes each package's shapes into its own\n * named graph of the app's default dataset (`{package IRI}/graph/shapes`, a `ShapesGraph` record\n * in the graph registry), skipped when the stored content hash matches, and the app's generated\n * single-owner shapes into the app's own shape graph, `{DATA_ROOT}/graph/shapes`. Shapes an\n * earlier sync wrote into the default graph are removed there once their package's graph holds\n * them. A store without graph queries gets core's default-graph shape sync instead.\n *\n * Where a package's Turtle comes from ({@link shapeTurtleSource}):\n * - an installed package: its built `lib/esm/data/shapes.ttl` (written by `linked build`); a\n * package without that file gets no shape graph, and nothing is generated for it;\n * - the app's own packages (the app and its workspace packages): in dev, generated in process\n * from their source shapes, through Vite's loader, so the core the SSR graph registered the\n * shapes on writes them (the same bytes as `packageShapesTurtle`, which `linked build` writes);\n * in a built app, their `lib/esm/data/shapes.ttl` when there is one, else generated in process.\n *\n * Kept apart from `LinkedServer` so the choices and the log can be tested without booting a\n * server.\n */\nimport fs from 'fs';\nimport path from 'path';\n\n/** The orphan policies a booting server may run with. */\nexport type BootOrphanScope = 'all' | 'none';\n\n/**\n * The orphan policy for the shape sync.\n *\n * `'none'` unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, which drops the\n * shape graph of every package that is no longer registered in this process (never the app's own\n * graph).\n *\n * The registry is routinely a subset of what the dataset holds: shapes are deep-importable one\n * file at a time, an app's backend may import no shapes at all, and an app-data dataset also holds\n * shapes other writers put there. So nothing is pruned by default.\n */\nexport function bootOrphanScope(\n env: Record<string, string | undefined> = process.env,\n): BootOrphanScope {\n return env.LINKED_SYNC_SHAPES_PRUNE_ORPHANS === 'true' ? 'all' : 'none';\n}\n\n/** What core's `syncShapeGraphs` reports (`@_linked/core/shapes/syncShapeGraphs`). */\nexport interface ShapeGraphSyncResult {\n written: string[];\n skipped: string[];\n pruned: string[];\n cleaned: number;\n appGraph?: 'written' | 'skipped' | 'dropped';\n defaultGraph?: number;\n}\n\n/** The subset of core's shape graph API this module calls. */\nexport interface ShapeGraphCore {\n syncShapeGraphs(\n ds: unknown,\n options: {\n orphanScope: BootOrphanScope;\n packages?: string[];\n turtleFor: (packageName: string) => Promise<string | undefined>;\n appShapes?: {graph: string; package?: string};\n },\n ): Promise<ShapeGraphSyncResult>;\n /** Every package with registered shapes (core counted for its built-ins). */\n packagesWithShapes(): string[];\n /** A package's shape graph IRI (`ShapesGraph.iriFor`). */\n shapesGraphFor(packageName: string): string;\n}\n\n/** Import core's shape graph sync. */\nexport async function loadShapeGraphCore(): Promise<ShapeGraphCore> {\n const sync = await import('@_linked/core/shapes/syncShapeGraphs');\n const turtle = await import('@_linked/core/shapes/shapesToTurtle');\n const graphs = await import('@_linked/core/shapes/NamedGraph');\n return {\n syncShapeGraphs: (ds, options) =>\n sync.syncShapeGraphs(ds as Parameters<typeof sync.syncShapeGraphs>[0], options),\n packagesWithShapes: () => turtle.packagesWithShapes(),\n shapesGraphFor: (packageName) => graphs.ShapesGraph.iriFor(packageName),\n };\n}\n\n/** Where `linked build` writes a package's shapes, relative to the package root. */\nexport const SHAPES_TURTLE_FILE = path.join('lib', 'esm', 'data', 'shapes.ttl');\n\n/** A package's built `shapes.ttl`, or undefined when the package root has none. */\nexport function readShapesTurtle(packageDir: string | undefined): string | undefined {\n if (!packageDir) return undefined;\n try {\n return fs.readFileSync(path.join(packageDir, SHAPES_TURTLE_FILE), 'utf-8');\n } catch {\n return undefined;\n }\n}\n\n/** Where one package's Turtle comes from. */\nexport type ShapeTurtleSource =\n | {source: 'file'; text: string}\n | {source: 'generated'}\n | {source: 'none'};\n\n/**\n * Where a registered package's Turtle comes from: in dev an own package is generated; otherwise\n * its built file when it has one; else an own package is generated and an installed one has\n * none (no shape graph, no generation).\n */\nexport function shapeTurtleSource(\n packageName: string,\n {\n own,\n dev,\n packageDir,\n read = readShapesTurtle,\n }: {\n /** Whether the package is the app or one of its workspace packages. */\n own: boolean;\n /** Whether the server runs with Vite (dev). */\n dev: boolean;\n /** The package root, when the server knows it. */\n packageDir: string | undefined;\n read?: (packageDir: string | undefined) => string | undefined;\n },\n): ShapeTurtleSource {\n if (own && dev) return {source: 'generated'};\n const text = read(packageDir);\n if (text !== undefined) return {source: 'file', text};\n return own ? {source: 'generated'} : {source: 'none'};\n}\n\n/**\n * The app's own shape graph, `{DATA_ROOT}/graph/shapes`, or undefined when `DATA_ROOT` is not\n * an absolute IRI.\n */\nexport function appShapesGraph(\n env: Record<string, string | undefined> = process.env,\n): string | undefined {\n const root = env.DATA_ROOT?.trim().replace(/\\/+$/, '');\n if (!root || !/^[a-z][a-z0-9+.-]*:\\/\\/[^/]/i.test(root)) return undefined;\n return `${root}/graph/shapes`;\n}\n\n/**\n * In dev, the generator for the app's own packages: core's `packageShapesTurtle` imported through\n * Vite's `ssrLoadModule`, the core the SSR graph registered the source shapes on. Undefined\n * without Vite.\n */\nexport function viteShapesTurtleGenerator(\n vite: {ssrLoadModule?: (url: string) => Promise<unknown>} | undefined | null,\n): ((packageName: string) => Promise<string | undefined>) | undefined {\n const ssrLoadModule = vite?.ssrLoadModule;\n if (typeof ssrLoadModule !== 'function') return undefined;\n return async (packageName) => {\n const mod = (await ssrLoadModule.call(vite, '@_linked/core/shapes/shapesToTurtle')) as {\n packageShapesTurtle?: (name: string) => string | undefined;\n };\n if (typeof mod?.packageShapesTurtle !== 'function') {\n throw new Error('@_linked/core/shapes/shapesToTurtle has no packageShapesTurtle');\n }\n return mod.packageShapesTurtle(packageName);\n };\n}\n\n/** What one shape sync did, as the boot logs it and `POST /admin/sync-shapes` reports it. */\nexport interface ShapeGraphsReport {\n /** Packages whose shape graph was written, and those left as they were. */\n written: string[];\n skipped: string[];\n /** Packages no longer registered whose shape graph was dropped (`orphanScope: 'all'`). */\n pruned: string[];\n /** The shape graphs written (package graphs, then the app graph), by IRI. */\n graphs: string[];\n /** The app's own shape graph and what the sync did with it; absent without `DATA_ROOT`. */\n appGraph?: {graph: string; status: 'written' | 'skipped' | 'dropped'};\n /**\n * Registered packages with no Turtle, so with no shape graph: an installed package without\n * `shapes.ttl`, or an own package whose shapes could not be generated.\n */\n withoutTurtle: string[];\n /** Shapes an earlier sync had written into the default graph, removed now. */\n cleaned: number;\n /** Set when the store has no graph queries: the default-graph sync's writes. */\n defaultGraph?: number;\n}\n\nconst list = (names: string[]) => (names.length ? names.join(', ') : 'none');\n\n/**\n * Sync the registered shapes into `ds`, one shape graph per package (see the module doc), and log\n * one line naming what was written and skipped, the graphs written, the packages without\n * `shapes.ttl` and the default-graph shapes removed. Throws when the sync fails.\n */\nexport async function syncShapeGraphsInto(\n ds: unknown,\n {\n core,\n orphanScope,\n isOwn,\n dev,\n packageDirOf,\n generate,\n appGraph,\n appPackage,\n read = readShapesTurtle,\n log = console.log,\n warn = console.warn,\n }: {\n core: ShapeGraphCore;\n orphanScope: BootOrphanScope;\n /** Whether a package is the app or one of its workspace packages. */\n isOwn: (packageName: string) => boolean;\n /** Whether the server runs with Vite (dev). */\n dev: boolean;\n /** A package's root directory, when the server knows it. */\n packageDirOf: (packageName: string) => string | undefined;\n /**\n * Generates an own package's Turtle (in dev through Vite's loader); undefined, or a result of\n * undefined, leaves the generation to the core that runs the sync.\n */\n generate?: (packageName: string) => Promise<string | undefined>;\n /** The app's own shape graph (`{DATA_ROOT}/graph/shapes`). */\n appGraph?: string;\n /** The app's package name, recorded on its shape graph's record. */\n appPackage?: string;\n read?: (packageDir: string | undefined) => string | undefined;\n log?: (message: string) => void;\n warn?: (message: string, err?: unknown) => void;\n },\n): Promise<ShapeGraphsReport> {\n const sources = new Map<string, ShapeTurtleSource>();\n for (const name of core.packagesWithShapes()) {\n sources.set(name, shapeTurtleSource(name, {own: isOwn(name), dev, packageDir: packageDirOf(name), read}));\n }\n const noFile = [...sources].filter(([, s]) => s.source === 'none').map(([name]) => name);\n\n const turtleFor = async (name: string): Promise<string | undefined> => {\n const source = sources.get(name);\n if (source?.source === 'file') return source.text;\n if (source?.source !== 'generated' || !generate) return undefined;\n try {\n return await generate(name);\n } catch (err) {\n warn(`[LinkedServer] generating the shapes of ${name} through the dev loader failed; trying the server's core instead:`, err);\n return undefined;\n }\n };\n\n const result = await core.syncShapeGraphs(ds, {\n orphanScope,\n // A package without Turtle is left out: core would generate it otherwise.\n ...(noFile.length ? {packages: [...sources.keys()].filter((n) => !noFile.includes(n))} : {}),\n turtleFor,\n ...(appGraph ? {appShapes: {graph: appGraph, ...(appPackage ? {package: appPackage} : {})}} : {}),\n });\n\n // A package core neither wrote nor skipped got no Turtle at all: generation produced nothing\n // (in dev, the server's core may not hold the source shapes the dev loader registered). The\n // default-graph sync (a store without graph queries) reports no packages, so it is not checked.\n const covered = new Set([...result.written, ...result.skipped]);\n const notGenerated =\n result.defaultGraph !== undefined\n ? []\n : [...sources.keys()].filter((n) => !noFile.includes(n) && !covered.has(n));\n if (notGenerated.length) {\n warn(\n `[LinkedServer] no shapes could be generated for ${list(notGenerated.sort())}, so ` +\n `${notGenerated.length === 1 ? 'it has' : 'they have'} no shape graph this sync`,\n );\n }\n const withoutTurtle = [...noFile, ...notGenerated].sort();\n\n const report: ShapeGraphsReport = {\n written: result.written,\n skipped: result.skipped,\n pruned: result.pruned,\n graphs: result.written.map((name) => core.shapesGraphFor(name)),\n withoutTurtle,\n cleaned: result.cleaned,\n };\n if (appGraph && result.appGraph) {\n report.appGraph = {graph: appGraph, status: result.appGraph};\n if (result.appGraph === 'written') report.graphs.push(appGraph);\n }\n if (result.defaultGraph !== undefined) report.defaultGraph = result.defaultGraph;\n\n if (report.defaultGraph !== undefined) {\n log(\n `[LinkedServer] shape sync (default graph: the store has no graph queries): ` +\n `${report.defaultGraph} shape(s) written`,\n );\n } else {\n log(\n `[LinkedServer] shape graph sync: written ${list(report.written)}; skipped ${list(report.skipped)}` +\n (report.appGraph ? `; app graph ${report.appGraph.status}` : '; no app graph (DATA_ROOT not set)') +\n (report.graphs.length ? `; graphs written ${list(report.graphs)}` : '') +\n (orphanScope === 'all' ? `; pruned ${list(report.pruned)}` : '') +\n (report.cleaned ? `; ${report.cleaned} default-graph shape(s) removed` : '') +\n (withoutTurtle.length ? `; no shapes.ttl (no shape graph): ${list(withoutTurtle)}` : ''),\n );\n }\n if (orphanScope === 'all' && withoutTurtle.length) {\n warn(\n '[LinkedServer] orphaned shape graphs were not pruned: core prunes only when the sync covers ' +\n `every registered package, and ${list(withoutTurtle)} has no Turtle`,\n );\n }\n return report;\n}\n\n/**\n * @deprecated The default-graph shape sync's callback type; the server now syncs shape graphs with\n * {@link syncShapeGraphsInto}. Kept so code built against an earlier server still compiles.\n */\nexport type SyncShapesFn = (\n ds: unknown,\n options: {orphanScope: BootOrphanScope},\n) => Promise<Array<() => Promise<void>>>;\n\n/**\n * @deprecated Use {@link syncShapeGraphsInto}, which writes each package's shapes into its own\n * shape graph. This runs the old default-graph sync: it plans every registered shape with\n * `syncShapes` (the orphan policy from {@link bootOrphanScope}) and runs the writes in batches\n * of 8. Kept so code built against an earlier server keeps working.\n */\nexport async function materializeShapes(\n ds: unknown,\n syncShapes: SyncShapesFn,\n {\n env = process.env,\n log = console.log,\n }: {\n env?: Record<string, string | undefined>;\n log?: (message: string) => void;\n } = {},\n): Promise<{count: number; orphanScope: BootOrphanScope}> {\n const orphanScope = bootOrphanScope(env);\n const thunks = await syncShapes(ds, {orphanScope});\n for (let i = 0; i < thunks.length; i += 8) {\n await Promise.all(thunks.slice(i, i + 8).map((run) => run()));\n }\n log(\n `[LinkedServer] synced shapes into the default graph with orphanScope '${orphanScope}': ` +\n `${thunks.length} operation(s)`,\n );\n return {count: thunks.length, orphanScope};\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": "3.
|
|
13
|
+
"version": "3.9.0",
|
|
14
14
|
"packageManager": "npm@12.2.0",
|
|
15
15
|
"linkedPackage": true,
|
|
16
16
|
"type": "module",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@_linked/cli": "^1.54.0",
|
|
42
|
-
"@_linked/core": "^2.
|
|
43
|
-
"@_linked/server-utils": "^1.
|
|
42
|
+
"@_linked/core": "^2.39.1",
|
|
43
|
+
"@_linked/server-utils": "^1.16.1",
|
|
44
44
|
"autoprefixer": "^10.4.18",
|
|
45
45
|
"compression": "^1.7.4",
|
|
46
46
|
"cors": "^2.8.5",
|