@_linked/core 2.26.0 → 2.28.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 (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +33 -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/interfaces/IFileStore.d.ts +6 -0
  9. package/lib/esm/interfaces/IFileStore.js.map +1 -1
  10. package/lib/esm/live/LiveQueryStore.d.ts +240 -0
  11. package/lib/esm/live/LiveQueryStore.js +729 -0
  12. package/lib/esm/live/LiveQueryStore.js.map +1 -0
  13. package/lib/esm/live/changes.d.ts +30 -0
  14. package/lib/esm/live/changes.js +30 -0
  15. package/lib/esm/live/changes.js.map +1 -0
  16. package/lib/esm/live/keys.d.ts +54 -0
  17. package/lib/esm/live/keys.js +112 -0
  18. package/lib/esm/live/keys.js.map +1 -0
  19. package/lib/esm/live/matcher.d.ts +43 -0
  20. package/lib/esm/live/matcher.js +115 -0
  21. package/lib/esm/live/matcher.js.map +1 -0
  22. package/lib/esm/live/registry.d.ts +24 -0
  23. package/lib/esm/live/registry.js +41 -0
  24. package/lib/esm/live/registry.js.map +1 -0
  25. package/lib/esm/live.d.ts +12 -0
  26. package/lib/esm/live.js +15 -0
  27. package/lib/esm/live.js.map +1 -0
  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/SelectQuery.d.ts +8 -8
  40. package/lib/esm/queries/SelectQuery.js.map +1 -1
  41. package/lib/esm/queries/queryDependencies.d.ts +58 -0
  42. package/lib/esm/queries/queryDependencies.js +494 -0
  43. package/lib/esm/queries/queryDependencies.js.map +1 -0
  44. package/lib/esm/queries/queryDispatch.d.ts +46 -0
  45. package/lib/esm/queries/queryDispatch.js +88 -3
  46. package/lib/esm/queries/queryDispatch.js.map +1 -1
  47. package/lib/esm/shapes/nodeShapeData.d.ts +16 -0
  48. package/lib/esm/shapes/nodeShapeData.js +41 -1
  49. package/lib/esm/shapes/nodeShapeData.js.map +1 -1
  50. package/lib/esm/sparql/irToAlgebra.js +1 -32
  51. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  52. package/lib/esm/utils/LinkedStorage.d.ts +6 -0
  53. package/lib/esm/utils/LinkedStorage.js +32 -6
  54. package/lib/esm/utils/LinkedStorage.js.map +1 -1
  55. package/package.json +6 -1
@@ -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"]}
@@ -1,4 +1,4 @@
1
- import { getPropertyShapes } from '../shapes/nodeShapeData.js';
1
+ import { findPropertyShapeById, getPropertyShapes } from '../shapes/nodeShapeData.js';
2
2
  import { pathExprToSparql, collectPathUris } from '../paths/pathExprToSparql.js';
3
3
  import { generateEntityUri } from './sparqlUtils.js';
4
4
  import { selectPlanToSparql, askPlanToSparql, insertDataPlanToSparql, deleteInsertPlanToSparql, } from './algebraToString.js';
@@ -85,37 +85,6 @@ function resolveShapeScanIri(shapeId) {
85
85
  // resolved before its shape registers can still resolve correctly afterwards.
86
86
  const predicateTermCache = new Map();
87
87
  let predicateTermCacheSize = -1;
88
- // The registry scan behind both predicate and datatype resolution, cached on the
89
- // same terms as the predicate cache above: successful lookups only, invalidated
90
- // by registry size.
91
- const propertyShapeCache = new Map();
92
- let propertyShapeCacheSize = -1;
93
- function findPropertyShapeById(propertyId) {
94
- // Scans the METAMODEL registry, which holds every shape — authored or data-only.
95
- // Scanning the class registry meant a data-only property was never found, and the
96
- // caller then fell through to emitting the PROPERTY SHAPE's IRI as the SPARQL
97
- // predicate: a silently wrong query that matched nothing, with no error.
98
- //
99
- // Cache keyed on the registration version rather than the registry SIZE. Size does not
100
- // change when a shape is re-registered in place, which is exactly what happens when a
101
- // shape is edited and its metadata replaced.
102
- const version = getRegistryVersion();
103
- if (version !== propertyShapeCacheSize) {
104
- propertyShapeCache.clear();
105
- propertyShapeCacheSize = version;
106
- }
107
- const cached = propertyShapeCache.get(propertyId);
108
- if (cached)
109
- return cached;
110
- for (const nodeShape of getAllNodeShapes().values()) {
111
- const propertyShape = getPropertyShapes(nodeShape, true).find((prop) => prop.id === propertyId);
112
- if (propertyShape) {
113
- propertyShapeCache.set(propertyId, propertyShape);
114
- return propertyShape;
115
- }
116
- }
117
- return undefined;
118
- }
119
88
  /**
120
89
  * The `sh:datatype` declared for a property, if any. Lets the serializer emit the
121
90
  * term the shape asks for rather than one inferred from the JavaScript value: a