@_linked/core 2.25.0 → 2.27.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +27 -0
  3. package/lib/esm/index.d.ts +9 -0
  4. package/lib/esm/index.js +8 -0
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/esm/interfaces/IDataset.d.ts +15 -0
  7. package/lib/esm/interfaces/IDataset.js.map +1 -1
  8. package/lib/esm/live/LiveQueryStore.d.ts +240 -0
  9. package/lib/esm/live/LiveQueryStore.js +729 -0
  10. package/lib/esm/live/LiveQueryStore.js.map +1 -0
  11. package/lib/esm/live/changes.d.ts +30 -0
  12. package/lib/esm/live/changes.js +30 -0
  13. package/lib/esm/live/changes.js.map +1 -0
  14. package/lib/esm/live/keys.d.ts +54 -0
  15. package/lib/esm/live/keys.js +112 -0
  16. package/lib/esm/live/keys.js.map +1 -0
  17. package/lib/esm/live/matcher.d.ts +43 -0
  18. package/lib/esm/live/matcher.js +115 -0
  19. package/lib/esm/live/matcher.js.map +1 -0
  20. package/lib/esm/live/registry.d.ts +24 -0
  21. package/lib/esm/live/registry.js +41 -0
  22. package/lib/esm/live/registry.js.map +1 -0
  23. package/lib/esm/live.d.ts +12 -0
  24. package/lib/esm/live.js +15 -0
  25. package/lib/esm/live.js.map +1 -0
  26. package/lib/esm/paths/normalizePropertyPath.js +3 -2
  27. package/lib/esm/paths/normalizePropertyPath.js.map +1 -1
  28. package/lib/esm/queries/AskBuilder.d.ts +10 -0
  29. package/lib/esm/queries/AskBuilder.js +14 -0
  30. package/lib/esm/queries/AskBuilder.js.map +1 -1
  31. package/lib/esm/queries/CountBuilder.d.ts +10 -0
  32. package/lib/esm/queries/CountBuilder.js +14 -0
  33. package/lib/esm/queries/CountBuilder.js.map +1 -1
  34. package/lib/esm/queries/QueryBuilder.d.ts +10 -0
  35. package/lib/esm/queries/QueryBuilder.js +14 -0
  36. package/lib/esm/queries/QueryBuilder.js.map +1 -1
  37. package/lib/esm/queries/QueryContext.js +9 -2
  38. package/lib/esm/queries/QueryContext.js.map +1 -1
  39. package/lib/esm/queries/queryDependencies.d.ts +58 -0
  40. package/lib/esm/queries/queryDependencies.js +494 -0
  41. package/lib/esm/queries/queryDependencies.js.map +1 -0
  42. package/lib/esm/queries/queryDispatch.d.ts +46 -0
  43. package/lib/esm/queries/queryDispatch.js +88 -3
  44. package/lib/esm/queries/queryDispatch.js.map +1 -1
  45. package/lib/esm/shapes/nodeShapeData.d.ts +16 -0
  46. package/lib/esm/shapes/nodeShapeData.js +41 -1
  47. package/lib/esm/shapes/nodeShapeData.js.map +1 -1
  48. package/lib/esm/shapes/relationShape.d.ts +35 -0
  49. package/lib/esm/shapes/relationShape.js +88 -0
  50. package/lib/esm/shapes/relationShape.js.map +1 -0
  51. package/lib/esm/shapes/validation.js +8 -4
  52. package/lib/esm/shapes/validation.js.map +1 -1
  53. package/lib/esm/sparql/irToAlgebra.js +5 -40
  54. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  55. package/lib/esm/utils/LinkedStorage.d.ts +6 -0
  56. package/lib/esm/utils/LinkedStorage.js +32 -6
  57. package/lib/esm/utils/LinkedStorage.js.map +1 -1
  58. package/lib/esm/utils/ShapeClass.d.ts +26 -0
  59. package/lib/esm/utils/ShapeClass.js +128 -0
  60. package/lib/esm/utils/ShapeClass.js.map +1 -1
  61. package/package.json +6 -2
@@ -101,10 +101,94 @@ export function resolveCount(target, query) {
101
101
  // Whichever copy runs the storage config sets it; every copy reads it.
102
102
  const dispatchGlobal = typeof globalThis !== 'undefined' ? globalThis : {};
103
103
  if (!('__linkedQueryDispatch' in dispatchGlobal)) {
104
- dispatchGlobal.__linkedQueryDispatch = { current: null };
104
+ dispatchGlobal.__linkedQueryDispatch = {
105
+ current: null,
106
+ listeners: new Set(),
107
+ };
108
+ }
109
+ else if (!dispatchGlobal.__linkedQueryDispatch.listeners) {
110
+ // An older copy of this module created the record before listeners existed.
111
+ dispatchGlobal.__linkedQueryDispatch.listeners = new Set();
112
+ }
113
+ /**
114
+ * Observe every query that runs through the dispatch. Returns an unsubscribe function.
115
+ *
116
+ * Listeners are notified **synchronously at call time**, as soon as the store has
117
+ * been handed the query, with the result promise attached — so an observer can both
118
+ * see what was asked and await what came back. Listeners never alter the result:
119
+ * the promise the caller receives is the store's own, whatever any listener does
120
+ * with it. A listener that throws is logged (`console.error`) and isolated — it
121
+ * neither rejects the query nor stops later listeners.
122
+ *
123
+ * The listener set lives on `globalThis`, beside `current`, for the same reason the
124
+ * dispatch itself does: core can be evaluated twice in dev, and a subscription made
125
+ * through one copy must see queries dispatched through the other.
126
+ */
127
+ export function subscribeQueryDispatch(listener) {
128
+ const listeners = dispatchGlobal.__linkedQueryDispatch.listeners;
129
+ listeners.add(listener);
130
+ return () => {
131
+ listeners.delete(listener);
132
+ };
133
+ }
134
+ function notifyDispatch(event) {
135
+ const listeners = dispatchGlobal.__linkedQueryDispatch.listeners;
136
+ // Snapshot so a listener that (un)subscribes during notification can't mutate
137
+ // the set mid-iteration.
138
+ for (const listener of [...listeners]) {
139
+ try {
140
+ listener(event);
141
+ }
142
+ catch (err) {
143
+ console.error('[linked] query dispatch listener failed', err);
144
+ }
145
+ }
146
+ }
147
+ /** Marker on a dispatch object that is already wrapped by {@link instrumentDispatch}. */
148
+ const INSTRUMENTED = '__instrumented';
149
+ /**
150
+ * Wrap a dispatch so every method call is reported to the listeners. Each method
151
+ * forwards to `d`, normalises the return to a promise, notifies, and returns that same
152
+ * promise. Idempotent: an already-instrumented dispatch is returned as is, so re-setting
153
+ * the current dispatch with itself never double-reports.
154
+ */
155
+ function instrumentDispatch(d, target) {
156
+ if (d === null || d === void 0 ? void 0 : d[INSTRUMENTED])
157
+ return d;
158
+ // `target` is only set for an explicit `exec(target)`: the wrapper is created
159
+ // per call and the user's dataset object is never marked or mutated.
160
+ const wrapped = {
161
+ selectQuery(query) {
162
+ const result = Promise.resolve(d.selectQuery(query));
163
+ notifyDispatch({ kind: 'select', query, result, target });
164
+ return result;
165
+ },
166
+ askQuery(query) {
167
+ const result = Promise.resolve(d.askQuery(query));
168
+ notifyDispatch({ kind: 'ask', query, result, target });
169
+ return result;
170
+ },
171
+ createQuery(query) {
172
+ const result = Promise.resolve(d.createQuery(query));
173
+ notifyDispatch({ kind: 'create', query, result, target });
174
+ return result;
175
+ },
176
+ updateQuery(query) {
177
+ const result = Promise.resolve(d.updateQuery(query));
178
+ notifyDispatch({ kind: 'update', query, result, target });
179
+ return result;
180
+ },
181
+ deleteQuery(query) {
182
+ const result = Promise.resolve(d.deleteQuery(query));
183
+ notifyDispatch({ kind: 'delete', query, result, target });
184
+ return result;
185
+ },
186
+ };
187
+ Object.defineProperty(wrapped, INSTRUMENTED, { value: true, enumerable: false });
188
+ return wrapped;
105
189
  }
106
190
  export function setQueryDispatch(d) {
107
- dispatchGlobal.__linkedQueryDispatch.current = d;
191
+ dispatchGlobal.__linkedQueryDispatch.current = instrumentDispatch(d);
108
192
  }
109
193
  export function getQueryDispatch() {
110
194
  const dispatch = dispatchGlobal.__linkedQueryDispatch.current;
@@ -128,6 +212,7 @@ export function resolveMutationDispatch(kind, target) {
128
212
  if (typeof target[`${kind}Query`] !== 'function') {
129
213
  throw new Error(`The target dataset does not support ${kind} queries.`);
130
214
  }
131
- return target;
215
+ // Instrumented so `builder.exec(target)` is observed like the global path.
216
+ return instrumentDispatch(target, target);
132
217
  }
133
218
  //# sourceMappingURL=queryDispatch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"queryDispatch.js","sourceRoot":"","sources":["../../../src/queries/queryDispatch.ts"],"names":[],"mappings":"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","sourcesContent":["import type {SelectQuery} from './SelectQuery.js';\nimport type {CreateQuery} from './CreateQuery.js';\nimport type {UpdateQuery} from './UpdateQuery.js';\nimport type {DeleteQuery, DeleteResponse} from './DeleteQuery.js';\nimport type {IDataset} from '../interfaces/IDataset.js';\nimport type {AskQuery} from './AskQuery.js';\nimport type {CountQuery} from './CountQuery.js';\n\n/**\n * Abstraction boundary between the DSL layer (Shape) and the storage layer\n * (LinkedStorage / IDataset). Both sides import this leaf module; neither\n * imports the other.\n *\n * Return types are intentionally `any` — the DSL layer threads precise\n * result types through its own generics; the dispatch is a runtime bridge.\n */\nexport interface QueryDispatch {\n /**\n * Answer a select query — **and a count**, which travels this channel too: a\n * count is a select with an aggregate projection, not a query form of its own, so\n * it needs no method here and no router has to grow an arm for it. It arrives as a\n * {@link CountQuery} and resolves to a `number`; see {@link resolveCount}.\n */\n selectQuery<R = any>(query: SelectQuery | CountQuery): Promise<R>;\n /** Answer an ask query — a boolean, not a result set. */\n askQuery(query: AskQuery): Promise<boolean>;\n createQuery<R = any>(query: CreateQuery): Promise<R>;\n updateQuery<R = any>(query: UpdateQuery): Promise<R>;\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse>;\n}\n\n/** A target that can answer an ask query. */\ntype ExistenceTarget = {askQuery(query: AskQuery): Promise<boolean>};\n\n/**\n * Answer an ask query against `target` — the single entry point for every\n * boolean-answered query in the library. `AskBuilder.exec()` and\n * `LinkedStorage.askQuery` both come here rather than calling `askQuery`\n * directly, so the contract below is enforced once for every store.\n *\n * There is deliberately **no translation to a select query anywhere in this\n * package.** An ask goes to `IDataset.askQuery` and a SPARQL-backed store turns\n * it into `ASK`. A store that lacks a boolean primitive implements `askQuery`\n * itself, in whatever way its backend allows — that decision belongs to the\n * store, and making it here would hide it.\n *\n * `askQuery` must resolve to a real boolean: anything else rejects rather than\n * being coerced, since a truthy non-boolean would read as \"exists\". Errors\n * propagate for the same reason — \"could not ask\" is never `false`.\n */\nexport async function resolveExistence(\n target: ExistenceTarget,\n query: AskQuery,\n): Promise<boolean> {\n if (typeof target?.askQuery !== 'function') {\n throw new Error(\n 'This dataset does not implement the required IDataset.askQuery(query). ' +\n 'An ask query is answered with a boolean — a SPARQL store emits ASK — and is ' +\n 'never rewritten as a select on its behalf.',\n );\n }\n const answer = await target.askQuery(query);\n if (typeof answer !== 'boolean') {\n throw new Error(\n `askQuery must resolve to a boolean; got ${answer === null ? 'null' : typeof answer}. ` +\n 'An ask query will not coerce a non-boolean into an answer — a truthy ' +\n 'value would silently read as \"exists\".',\n );\n }\n return answer;\n}\n\n/**\n * A target that can answer a count query — which is to say, any target that can\n * answer a select. There is no count-specific member to look for.\n */\ntype CountTarget = {selectQuery?(query: any): Promise<any>};\n\n/**\n * Answer a count query against `target` — the single entry point for every\n * number-answered query in the library, as {@link resolveExistence} is for boolean\n * ones. `CountBuilder.exec()` comes here rather than calling the store directly, so\n * the contract below is enforced once for every store.\n *\n * **A count is dispatched over the SELECT channel.** It is not a separate query\n * form: `SELECT (COUNT(DISTINCT ?s) AS ?count) WHERE { … }` is a select with an\n * aggregate projection, run over the same transport and answered with the same\n * result-set response — which is exactly why an ask, which really is a different\n * form (`ASK WHERE { … }`), has its own `IDataset.askQuery` and a count does not.\n * Riding `selectQuery` is what lets a count reach a store through every router that\n * already forwards a select, rather than needing a new arm in each of them (the\n * arm `LinkedStorage` never grew, which is how a count came to fail on every\n * application path while passing against a store held directly).\n *\n * There is still deliberately **no translation to a row query anywhere in this\n * package.** A store receives the count query itself and decides how to answer it;\n * one extending `SparqlDataset` branches on the lowered IR and emits the aggregate.\n * Nothing here fetches every row and measures the array — that would hide an\n * unbounded read behind a call that looks cheap.\n *\n * The answer must be a **finite, non-negative integer**: anything else rejects\n * rather than being coerced. That validation lives here, at the dispatch, because\n * this is where the count result is read — and it is load-bearing. Errors propagate\n * for the same reason a failed ask is never `false`, and more sharply, because `0`\n * is a *plausible* count: a count that reported an unreachable store as `0` would\n * render an empty table that looks exactly like real data. A store that ignored the\n * count and answered with rows is caught by the same check.\n */\nexport async function resolveCount(\n target: CountTarget,\n query: CountQuery,\n): Promise<number> {\n if (typeof target?.selectQuery !== 'function') {\n throw new Error(\n 'This dataset does not implement IDataset.selectQuery(query), which is how a ' +\n 'count query is dispatched: a count is a select with an aggregate projection ' +\n '(a SPARQL store emits SELECT (COUNT(DISTINCT ?s) AS ?count)), not a query ' +\n 'form of its own.',\n );\n }\n const answer = await target.selectQuery(query);\n if (typeof answer !== 'number' || !Number.isInteger(answer) || answer < 0) {\n throw new Error(\n `A count query must be answered with a non-negative integer; got ${\n answer === null\n ? 'null'\n : typeof answer === 'number'\n ? String(answer)\n : Array.isArray(answer)\n ? 'an array of rows'\n : typeof answer\n }. It is not coerced — a NaN or a missing value that fell through as 0 would ` +\n 'silently read as \"no matches\". A store that answered with rows ran the ' +\n 'pattern as a row query instead of counting it: it must branch on the lowered ' +\n \"IR's `kind === 'count'` and emit an aggregate.\",\n );\n }\n return answer;\n}\n\n// Global-backed so it is SHARED across duplicate copies of this module. In dev,\n// `@_linked/core` can be evaluated twice (Vite/`src` + Node/`lib` — an accepted\n// 2-instance state, see report-011). The module REGISTRY already lives on the\n// shared global; the query dispatch must too, or `setDefaultDataset()` on one\n// copy is invisible to queries on the other (\"No query dispatch configured\").\n// Whichever copy runs the storage config sets it; every copy reads it.\nconst dispatchGlobal: any =\n typeof globalThis !== 'undefined' ? globalThis : ({} as any);\nif (!('__linkedQueryDispatch' in dispatchGlobal)) {\n dispatchGlobal.__linkedQueryDispatch = {current: null as QueryDispatch | null};\n}\n\nexport function setQueryDispatch(d: QueryDispatch): void {\n dispatchGlobal.__linkedQueryDispatch.current = d;\n}\n\nexport function getQueryDispatch(): QueryDispatch {\n const dispatch = dispatchGlobal.__linkedQueryDispatch.current as QueryDispatch | null;\n if (!dispatch) {\n throw new Error(\n 'No query dispatch configured. Call LinkedStorage.setDefaultDataset() first.',\n );\n }\n return dispatch;\n}\n\n/** The mutating query kinds — the `IDataset` methods that are optional per-store. */\nexport type MutationKind = 'create' | 'update' | 'delete';\n\n/**\n * Pick the object a mutation `exec(target?)` dispatches through: the explicit `target`\n * dataset (validated to implement the op) when a target is given, otherwise the global\n * dispatch. Unlike `selectQuery`, a store's mutation methods are optional on `IDataset`,\n * so a target that can't perform `kind` is a caller error.\n *\n * Throws synchronously if the target lacks the method — callers invoke this from within an\n * `async exec`, so the throw surfaces as a rejected promise rather than a synchronous throw.\n */\nexport function resolveMutationDispatch(\n kind: MutationKind,\n target?: IDataset,\n): QueryDispatch {\n if (!target) return getQueryDispatch();\n if (typeof target[`${kind}Query`] !== 'function') {\n throw new Error(`The target dataset does not support ${kind} queries.`);\n }\n return target as unknown as QueryDispatch;\n}\n"]}
1
+ {"version":3,"file":"queryDispatch.js","sourceRoot":"","sources":["../../../src/queries/queryDispatch.ts"],"names":[],"mappings":"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","sourcesContent":["import type {SelectQuery} from './SelectQuery.js';\nimport type {CreateQuery} from './CreateQuery.js';\nimport type {UpdateQuery} from './UpdateQuery.js';\nimport type {DeleteQuery, DeleteResponse} from './DeleteQuery.js';\nimport type {IDataset} from '../interfaces/IDataset.js';\nimport type {AskQuery} from './AskQuery.js';\nimport type {CountQuery} from './CountQuery.js';\n\n/**\n * Abstraction boundary between the DSL layer (Shape) and the storage layer\n * (LinkedStorage / IDataset). Both sides import this leaf module; neither\n * imports the other.\n *\n * Return types are intentionally `any` — the DSL layer threads precise\n * result types through its own generics; the dispatch is a runtime bridge.\n */\nexport interface QueryDispatch {\n /**\n * Answer a select query — **and a count**, which travels this channel too: a\n * count is a select with an aggregate projection, not a query form of its own, so\n * it needs no method here and no router has to grow an arm for it. It arrives as a\n * {@link CountQuery} and resolves to a `number`; see {@link resolveCount}.\n */\n selectQuery<R = any>(query: SelectQuery | CountQuery): Promise<R>;\n /** Answer an ask query — a boolean, not a result set. */\n askQuery(query: AskQuery): Promise<boolean>;\n createQuery<R = any>(query: CreateQuery): Promise<R>;\n updateQuery<R = any>(query: UpdateQuery): Promise<R>;\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse>;\n}\n\n/** A target that can answer an ask query. */\ntype ExistenceTarget = {askQuery(query: AskQuery): Promise<boolean>};\n\n/**\n * Answer an ask query against `target` — the single entry point for every\n * boolean-answered query in the library. `AskBuilder.exec()` and\n * `LinkedStorage.askQuery` both come here rather than calling `askQuery`\n * directly, so the contract below is enforced once for every store.\n *\n * There is deliberately **no translation to a select query anywhere in this\n * package.** An ask goes to `IDataset.askQuery` and a SPARQL-backed store turns\n * it into `ASK`. A store that lacks a boolean primitive implements `askQuery`\n * itself, in whatever way its backend allows — that decision belongs to the\n * store, and making it here would hide it.\n *\n * `askQuery` must resolve to a real boolean: anything else rejects rather than\n * being coerced, since a truthy non-boolean would read as \"exists\". Errors\n * propagate for the same reason — \"could not ask\" is never `false`.\n */\nexport async function resolveExistence(\n target: ExistenceTarget,\n query: AskQuery,\n): Promise<boolean> {\n if (typeof target?.askQuery !== 'function') {\n throw new Error(\n 'This dataset does not implement the required IDataset.askQuery(query). ' +\n 'An ask query is answered with a boolean — a SPARQL store emits ASK — and is ' +\n 'never rewritten as a select on its behalf.',\n );\n }\n const answer = await target.askQuery(query);\n if (typeof answer !== 'boolean') {\n throw new Error(\n `askQuery must resolve to a boolean; got ${answer === null ? 'null' : typeof answer}. ` +\n 'An ask query will not coerce a non-boolean into an answer — a truthy ' +\n 'value would silently read as \"exists\".',\n );\n }\n return answer;\n}\n\n/**\n * A target that can answer a count query — which is to say, any target that can\n * answer a select. There is no count-specific member to look for.\n */\ntype CountTarget = {selectQuery?(query: any): Promise<any>};\n\n/**\n * Answer a count query against `target` — the single entry point for every\n * number-answered query in the library, as {@link resolveExistence} is for boolean\n * ones. `CountBuilder.exec()` comes here rather than calling the store directly, so\n * the contract below is enforced once for every store.\n *\n * **A count is dispatched over the SELECT channel.** It is not a separate query\n * form: `SELECT (COUNT(DISTINCT ?s) AS ?count) WHERE { … }` is a select with an\n * aggregate projection, run over the same transport and answered with the same\n * result-set response — which is exactly why an ask, which really is a different\n * form (`ASK WHERE { … }`), has its own `IDataset.askQuery` and a count does not.\n * Riding `selectQuery` is what lets a count reach a store through every router that\n * already forwards a select, rather than needing a new arm in each of them (the\n * arm `LinkedStorage` never grew, which is how a count came to fail on every\n * application path while passing against a store held directly).\n *\n * There is still deliberately **no translation to a row query anywhere in this\n * package.** A store receives the count query itself and decides how to answer it;\n * one extending `SparqlDataset` branches on the lowered IR and emits the aggregate.\n * Nothing here fetches every row and measures the array — that would hide an\n * unbounded read behind a call that looks cheap.\n *\n * The answer must be a **finite, non-negative integer**: anything else rejects\n * rather than being coerced. That validation lives here, at the dispatch, because\n * this is where the count result is read — and it is load-bearing. Errors propagate\n * for the same reason a failed ask is never `false`, and more sharply, because `0`\n * is a *plausible* count: a count that reported an unreachable store as `0` would\n * render an empty table that looks exactly like real data. A store that ignored the\n * count and answered with rows is caught by the same check.\n */\nexport async function resolveCount(\n target: CountTarget,\n query: CountQuery,\n): Promise<number> {\n if (typeof target?.selectQuery !== 'function') {\n throw new Error(\n 'This dataset does not implement IDataset.selectQuery(query), which is how a ' +\n 'count query is dispatched: a count is a select with an aggregate projection ' +\n '(a SPARQL store emits SELECT (COUNT(DISTINCT ?s) AS ?count)), not a query ' +\n 'form of its own.',\n );\n }\n const answer = await target.selectQuery(query);\n if (typeof answer !== 'number' || !Number.isInteger(answer) || answer < 0) {\n throw new Error(\n `A count query must be answered with a non-negative integer; got ${\n answer === null\n ? 'null'\n : typeof answer === 'number'\n ? String(answer)\n : Array.isArray(answer)\n ? 'an array of rows'\n : typeof answer\n }. It is not coerced — a NaN or a missing value that fell through as 0 would ` +\n 'silently read as \"no matches\". A store that answered with rows ran the ' +\n 'pattern as a row query instead of counting it: it must branch on the lowered ' +\n \"IR's `kind === 'count'` and emit an aggregate.\",\n );\n }\n return answer;\n}\n\n// Global-backed so it is SHARED across duplicate copies of this module. In dev,\n// `@_linked/core` can be evaluated twice (Vite/`src` + Node/`lib` — an accepted\n// 2-instance state, see report-011). The module REGISTRY already lives on the\n// shared global; the query dispatch must too, or `setDefaultDataset()` on one\n// copy is invisible to queries on the other (\"No query dispatch configured\").\n// Whichever copy runs the storage config sets it; every copy reads it.\nconst dispatchGlobal: any =\n typeof globalThis !== 'undefined' ? globalThis : ({} as any);\nif (!('__linkedQueryDispatch' in dispatchGlobal)) {\n dispatchGlobal.__linkedQueryDispatch = {\n current: null as QueryDispatch | null,\n listeners: new Set<QueryDispatchListener>(),\n };\n} else if (!dispatchGlobal.__linkedQueryDispatch.listeners) {\n // An older copy of this module created the record before listeners existed.\n dispatchGlobal.__linkedQueryDispatch.listeners = new Set<QueryDispatchListener>();\n}\n\n/**\n * One query passing through the dispatch, reported to {@link subscribeQueryDispatch}\n * listeners. `query` is the very builder handed to the store (same identity), and\n * `result` is the store's answer as a promise — not yet settled when the event fires.\n */\nexport type QueryDispatchEvent = (\n | {kind: 'select'; query: SelectQuery | CountQuery; result: Promise<unknown>}\n | {kind: 'ask'; query: AskQuery; result: Promise<boolean>}\n | {kind: 'create'; query: CreateQuery; result: Promise<unknown>}\n | {kind: 'update'; query: UpdateQuery; result: Promise<unknown>}\n | {kind: 'delete'; query: DeleteQuery; result: Promise<DeleteResponse>}\n) & {\n /** The explicit dataset of an `exec(target)` call; absent for the global dispatch. */\n target?: IDataset;\n};\n\n/** Listener notified for every query that runs through the dispatch. */\nexport type QueryDispatchListener = (event: QueryDispatchEvent) => void;\n\n/**\n * Observe every query that runs through the dispatch. Returns an unsubscribe function.\n *\n * Listeners are notified **synchronously at call time**, as soon as the store has\n * been handed the query, with the result promise attached — so an observer can both\n * see what was asked and await what came back. Listeners never alter the result:\n * the promise the caller receives is the store's own, whatever any listener does\n * with it. A listener that throws is logged (`console.error`) and isolated — it\n * neither rejects the query nor stops later listeners.\n *\n * The listener set lives on `globalThis`, beside `current`, for the same reason the\n * dispatch itself does: core can be evaluated twice in dev, and a subscription made\n * through one copy must see queries dispatched through the other.\n */\nexport function subscribeQueryDispatch(listener: QueryDispatchListener): () => void {\n const listeners = dispatchGlobal.__linkedQueryDispatch.listeners as Set<QueryDispatchListener>;\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nfunction notifyDispatch(event: QueryDispatchEvent): void {\n const listeners = dispatchGlobal.__linkedQueryDispatch.listeners as Set<QueryDispatchListener>;\n // Snapshot so a listener that (un)subscribes during notification can't mutate\n // the set mid-iteration.\n for (const listener of [...listeners]) {\n try {\n listener(event);\n } catch (err) {\n console.error('[linked] query dispatch listener failed', err);\n }\n }\n}\n\n/** Marker on a dispatch object that is already wrapped by {@link instrumentDispatch}. */\nconst INSTRUMENTED = '__instrumented';\n\n/**\n * Wrap a dispatch so every method call is reported to the listeners. Each method\n * forwards to `d`, normalises the return to a promise, notifies, and returns that same\n * promise. Idempotent: an already-instrumented dispatch is returned as is, so re-setting\n * the current dispatch with itself never double-reports.\n */\nfunction instrumentDispatch(d: QueryDispatch, target?: IDataset): QueryDispatch {\n if ((d as any)?.[INSTRUMENTED]) return d;\n // `target` is only set for an explicit `exec(target)`: the wrapper is created\n // per call and the user's dataset object is never marked or mutated.\n const wrapped: QueryDispatch = {\n selectQuery<R = any>(query: SelectQuery | CountQuery): Promise<R> {\n const result = Promise.resolve(d.selectQuery<R>(query));\n notifyDispatch({kind: 'select', query, result, target});\n return result;\n },\n askQuery(query: AskQuery): Promise<boolean> {\n const result = Promise.resolve(d.askQuery(query));\n notifyDispatch({kind: 'ask', query, result, target});\n return result;\n },\n createQuery<R = any>(query: CreateQuery): Promise<R> {\n const result = Promise.resolve(d.createQuery<R>(query));\n notifyDispatch({kind: 'create', query, result, target});\n return result;\n },\n updateQuery<R = any>(query: UpdateQuery): Promise<R> {\n const result = Promise.resolve(d.updateQuery<R>(query));\n notifyDispatch({kind: 'update', query, result, target});\n return result;\n },\n deleteQuery(query: DeleteQuery): Promise<DeleteResponse> {\n const result = Promise.resolve(d.deleteQuery(query));\n notifyDispatch({kind: 'delete', query, result, target});\n return result;\n },\n };\n Object.defineProperty(wrapped, INSTRUMENTED, {value: true, enumerable: false});\n return wrapped;\n}\n\nexport function setQueryDispatch(d: QueryDispatch): void {\n dispatchGlobal.__linkedQueryDispatch.current = instrumentDispatch(d);\n}\n\nexport function getQueryDispatch(): QueryDispatch {\n const dispatch = dispatchGlobal.__linkedQueryDispatch.current as QueryDispatch | null;\n if (!dispatch) {\n throw new Error(\n 'No query dispatch configured. Call LinkedStorage.setDefaultDataset() first.',\n );\n }\n return dispatch;\n}\n\n/** The mutating query kinds — the `IDataset` methods that are optional per-store. */\nexport type MutationKind = 'create' | 'update' | 'delete';\n\n/**\n * Pick the object a mutation `exec(target?)` dispatches through: the explicit `target`\n * dataset (validated to implement the op) when a target is given, otherwise the global\n * dispatch. Unlike `selectQuery`, a store's mutation methods are optional on `IDataset`,\n * so a target that can't perform `kind` is a caller error.\n *\n * Throws synchronously if the target lacks the method — callers invoke this from within an\n * `async exec`, so the throw surfaces as a rejected promise rather than a synchronous throw.\n */\nexport function resolveMutationDispatch(\n kind: MutationKind,\n target?: IDataset,\n): QueryDispatch {\n if (!target) return getQueryDispatch();\n if (typeof target[`${kind}Query`] !== 'function') {\n throw new Error(`The target dataset does not support ${kind} queries.`);\n }\n // Instrumented so `builder.exec(target)` is observed like the global path.\n return instrumentDispatch(target as unknown as QueryDispatch, target);\n}\n"]}
@@ -108,6 +108,22 @@ export declare function createPropertyShapeData(): PropertyShapeData;
108
108
  * as data.
109
109
  */
110
110
  export declare function getPropertyShapes(nodeShape: NodeShapeData, includeSuperClasses?: boolean): PropertyShapeData[];
111
+ /**
112
+ * Find the property shape declared under a property-shape IRI, across every
113
+ * registered node shape.
114
+ *
115
+ * Scans the METAMODEL registry, which holds every shape — authored or data-only.
116
+ * Scanning the class registry meant a data-only property was never found, and the
117
+ * caller then fell through to emitting the PROPERTY SHAPE's IRI as the SPARQL
118
+ * predicate: a silently wrong query that matched nothing, with no error.
119
+ *
120
+ * Cache keyed on the registration version rather than the registry SIZE. Size does not
121
+ * change when a shape is re-registered in place, which is exactly what happens when a
122
+ * shape is edited and its metadata replaced. Only successful resolutions are cached — a
123
+ * not-found is never stored, so a property resolved before its shape registers can
124
+ * still resolve correctly afterwards.
125
+ */
126
+ export declare function findPropertyShapeById(propertyId: string): PropertyShapeData | undefined;
111
127
  /** Property shapes across the inheritance chain, deduped by label (most specific wins). */
112
128
  export declare function getUniquePropertyShapes(nodeShape: NodeShapeData): PropertyShapeData[];
113
129
  /**
@@ -1,4 +1,4 @@
1
- import { getSuperShapes } from '../utils/ShapeClass.js';
1
+ import { getAllNodeShapes, getRegistryVersion, getSuperShapes } from '../utils/ShapeClass.js';
2
2
  // ---------------------------------------------------------------------------
3
3
  // Factories
4
4
  // ---------------------------------------------------------------------------
@@ -60,6 +60,46 @@ export function getPropertyShapes(nodeShape, includeSuperClasses = false) {
60
60
  }
61
61
  return res;
62
62
  }
63
+ // ---------------------------------------------------------------------------
64
+ // Property-shape lookup by id
65
+ // ---------------------------------------------------------------------------
66
+ // The registry scan behind predicate, datatype and dependency resolution, cached on
67
+ // successful lookups only and invalidated by the registry version.
68
+ const propertyShapeCache = new Map();
69
+ let propertyShapeCacheVersion = -1;
70
+ /**
71
+ * Find the property shape declared under a property-shape IRI, across every
72
+ * registered node shape.
73
+ *
74
+ * Scans the METAMODEL registry, which holds every shape — authored or data-only.
75
+ * Scanning the class registry meant a data-only property was never found, and the
76
+ * caller then fell through to emitting the PROPERTY SHAPE's IRI as the SPARQL
77
+ * predicate: a silently wrong query that matched nothing, with no error.
78
+ *
79
+ * Cache keyed on the registration version rather than the registry SIZE. Size does not
80
+ * change when a shape is re-registered in place, which is exactly what happens when a
81
+ * shape is edited and its metadata replaced. Only successful resolutions are cached — a
82
+ * not-found is never stored, so a property resolved before its shape registers can
83
+ * still resolve correctly afterwards.
84
+ */
85
+ export function findPropertyShapeById(propertyId) {
86
+ const version = getRegistryVersion();
87
+ if (version !== propertyShapeCacheVersion) {
88
+ propertyShapeCache.clear();
89
+ propertyShapeCacheVersion = version;
90
+ }
91
+ const cached = propertyShapeCache.get(propertyId);
92
+ if (cached)
93
+ return cached;
94
+ for (const nodeShape of getAllNodeShapes().values()) {
95
+ const propertyShape = getPropertyShapes(nodeShape, true).find((prop) => prop.id === propertyId);
96
+ if (propertyShape) {
97
+ propertyShapeCache.set(propertyId, propertyShape);
98
+ return propertyShape;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
63
103
  /** Property shapes across the inheritance chain, deduped by label (most specific wins). */
64
104
  export function getUniquePropertyShapes(nodeShape) {
65
105
  const unique = [];
@@ -1 +1 @@
1
- {"version":3,"file":"nodeShapeData.js","sourceRoot":"","sources":["../../../src/shapes/nodeShapeData.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n */\nimport type {NodeReferenceValue} from '../utils/NodeReference.js';\nimport type {PathExpr} from '../paths/PropertyPathExpr.js';\nimport {getSuperShapes} from '../utils/ShapeClass.js';\n\n/**\n * Plain-object SHACL metadata — the QResult-like shape of a `sh:PropertyShape`.\n *\n * Shapes are metadata, not data: a shape class's `.shape` and its property shapes\n * are plain objects (no class instance, no methods). Operations that used to be\n * instance methods now live as free functions in this module.\n */\nexport interface PropertyShapeData {\n id: string;\n label: string;\n path: PathExpr;\n nodeKind?: NodeReferenceValue;\n datatype?: NodeReferenceValue;\n minCount?: number;\n maxCount?: number;\n name?: string;\n description?: string;\n order?: number;\n group?: string;\n /**\n * `core:displayRank` — a single linear importance rank, lower = more\n * important. Truncated display contexts (\"the top 3 properties\") derive from it.\n * Distinct from `order`, which is arrangement rather than importance.\n */\n displayRank?: number;\n /** `core:displayHidden` — omit from generic rendering. */\n displayHidden?: boolean;\n class?: NodeReferenceValue;\n in?: (NodeReferenceValue | string | number | boolean)[];\n equalsConstraint?: NodeReferenceValue;\n disjoint?: NodeReferenceValue;\n lessThan?: NodeReferenceValue;\n lessThanOrEquals?: NodeReferenceValue;\n /** Value-range constraints (sh:minInclusive / sh:maxInclusive / sh:minExclusive / sh:maxExclusive). */\n minInclusive?: number;\n maxInclusive?: number;\n minExclusive?: number | string;\n maxExclusive?: number;\n /** String-length constraints (sh:minLength / sh:maxLength). */\n minLength?: number;\n maxLength?: number;\n /** Regex constraint (sh:pattern); serialized as its source string. */\n pattern?: RegExp;\n hasValueConstraint?: NodeReferenceValue | string | number | boolean;\n defaultValue?: unknown;\n sortBy?: PathExpr;\n valueShape?: NodeReferenceValue;\n /** Composition marker: the value(s) of this property are owned by the subject. */\n contains?: boolean;\n parentNodeShape?: NodeShapeData;\n}\n\n/**\n * Plain-object SHACL metadata — the QResult-like shape of a `sh:NodeShape`.\n */\nexport interface NodeShapeData {\n id: string;\n label?: string;\n description?: string;\n targetClass?: NodeReferenceValue;\n extends?: NodeReferenceValue;\n /** Composition marker: instances are dependent (cascade-deletable via `contains`). */\n dependent?: boolean;\n /** sh:closed — target nodes with undeclared properties are invalid. */\n closed?: boolean;\n /** sh:ignoredProperties — extra properties permitted when the shape is closed. */\n ignoredProperties?: NodeReferenceValue[];\n propertyShapes: PropertyShapeData[];\n}\n\n/**\n * Result object produced by `propertyShapeToResult()` (SHACL projection).\n * @deprecated No production callers remain (the `NodeShape.properties` getter and\n * `PropertyShape.getResult()` were removed with the plain-object conversion). Read\n * the plain `PropertyShapeData` fields directly. Scheduled for removal.\n */\nexport interface PropertyShapeResult {\n id: string;\n label: string;\n path: PathExpr;\n nodeKind?: NodeReferenceValue;\n datatype?: NodeReferenceValue;\n minCount?: number;\n maxCount?: number;\n name?: string;\n description?: string;\n order?: number;\n group?: string;\n class?: NodeReferenceValue;\n in?: (NodeReferenceValue | string | number | boolean)[];\n equals?: NodeReferenceValue;\n disjoint?: NodeReferenceValue;\n lessThan?: NodeReferenceValue;\n lessThanOrEquals?: NodeReferenceValue;\n hasValue?: NodeReferenceValue | string | number | boolean;\n defaultValue?: unknown;\n sortBy?: PathExpr;\n valueShape?: NodeReferenceValue;\n}\n\n// ---------------------------------------------------------------------------\n// Factories\n// ---------------------------------------------------------------------------\n\n/** Create an empty NodeShape metadata object for the given shape IRI. */\nexport function createNodeShapeData(id: string): NodeShapeData {\n return {id, propertyShapes: []};\n}\n\n/** Create a blank PropertyShape metadata object (fields filled in by the caller). */\nexport function createPropertyShapeData(): PropertyShapeData {\n return {id: '', label: '', path: null as unknown as PathExpr};\n}\n\n// ---------------------------------------------------------------------------\n// Free functions (formerly NodeShape/PropertyShape instance methods)\n// ---------------------------------------------------------------------------\n\n/** One-time warning keys for shapes whose `propertyShapes` is missing/invalid. */\nconst warnedMissingPropertyShapes = new Set<string>();\n\n/**\n * Read a node shape's own property shapes, tolerating a missing/invalid array.\n *\n * A non-array `propertyShapes` almost always means a duplicate `@_linked/core`\n * install or a non-normalized static shape on a superclass; warn once per shape id\n * (diagnostic parity with the former `NodeShape.listPropertyShapesSafe`) and treat\n * it as empty.\n */\nfunction ownPropertyShapes(nodeShape: NodeShapeData): PropertyShapeData[] {\n const own = (nodeShape as {propertyShapes?: PropertyShapeData[]}).propertyShapes;\n if (Array.isArray(own)) {\n return own;\n }\n const id = (nodeShape as {id?: string}).id ?? '';\n if (!warnedMissingPropertyShapes.has(id)) {\n warnedMissingPropertyShapes.add(id);\n console.warn(\n `[@_linked/core] static shape ${id ? `'${id}'` : '(unknown id)'} has missing ` +\n `or invalid propertyShapes. Treating as []. Often caused by duplicate ` +\n `@_linked/core installs or a non-normalized static shape on a superclass.`,\n );\n }\n return [];\n}\n\n/**\n * Property shapes declared on this NodeShape. With `includeSuperClasses`, walks the\n * inheritance chain via `getSuperShapes` and concatenates each ancestor's own property\n * shapes — the prototype chain for a class-backed shape, `extends` for one known only\n * as data.\n */\nexport function getPropertyShapes(\n nodeShape: NodeShapeData,\n includeSuperClasses: boolean = false,\n): PropertyShapeData[] {\n if (!includeSuperClasses) {\n return [...ownPropertyShapes(nodeShape)];\n }\n // One inheritance walk, shared with getSuperShapes: the prototype chain for a\n // class-backed shape (which includes the framework `Shape` root and its `label` /\n // `type` properties), or `extends` through the registry for a shape that exists only\n // as data. Previously this walked the prototype chain directly and returned ONLY own\n // properties when no class existed — so a project-authored shape silently lost\n // everything it inherited.\n const res: PropertyShapeData[] = [...ownPropertyShapes(nodeShape)];\n for (const superShape of getSuperShapes(nodeShape)) {\n res.push(...ownPropertyShapes(superShape));\n }\n return res;\n}\n\n/** Property shapes across the inheritance chain, deduped by label (most specific wins). */\nexport function getUniquePropertyShapes(\n nodeShape: NodeShapeData,\n): PropertyShapeData[] {\n const unique: PropertyShapeData[] = [];\n const seen = new Set<string>();\n for (const ps of getPropertyShapes(nodeShape, true)) {\n if (!seen.has(ps.label)) {\n seen.add(ps.label);\n unique.push(ps);\n }\n }\n return unique;\n}\n\n/**\n * Find a property shape by label. With `checkSubShapes` (default true), ascends the\n * inheritance chain if the label isn't found locally.\n *\n * Shares `getSuperShapes` with {@link getPropertyShapes}, so the singular and plural\n * lookups can never disagree about what a shape inherits — and so a shape registered\n * from data resolves inherited properties through `extends` rather than stopping at its\n * own. This is what `getPropertyShapeByLabel` delegates to (PR #211), so closing the gap\n * here closes it for the query proxies too. See docs/backlog/040.\n */\nexport function getPropertyShape(\n nodeShape: NodeShapeData,\n label: string,\n checkSubShapes: boolean = true,\n): PropertyShapeData | undefined {\n const own = ownPropertyShapes(nodeShape).find((ps) => ps.label === label);\n if (own || !checkSubShapes) return own;\n for (const superShape of getSuperShapes(nodeShape)) {\n const inherited = ownPropertyShapes(superShape).find(\n (ps) => ps.label === label,\n );\n if (inherited) return inherited;\n }\n return undefined;\n}\n\n/** Two node shapes are equal when they share the same IRI. */\nexport function nodeShapeEquals(a: NodeShapeData, b?: NodeShapeData): boolean {\n return !!b && a?.id === b.id;\n}\n\n/** Append a property shape to a node shape, wiring the back-reference. */\nexport function addPropertyShape(\n nodeShape: NodeShapeData,\n propertyShape: PropertyShapeData,\n): void {\n propertyShape.parentNodeShape = nodeShape;\n if (!Array.isArray(nodeShape.propertyShapes)) {\n nodeShape.propertyShapes = [];\n }\n nodeShape.propertyShapes.push(propertyShape);\n}\n\n/** Shallow-clone a property shape (used by property override / disallow). */\nexport function clonePropertyShape(\n propertyShape: PropertyShapeData,\n): PropertyShapeData {\n return {...propertyShape};\n}\n\n/**\n * Project a property shape to its SHACL result object.\n * @deprecated No production callers remain. Read the plain `PropertyShapeData`\n * fields directly (note this projection renames `equalsConstraint`→`equals`,\n * `hasValueConstraint`→`hasValue`, and serializes `pattern` to its source string).\n * Scheduled for removal.\n */\nexport function propertyShapeToResult(ps: PropertyShapeData): PropertyShapeResult {\n const result: Record<string, unknown> & {id: string; label: string; path: PathExpr} = {\n id: ps.id,\n label: ps.label,\n path: ps.path,\n };\n if (ps.nodeKind) result.nodeKind = ps.nodeKind;\n if (ps.datatype) result.datatype = ps.datatype;\n if (typeof ps.minCount === 'number') result.minCount = ps.minCount;\n if (typeof ps.maxCount === 'number') result.maxCount = ps.maxCount;\n if (ps.name) result.name = ps.name;\n if (ps.description) result.description = ps.description;\n if (typeof ps.order === 'number') result.order = ps.order;\n if (ps.group) result.group = ps.group;\n if (ps.class) result.class = ps.class;\n if (ps.in) result.in = ps.in;\n if (ps.equalsConstraint) result.equals = ps.equalsConstraint;\n if (ps.disjoint) result.disjoint = ps.disjoint;\n if (ps.lessThan) result.lessThan = ps.lessThan;\n if (ps.lessThanOrEquals) result.lessThanOrEquals = ps.lessThanOrEquals;\n if (ps.minInclusive !== undefined) result.minInclusive = ps.minInclusive;\n if (ps.maxInclusive !== undefined) result.maxInclusive = ps.maxInclusive;\n if (ps.minExclusive !== undefined) result.minExclusive = ps.minExclusive;\n if (ps.maxExclusive !== undefined) result.maxExclusive = ps.maxExclusive;\n if (typeof ps.minLength === 'number') result.minLength = ps.minLength;\n if (typeof ps.maxLength === 'number') result.maxLength = ps.maxLength;\n if (ps.pattern) result.pattern = ps.pattern.source;\n if (ps.hasValueConstraint !== undefined) result.hasValue = ps.hasValueConstraint;\n if (ps.defaultValue !== undefined) result.defaultValue = ps.defaultValue;\n if (ps.sortBy) result.sortBy = ps.sortBy;\n if (ps.valueShape) result.valueShape = ps.valueShape;\n return result as PropertyShapeResult;\n}\n"]}
1
+ {"version":3,"file":"nodeShapeData.js","sourceRoot":"","sources":["../../../src/shapes/nodeShapeData.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n */\nimport type {NodeReferenceValue} from '../utils/NodeReference.js';\nimport type {PathExpr} from '../paths/PropertyPathExpr.js';\nimport {getAllNodeShapes, getRegistryVersion, getSuperShapes} from '../utils/ShapeClass.js';\n\n/**\n * Plain-object SHACL metadata — the QResult-like shape of a `sh:PropertyShape`.\n *\n * Shapes are metadata, not data: a shape class's `.shape` and its property shapes\n * are plain objects (no class instance, no methods). Operations that used to be\n * instance methods now live as free functions in this module.\n */\nexport interface PropertyShapeData {\n id: string;\n label: string;\n path: PathExpr;\n nodeKind?: NodeReferenceValue;\n datatype?: NodeReferenceValue;\n minCount?: number;\n maxCount?: number;\n name?: string;\n description?: string;\n order?: number;\n group?: string;\n /**\n * `core:displayRank` — a single linear importance rank, lower = more\n * important. Truncated display contexts (\"the top 3 properties\") derive from it.\n * Distinct from `order`, which is arrangement rather than importance.\n */\n displayRank?: number;\n /** `core:displayHidden` — omit from generic rendering. */\n displayHidden?: boolean;\n class?: NodeReferenceValue;\n in?: (NodeReferenceValue | string | number | boolean)[];\n equalsConstraint?: NodeReferenceValue;\n disjoint?: NodeReferenceValue;\n lessThan?: NodeReferenceValue;\n lessThanOrEquals?: NodeReferenceValue;\n /** Value-range constraints (sh:minInclusive / sh:maxInclusive / sh:minExclusive / sh:maxExclusive). */\n minInclusive?: number;\n maxInclusive?: number;\n minExclusive?: number | string;\n maxExclusive?: number;\n /** String-length constraints (sh:minLength / sh:maxLength). */\n minLength?: number;\n maxLength?: number;\n /** Regex constraint (sh:pattern); serialized as its source string. */\n pattern?: RegExp;\n hasValueConstraint?: NodeReferenceValue | string | number | boolean;\n defaultValue?: unknown;\n sortBy?: PathExpr;\n valueShape?: NodeReferenceValue;\n /** Composition marker: the value(s) of this property are owned by the subject. */\n contains?: boolean;\n parentNodeShape?: NodeShapeData;\n}\n\n/**\n * Plain-object SHACL metadata — the QResult-like shape of a `sh:NodeShape`.\n */\nexport interface NodeShapeData {\n id: string;\n label?: string;\n description?: string;\n targetClass?: NodeReferenceValue;\n extends?: NodeReferenceValue;\n /** Composition marker: instances are dependent (cascade-deletable via `contains`). */\n dependent?: boolean;\n /** sh:closed — target nodes with undeclared properties are invalid. */\n closed?: boolean;\n /** sh:ignoredProperties — extra properties permitted when the shape is closed. */\n ignoredProperties?: NodeReferenceValue[];\n propertyShapes: PropertyShapeData[];\n}\n\n/**\n * Result object produced by `propertyShapeToResult()` (SHACL projection).\n * @deprecated No production callers remain (the `NodeShape.properties` getter and\n * `PropertyShape.getResult()` were removed with the plain-object conversion). Read\n * the plain `PropertyShapeData` fields directly. Scheduled for removal.\n */\nexport interface PropertyShapeResult {\n id: string;\n label: string;\n path: PathExpr;\n nodeKind?: NodeReferenceValue;\n datatype?: NodeReferenceValue;\n minCount?: number;\n maxCount?: number;\n name?: string;\n description?: string;\n order?: number;\n group?: string;\n class?: NodeReferenceValue;\n in?: (NodeReferenceValue | string | number | boolean)[];\n equals?: NodeReferenceValue;\n disjoint?: NodeReferenceValue;\n lessThan?: NodeReferenceValue;\n lessThanOrEquals?: NodeReferenceValue;\n hasValue?: NodeReferenceValue | string | number | boolean;\n defaultValue?: unknown;\n sortBy?: PathExpr;\n valueShape?: NodeReferenceValue;\n}\n\n// ---------------------------------------------------------------------------\n// Factories\n// ---------------------------------------------------------------------------\n\n/** Create an empty NodeShape metadata object for the given shape IRI. */\nexport function createNodeShapeData(id: string): NodeShapeData {\n return {id, propertyShapes: []};\n}\n\n/** Create a blank PropertyShape metadata object (fields filled in by the caller). */\nexport function createPropertyShapeData(): PropertyShapeData {\n return {id: '', label: '', path: null as unknown as PathExpr};\n}\n\n// ---------------------------------------------------------------------------\n// Free functions (formerly NodeShape/PropertyShape instance methods)\n// ---------------------------------------------------------------------------\n\n/** One-time warning keys for shapes whose `propertyShapes` is missing/invalid. */\nconst warnedMissingPropertyShapes = new Set<string>();\n\n/**\n * Read a node shape's own property shapes, tolerating a missing/invalid array.\n *\n * A non-array `propertyShapes` almost always means a duplicate `@_linked/core`\n * install or a non-normalized static shape on a superclass; warn once per shape id\n * (diagnostic parity with the former `NodeShape.listPropertyShapesSafe`) and treat\n * it as empty.\n */\nfunction ownPropertyShapes(nodeShape: NodeShapeData): PropertyShapeData[] {\n const own = (nodeShape as {propertyShapes?: PropertyShapeData[]}).propertyShapes;\n if (Array.isArray(own)) {\n return own;\n }\n const id = (nodeShape as {id?: string}).id ?? '';\n if (!warnedMissingPropertyShapes.has(id)) {\n warnedMissingPropertyShapes.add(id);\n console.warn(\n `[@_linked/core] static shape ${id ? `'${id}'` : '(unknown id)'} has missing ` +\n `or invalid propertyShapes. Treating as []. Often caused by duplicate ` +\n `@_linked/core installs or a non-normalized static shape on a superclass.`,\n );\n }\n return [];\n}\n\n/**\n * Property shapes declared on this NodeShape. With `includeSuperClasses`, walks the\n * inheritance chain via `getSuperShapes` and concatenates each ancestor's own property\n * shapes — the prototype chain for a class-backed shape, `extends` for one known only\n * as data.\n */\nexport function getPropertyShapes(\n nodeShape: NodeShapeData,\n includeSuperClasses: boolean = false,\n): PropertyShapeData[] {\n if (!includeSuperClasses) {\n return [...ownPropertyShapes(nodeShape)];\n }\n // One inheritance walk, shared with getSuperShapes: the prototype chain for a\n // class-backed shape (which includes the framework `Shape` root and its `label` /\n // `type` properties), or `extends` through the registry for a shape that exists only\n // as data. Previously this walked the prototype chain directly and returned ONLY own\n // properties when no class existed — so a project-authored shape silently lost\n // everything it inherited.\n const res: PropertyShapeData[] = [...ownPropertyShapes(nodeShape)];\n for (const superShape of getSuperShapes(nodeShape)) {\n res.push(...ownPropertyShapes(superShape));\n }\n return res;\n}\n\n// ---------------------------------------------------------------------------\n// Property-shape lookup by id\n// ---------------------------------------------------------------------------\n\n// The registry scan behind predicate, datatype and dependency resolution, cached on\n// successful lookups only and invalidated by the registry version.\nconst propertyShapeCache = new Map<string, PropertyShapeData>();\nlet propertyShapeCacheVersion = -1;\n\n/**\n * Find the property shape declared under a property-shape IRI, across every\n * registered node shape.\n *\n * Scans the METAMODEL registry, which holds every shape — authored or data-only.\n * Scanning the class registry meant a data-only property was never found, and the\n * caller then fell through to emitting the PROPERTY SHAPE's IRI as the SPARQL\n * predicate: a silently wrong query that matched nothing, with no error.\n *\n * Cache keyed on the registration version rather than the registry SIZE. Size does not\n * change when a shape is re-registered in place, which is exactly what happens when a\n * shape is edited and its metadata replaced. Only successful resolutions are cached — a\n * not-found is never stored, so a property resolved before its shape registers can\n * still resolve correctly afterwards.\n */\nexport function findPropertyShapeById(propertyId: string): PropertyShapeData | undefined {\n const version = getRegistryVersion();\n if (version !== propertyShapeCacheVersion) {\n propertyShapeCache.clear();\n propertyShapeCacheVersion = version;\n }\n const cached = propertyShapeCache.get(propertyId);\n if (cached) return cached;\n\n for (const nodeShape of getAllNodeShapes().values()) {\n const propertyShape = getPropertyShapes(nodeShape, true).find(\n (prop: {id?: string}) => prop.id === propertyId,\n );\n if (propertyShape) {\n propertyShapeCache.set(propertyId, propertyShape);\n return propertyShape;\n }\n }\n return undefined;\n}\n\n/** Property shapes across the inheritance chain, deduped by label (most specific wins). */\nexport function getUniquePropertyShapes(\n nodeShape: NodeShapeData,\n): PropertyShapeData[] {\n const unique: PropertyShapeData[] = [];\n const seen = new Set<string>();\n for (const ps of getPropertyShapes(nodeShape, true)) {\n if (!seen.has(ps.label)) {\n seen.add(ps.label);\n unique.push(ps);\n }\n }\n return unique;\n}\n\n/**\n * Find a property shape by label. With `checkSubShapes` (default true), ascends the\n * inheritance chain if the label isn't found locally.\n *\n * Shares `getSuperShapes` with {@link getPropertyShapes}, so the singular and plural\n * lookups can never disagree about what a shape inherits — and so a shape registered\n * from data resolves inherited properties through `extends` rather than stopping at its\n * own. This is what `getPropertyShapeByLabel` delegates to (PR #211), so closing the gap\n * here closes it for the query proxies too. See docs/backlog/040.\n */\nexport function getPropertyShape(\n nodeShape: NodeShapeData,\n label: string,\n checkSubShapes: boolean = true,\n): PropertyShapeData | undefined {\n const own = ownPropertyShapes(nodeShape).find((ps) => ps.label === label);\n if (own || !checkSubShapes) return own;\n for (const superShape of getSuperShapes(nodeShape)) {\n const inherited = ownPropertyShapes(superShape).find(\n (ps) => ps.label === label,\n );\n if (inherited) return inherited;\n }\n return undefined;\n}\n\n/** Two node shapes are equal when they share the same IRI. */\nexport function nodeShapeEquals(a: NodeShapeData, b?: NodeShapeData): boolean {\n return !!b && a?.id === b.id;\n}\n\n/** Append a property shape to a node shape, wiring the back-reference. */\nexport function addPropertyShape(\n nodeShape: NodeShapeData,\n propertyShape: PropertyShapeData,\n): void {\n propertyShape.parentNodeShape = nodeShape;\n if (!Array.isArray(nodeShape.propertyShapes)) {\n nodeShape.propertyShapes = [];\n }\n nodeShape.propertyShapes.push(propertyShape);\n}\n\n/** Shallow-clone a property shape (used by property override / disallow). */\nexport function clonePropertyShape(\n propertyShape: PropertyShapeData,\n): PropertyShapeData {\n return {...propertyShape};\n}\n\n/**\n * Project a property shape to its SHACL result object.\n * @deprecated No production callers remain. Read the plain `PropertyShapeData`\n * fields directly (note this projection renames `equalsConstraint`→`equals`,\n * `hasValueConstraint`→`hasValue`, and serializes `pattern` to its source string).\n * Scheduled for removal.\n */\nexport function propertyShapeToResult(ps: PropertyShapeData): PropertyShapeResult {\n const result: Record<string, unknown> & {id: string; label: string; path: PathExpr} = {\n id: ps.id,\n label: ps.label,\n path: ps.path,\n };\n if (ps.nodeKind) result.nodeKind = ps.nodeKind;\n if (ps.datatype) result.datatype = ps.datatype;\n if (typeof ps.minCount === 'number') result.minCount = ps.minCount;\n if (typeof ps.maxCount === 'number') result.maxCount = ps.maxCount;\n if (ps.name) result.name = ps.name;\n if (ps.description) result.description = ps.description;\n if (typeof ps.order === 'number') result.order = ps.order;\n if (ps.group) result.group = ps.group;\n if (ps.class) result.class = ps.class;\n if (ps.in) result.in = ps.in;\n if (ps.equalsConstraint) result.equals = ps.equalsConstraint;\n if (ps.disjoint) result.disjoint = ps.disjoint;\n if (ps.lessThan) result.lessThan = ps.lessThan;\n if (ps.lessThanOrEquals) result.lessThanOrEquals = ps.lessThanOrEquals;\n if (ps.minInclusive !== undefined) result.minInclusive = ps.minInclusive;\n if (ps.maxInclusive !== undefined) result.maxInclusive = ps.maxInclusive;\n if (ps.minExclusive !== undefined) result.minExclusive = ps.minExclusive;\n if (ps.maxExclusive !== undefined) result.maxExclusive = ps.maxExclusive;\n if (typeof ps.minLength === 'number') result.minLength = ps.minLength;\n if (typeof ps.maxLength === 'number') result.maxLength = ps.maxLength;\n if (ps.pattern) result.pattern = ps.pattern.source;\n if (ps.hasValueConstraint !== undefined) result.hasValue = ps.hasValueConstraint;\n if (ps.defaultValue !== undefined) result.defaultValue = ps.defaultValue;\n if (ps.sortBy) result.sortBy = ps.sortBy;\n if (ps.valueShape) result.valueShape = ps.valueShape;\n return result as PropertyShapeResult;\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import type { NodeShapeData, PropertyShapeData } from './nodeShapeData.js';
2
+ import type { NodeShapeWire } from './nodeShapeWire.js';
3
+ /** The fields of a property shape these helpers read; data and wire form both satisfy it. */
4
+ export type RelationPropertyLike = Pick<PropertyShapeData, 'valueShape' | 'class' | 'nodeKind'>;
5
+ /** Shapes to resolve against in place of the registry. Iterated more than once — pass an array. */
6
+ export type ShapeCandidates = Iterable<NodeShapeData | NodeShapeWire>;
7
+ /** How a relation property resolved to the shape its values are viewed through. */
8
+ export interface RelationShapeResolution {
9
+ /** The shape to use, when there is one. */
10
+ shapeId?: string;
11
+ /** Every shape that qualified, in preference order; `shapeId` is the first. */
12
+ candidates: string[];
13
+ /** `node`: declared by `sh:node`. `class`: matched on `sh:class`. `none`: no shape found. */
14
+ source: 'node' | 'class' | 'none';
15
+ }
16
+ /** True when the property's values are nodes (IRIs or blank nodes) rather than literals. */
17
+ export declare function isRelation(p: RelationPropertyLike): boolean;
18
+ /**
19
+ * The class a relation's values are instances of: the declared `sh:class`, else the
20
+ * targetClass of its `sh:node` shape. `undefined` when neither says.
21
+ */
22
+ export declare function rangeClassOf(p: RelationPropertyLike, shapes?: ShapeCandidates): string | undefined;
23
+ /**
24
+ * The shape a relation's values are viewed, picked and created through.
25
+ *
26
+ * A declared `sh:node` wins outright: the relation uses exactly that shape. Otherwise the
27
+ * shapes targeting the property's `sh:class` are candidates — the least specific ones (a
28
+ * shape that extends another candidate is left out, so a relation to `Person` creates a
29
+ * `Person`, never an `Employee extends Person`), sorted by id — and the first is used. More
30
+ * than one root is a real ambiguity the data does not settle, so it warns — once per class
31
+ * and candidate set — naming every candidate and the one chosen. No candidate (or no
32
+ * `sh:class` either) resolves to `none`: the value can still be shown as a reference,
33
+ * but there is no shape to open or create it through.
34
+ */
35
+ export declare function resolveRelationShape(p: RelationPropertyLike, shapes?: ShapeCandidates): RelationShapeResolution;
@@ -0,0 +1,88 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+ /**
7
+ * Reading what a relation property points at.
8
+ *
9
+ * SHACL says it two ways, and they mean different things:
10
+ *
11
+ * - `sh:node S` (`valueShape`) names the SHAPE a value conforms to — and so the shape to
12
+ * view, edit or create it through.
13
+ * - `sh:class C` (`class`) names the CLASS a value is an instance of. It says what the value
14
+ * *is*, not which shape describes it; several shapes may target the same class, or none.
15
+ *
16
+ * These helpers keep the two apart instead of folding one into the other. A `class`-only
17
+ * relation is resolved to a shape here, at read time, and the answer is never written back
18
+ * into the shape: a derived `sh:node` would then be indistinguishable from an authored one.
19
+ *
20
+ * Every helper works on a property in data or wire form, and takes an optional set of
21
+ * shapes to resolve against — a project's catalog — in place of the registry, which also
22
+ * holds compiled framework shapes for the same classes.
23
+ */
24
+ import { shacl } from '../ontologies/shacl.js';
25
+ import { getShapesForTargetClass, getTargetClassId } from '../utils/ShapeClass.js';
26
+ const NODE_KINDS = new Set([shacl.IRI.id, shacl.BlankNode.id, shacl.BlankNodeOrIRI.id]);
27
+ /** True when the property's values are nodes (IRIs or blank nodes) rather than literals. */
28
+ export function isRelation(p) {
29
+ var _a;
30
+ if (p.valueShape || p.class)
31
+ return true;
32
+ return !!((_a = p.nodeKind) === null || _a === void 0 ? void 0 : _a.id) && NODE_KINDS.has(p.nodeKind.id);
33
+ }
34
+ /**
35
+ * The class a relation's values are instances of: the declared `sh:class`, else the
36
+ * targetClass of its `sh:node` shape. `undefined` when neither says.
37
+ */
38
+ export function rangeClassOf(p, shapes) {
39
+ var _a, _b;
40
+ if ((_a = p.class) === null || _a === void 0 ? void 0 : _a.id)
41
+ return p.class.id;
42
+ if (!((_b = p.valueShape) === null || _b === void 0 ? void 0 : _b.id))
43
+ return undefined;
44
+ return getTargetClassId(p.valueShape, shapes);
45
+ }
46
+ /**
47
+ * Ambiguities already warned about, keyed by class and candidate set, so a list of many rows
48
+ * warns once and not once per row — while a different set of shapes competing for the same
49
+ * class (another project's catalog, or the registry next to a catalog) still gets its own.
50
+ */
51
+ const warnedAmbiguities = new Set();
52
+ /**
53
+ * The shape a relation's values are viewed, picked and created through.
54
+ *
55
+ * A declared `sh:node` wins outright: the relation uses exactly that shape. Otherwise the
56
+ * shapes targeting the property's `sh:class` are candidates — the least specific ones (a
57
+ * shape that extends another candidate is left out, so a relation to `Person` creates a
58
+ * `Person`, never an `Employee extends Person`), sorted by id — and the first is used. More
59
+ * than one root is a real ambiguity the data does not settle, so it warns — once per class
60
+ * and candidate set — naming every candidate and the one chosen. No candidate (or no
61
+ * `sh:class` either) resolves to `none`: the value can still be shown as a reference,
62
+ * but there is no shape to open or create it through.
63
+ */
64
+ export function resolveRelationShape(p, shapes) {
65
+ var _a, _b;
66
+ if ((_a = p.valueShape) === null || _a === void 0 ? void 0 : _a.id) {
67
+ return { shapeId: p.valueShape.id, candidates: [p.valueShape.id], source: 'node' };
68
+ }
69
+ const classIri = (_b = p.class) === null || _b === void 0 ? void 0 : _b.id;
70
+ if (classIri) {
71
+ const matches = shapes
72
+ ? getShapesForTargetClass(classIri, shapes)
73
+ : getShapesForTargetClass(classIri);
74
+ const candidates = matches.map((shape) => shape.id);
75
+ if (candidates.length) {
76
+ const warnKey = `${classIri}|${candidates.join(' ')}`;
77
+ if (candidates.length > 1 && !warnedAmbiguities.has(warnKey)) {
78
+ warnedAmbiguities.add(warnKey);
79
+ console.warn(`[linked] ${candidates.length} shapes target class '${classIri}': ` +
80
+ `${candidates.join(', ')}. Using '${candidates[0]}' for relations declared ` +
81
+ `with sh:class only. Declare sh:node on the property to choose explicitly.`);
82
+ }
83
+ return { shapeId: candidates[0], candidates, source: 'class' };
84
+ }
85
+ }
86
+ return { candidates: [], source: 'none' };
87
+ }
88
+ //# sourceMappingURL=relationShape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationShape.js","sourceRoot":"","sources":["../../../src/shapes/relationShape.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n */\n\n/**\n * Reading what a relation property points at.\n *\n * SHACL says it two ways, and they mean different things:\n *\n * - `sh:node S` (`valueShape`) names the SHAPE a value conforms to — and so the shape to\n * view, edit or create it through.\n * - `sh:class C` (`class`) names the CLASS a value is an instance of. It says what the value\n * *is*, not which shape describes it; several shapes may target the same class, or none.\n *\n * These helpers keep the two apart instead of folding one into the other. A `class`-only\n * relation is resolved to a shape here, at read time, and the answer is never written back\n * into the shape: a derived `sh:node` would then be indistinguishable from an authored one.\n *\n * Every helper works on a property in data or wire form, and takes an optional set of\n * shapes to resolve against — a project's catalog — in place of the registry, which also\n * holds compiled framework shapes for the same classes.\n */\n\nimport {shacl} from '../ontologies/shacl.js';\nimport {getShapesForTargetClass, getTargetClassId} from '../utils/ShapeClass.js';\nimport type {NodeShapeData, PropertyShapeData} from './nodeShapeData.js';\nimport type {NodeShapeWire} from './nodeShapeWire.js';\n\n/** The fields of a property shape these helpers read; data and wire form both satisfy it. */\nexport type RelationPropertyLike = Pick<PropertyShapeData, 'valueShape' | 'class' | 'nodeKind'>;\n\n/** Shapes to resolve against in place of the registry. Iterated more than once — pass an array. */\nexport type ShapeCandidates = Iterable<NodeShapeData | NodeShapeWire>;\n\n/** How a relation property resolved to the shape its values are viewed through. */\nexport interface RelationShapeResolution {\n /** The shape to use, when there is one. */\n shapeId?: string;\n /** Every shape that qualified, in preference order; `shapeId` is the first. */\n candidates: string[];\n /** `node`: declared by `sh:node`. `class`: matched on `sh:class`. `none`: no shape found. */\n source: 'node' | 'class' | 'none';\n}\n\nconst NODE_KINDS = new Set<string>([shacl.IRI.id, shacl.BlankNode.id, shacl.BlankNodeOrIRI.id]);\n\n/** True when the property's values are nodes (IRIs or blank nodes) rather than literals. */\nexport function isRelation(p: RelationPropertyLike): boolean {\n if (p.valueShape || p.class) return true;\n return !!p.nodeKind?.id && NODE_KINDS.has(p.nodeKind.id);\n}\n\n/**\n * The class a relation's values are instances of: the declared `sh:class`, else the\n * targetClass of its `sh:node` shape. `undefined` when neither says.\n */\nexport function rangeClassOf(\n p: RelationPropertyLike,\n shapes?: ShapeCandidates,\n): string | undefined {\n if (p.class?.id) return p.class.id;\n if (!p.valueShape?.id) return undefined;\n return getTargetClassId(p.valueShape, shapes);\n}\n\n/**\n * Ambiguities already warned about, keyed by class and candidate set, so a list of many rows\n * warns once and not once per row — while a different set of shapes competing for the same\n * class (another project's catalog, or the registry next to a catalog) still gets its own.\n */\nconst warnedAmbiguities = new Set<string>();\n\n/**\n * The shape a relation's values are viewed, picked and created through.\n *\n * A declared `sh:node` wins outright: the relation uses exactly that shape. Otherwise the\n * shapes targeting the property's `sh:class` are candidates — the least specific ones (a\n * shape that extends another candidate is left out, so a relation to `Person` creates a\n * `Person`, never an `Employee extends Person`), sorted by id — and the first is used. More\n * than one root is a real ambiguity the data does not settle, so it warns — once per class\n * and candidate set — naming every candidate and the one chosen. No candidate (or no\n * `sh:class` either) resolves to `none`: the value can still be shown as a reference,\n * but there is no shape to open or create it through.\n */\nexport function resolveRelationShape(\n p: RelationPropertyLike,\n shapes?: ShapeCandidates,\n): RelationShapeResolution {\n if (p.valueShape?.id) {\n return {shapeId: p.valueShape.id, candidates: [p.valueShape.id], source: 'node'};\n }\n const classIri = p.class?.id;\n if (classIri) {\n const matches = shapes\n ? getShapesForTargetClass(classIri, shapes)\n : getShapesForTargetClass(classIri);\n const candidates = matches.map((shape) => shape.id);\n if (candidates.length) {\n const warnKey = `${classIri}|${candidates.join(' ')}`;\n if (candidates.length > 1 && !warnedAmbiguities.has(warnKey)) {\n warnedAmbiguities.add(warnKey);\n console.warn(\n `[linked] ${candidates.length} shapes target class '${classIri}': ` +\n `${candidates.join(', ')}. Using '${candidates[0]}' for relations declared ` +\n `with sh:class only. Declare sh:node on the property to choose explicitly.`,\n );\n }\n return {shapeId: candidates[0], candidates, source: 'class'};\n }\n }\n return {candidates: [], source: 'none'};\n}\n"]}
@@ -169,16 +169,20 @@ const minCountCheck = (values, ctx) => {
169
169
  function expectsLiteral(ps) {
170
170
  if (ps.nodeKind)
171
171
  return ps.nodeKind.id === shacl.Literal.id;
172
- return !!ps.datatype && !ps.valueShape;
172
+ return !!ps.datatype && !ps.valueShape && !ps.class;
173
173
  }
174
- /** True when the property clearly accepts only nodes (IRIs/blank nodes). */
174
+ /**
175
+ * True when the property clearly accepts only nodes (IRIs/blank nodes). `sh:class` counts
176
+ * as much as `sh:node`: a value that is an instance of a class is a node, whether or not
177
+ * the property names a shape for it.
178
+ */
175
179
  function expectsNode(ps) {
176
180
  if (ps.nodeKind) {
177
181
  return (ps.nodeKind.id === shacl.IRI.id ||
178
182
  ps.nodeKind.id === shacl.BlankNode.id ||
179
183
  ps.nodeKind.id === shacl.BlankNodeOrIRI.id);
180
184
  }
181
- return !!ps.valueShape;
185
+ return !!(ps.valueShape || ps.class);
182
186
  }
183
187
  function isScalarValue(value) {
184
188
  return (typeof value === 'string' ||
@@ -189,7 +193,7 @@ function isScalarValue(value) {
189
193
  /**
190
194
  * `sh:nodeKind` — literal properties reject nodes/objects, relation properties
191
195
  * reject bare scalars. Ambiguous kinds (`sh:IRIOrLiteral`, or no `nodeKind` and
192
- * no `datatype`/`valueShape` to infer from) are not enforced.
196
+ * no `datatype`/`valueShape`/`class` to infer from) are not enforced.
193
197
  */
194
198
  const nodeKindCheck = (values, ctx) => {
195
199
  const ps = ctx.propertyShape;