@canonical/pragma-cli 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cursor a caller spends to ask for the page after this one.
|
|
3
|
+
*
|
|
4
|
+
* WHY AN OFFSET. The candidate alternative is a keyset cursor encoding the last
|
|
5
|
+
* row's ordering key, which does not drift when the store changes underneath a
|
|
6
|
+
* caller. It cannot be general, and `block list` is the counterexample: it
|
|
7
|
+
* orders by `?tierRank`, a variable its SELECT does not project, so the
|
|
8
|
+
* ordering key is not in the rows at all — and a pagination mechanism that
|
|
9
|
+
* works only for stories whose ordering keys happen to be columns is not a
|
|
10
|
+
* pagination mechanism. An offset needs nothing from the rows.
|
|
11
|
+
*
|
|
12
|
+
* The drift an offset admits is bounded by what the store is: a read-only,
|
|
13
|
+
* content-addressed pack that changes only when `sources update` rebuilds it.
|
|
14
|
+
* Between two pages of one browse there is no writer.
|
|
15
|
+
*
|
|
16
|
+
* What an offset cannot survive is the caller changing the QUESTION between
|
|
17
|
+
* pages — different filters, a different search term, another story — which is
|
|
18
|
+
* why the cursor carries a fingerprint of the read it was issued for and
|
|
19
|
+
* refuses to be spent on another. That is the failure worth catching, and the
|
|
20
|
+
* one a bare integer would have answered wrongly and in silence.
|
|
21
|
+
*
|
|
22
|
+
* The encoding is opaque on purpose: a caller who cannot read it cannot come to
|
|
23
|
+
* depend on it, so the day a story needs a keyset cursor the cursor's contents
|
|
24
|
+
* change and its contract does not.
|
|
25
|
+
*
|
|
26
|
+
* The fingerprint does not cover the OFFSET, and cannot: a cursor is a token
|
|
27
|
+
* this build issued, not a signed one, so its `o` is editable by anyone who
|
|
28
|
+
* base64-decodes it. That is why the offset is BOUNDED here as well as parsed
|
|
29
|
+
* here — an `o` of 2^53 is a legal JSON integer that the store answers with a
|
|
30
|
+
* raw parse error, so {@link ./paging.MAX_LIST_WINDOW} is checked before the
|
|
31
|
+
* number can reach a query.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { PragmaError } from "../error/index.js";
|
|
36
|
+
import { MAX_LIST_WINDOW } from "./paging.js";
|
|
37
|
+
|
|
38
|
+
/** The cursor encoding's own version, so a future keyset form is tellable apart. */
|
|
39
|
+
const CURSOR_VERSION = 1;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The identity of the read a cursor belongs to — the author query and the
|
|
43
|
+
* arguments that narrow it, hashed short.
|
|
44
|
+
*
|
|
45
|
+
* Each part is LENGTH-PREFIXED rather than merely joined. A separator is only
|
|
46
|
+
* unambiguous if it cannot occur inside a part, and these parts are SPARQL
|
|
47
|
+
* text and JSON — there is no such character. Two different argument lists
|
|
48
|
+
* hashing the same would let a cursor be spent on a read it was not issued
|
|
49
|
+
* for, which is the single thing this function exists to prevent.
|
|
50
|
+
*
|
|
51
|
+
* @param parts - Everything that decides which rows the read returns.
|
|
52
|
+
* @returns A short, stable fingerprint.
|
|
53
|
+
*/
|
|
54
|
+
export function pageFingerprint(parts: readonly string[]): string {
|
|
55
|
+
const hash = createHash("sha256");
|
|
56
|
+
for (const part of parts) hash.update(`${part.length}:${part}`);
|
|
57
|
+
return hash.digest("base64url").slice(0, 12);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Encode the cursor that asks for the rows after this page.
|
|
62
|
+
*
|
|
63
|
+
* @param offset - The row index the next page starts at.
|
|
64
|
+
* @param fingerprint - The read this cursor may be spent on.
|
|
65
|
+
* @returns An opaque, URL-safe token.
|
|
66
|
+
*/
|
|
67
|
+
export function encodeCursor(offset: number, fingerprint: string): string {
|
|
68
|
+
const payload = JSON.stringify({
|
|
69
|
+
v: CURSOR_VERSION,
|
|
70
|
+
o: offset,
|
|
71
|
+
q: fingerprint,
|
|
72
|
+
});
|
|
73
|
+
return Buffer.from(payload, "utf8").toString("base64url");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read an `--after`/`after` cursor back into an offset.
|
|
78
|
+
*
|
|
79
|
+
* @param provided - The raw argument, if any.
|
|
80
|
+
* @param fingerprint - The read the cursor must have been issued for.
|
|
81
|
+
* @returns The row index to start at (0 when no cursor was given).
|
|
82
|
+
* @throws PragmaError INVALID_INPUT when the token is not a cursor this build
|
|
83
|
+
* issued, or was issued for a different read — a mismatch answered with rows
|
|
84
|
+
* would be a page of the wrong question, with nothing to show it.
|
|
85
|
+
*/
|
|
86
|
+
export function readCursor(provided: unknown, fingerprint: string): number {
|
|
87
|
+
if (provided === undefined) return 0;
|
|
88
|
+
const decoded = decodeCursor(provided);
|
|
89
|
+
if (!decoded) {
|
|
90
|
+
throw PragmaError.invalidInput("after", String(provided), {
|
|
91
|
+
recovery: {
|
|
92
|
+
message:
|
|
93
|
+
"--after takes the cursor from a previous page of this same read; run the read without it to start from the first page.",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (decoded.q !== fingerprint) {
|
|
98
|
+
throw PragmaError.invalidInput("after", String(provided), {
|
|
99
|
+
recovery: {
|
|
100
|
+
message:
|
|
101
|
+
"This cursor was issued for a different read — the filters or the search term changed. Run the new read without --after to start from its first page.",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return decoded.o;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The cursor payload, or `undefined` for anything that is not one.
|
|
110
|
+
*
|
|
111
|
+
* The offset is checked against {@link MAX_LIST_WINDOW} here rather than at the
|
|
112
|
+
* store: an unbounded `o` is emitted straight into the generated query's
|
|
113
|
+
* `OFFSET`, where a number the engine cannot express comes back as a parse
|
|
114
|
+
* error dressed as an internal one. Refused here, a hand-edited offset is the
|
|
115
|
+
* same calm INVALID_INPUT as every other malformed cursor.
|
|
116
|
+
*/
|
|
117
|
+
function decodeCursor(
|
|
118
|
+
provided: unknown,
|
|
119
|
+
): { readonly o: number; readonly q: string } | undefined {
|
|
120
|
+
if (typeof provided !== "string" || provided === "") return undefined;
|
|
121
|
+
let parsed: unknown;
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(Buffer.from(provided, "base64url").toString("utf8"));
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
128
|
+
const payload = parsed as Record<string, unknown>;
|
|
129
|
+
if (payload.v !== CURSOR_VERSION) return undefined;
|
|
130
|
+
if (!Number.isInteger(payload.o)) return undefined;
|
|
131
|
+
const offset = payload.o as number;
|
|
132
|
+
if (offset < 0 || offset > MAX_LIST_WINDOW) return undefined;
|
|
133
|
+
if (typeof payload.q !== "string") return undefined;
|
|
134
|
+
return { o: offset, q: payload.q };
|
|
135
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A zero-row answer names the FILTER and still explains the POPULATION.
|
|
3
|
+
*
|
|
4
|
+
* Two facts can empty a list and the rows cannot tell them apart: nothing was
|
|
5
|
+
* there, or a filter missed what was. So the page says both — the kernel's
|
|
6
|
+
* sentence names the filters in force (`token list --search zzzznotreal` used
|
|
7
|
+
* to answer "No token symbols in the store … run `pragma sources update`",
|
|
8
|
+
* telling a reader with 745 present symbols to rebuild for nothing), and the
|
|
9
|
+
* story's own `emptyRecovery` follows it, exactly as it does when nothing
|
|
10
|
+
* narrowed the read.
|
|
11
|
+
*
|
|
12
|
+
* Which puts one requirement on the stories rather than on the kernel: a
|
|
13
|
+
* recovery is worded to hold in BOTH cases, or the story declares none. The
|
|
14
|
+
* fixture below is worded that way on purpose.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
18
|
+
import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
|
|
19
|
+
import { executeVerb } from "../project/cli/dispatch.js";
|
|
20
|
+
import type { GlobalFlags, PragmaRuntime } from "../runtime/types.js";
|
|
21
|
+
import type { VerbSpec } from "../spec/types.js";
|
|
22
|
+
import { compilePack } from "./compile.js";
|
|
23
|
+
import type { PackDefinition } from "./types.js";
|
|
24
|
+
import { distributionSource } from "./types.js";
|
|
25
|
+
import { verbKey } from "./uniqueness.js";
|
|
26
|
+
|
|
27
|
+
const PREFIXES = {
|
|
28
|
+
ex: "https://example.org/widgets#",
|
|
29
|
+
owl: "http://www.w3.org/2002/07/owl#",
|
|
30
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
31
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const TTL = `
|
|
35
|
+
@prefix ex: <https://example.org/widgets#> .
|
|
36
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
37
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
38
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
39
|
+
|
|
40
|
+
ex:Widget a owl:Class .
|
|
41
|
+
ex:Gadget a owl:Class .
|
|
42
|
+
ex:name a owl:DatatypeProperty ; rdfs:domain ex:Widget ; rdfs:range xsd:string .
|
|
43
|
+
ex:kind a owl:DatatypeProperty ; rdfs:domain ex:Widget ; rdfs:range xsd:string .
|
|
44
|
+
|
|
45
|
+
ex:button a ex:Widget ; ex:name "Button" ; ex:kind "input" .
|
|
46
|
+
ex:label a ex:Widget ; ex:name "Label" ; ex:kind "display" .
|
|
47
|
+
ex:slider a ex:Widget ; ex:name "Slider" ; ex:kind "input" .
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The recovery a story authors — where the rows come from, said so it holds
|
|
52
|
+
* over a populated table a filter missed as well as over an empty one.
|
|
53
|
+
*
|
|
54
|
+
* "No widgets in the store." would not: it is a claim about the population, and
|
|
55
|
+
* the kernel prints this under a filtered miss too, where three widgets are
|
|
56
|
+
* sitting in the store.
|
|
57
|
+
*/
|
|
58
|
+
const EMPTY_RECOVERY = {
|
|
59
|
+
message:
|
|
60
|
+
"Widgets are recorded by the catalogue build, and a store built before it ran records none.",
|
|
61
|
+
cli: "sources update",
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
const listShape = (uriClass: string) => ({
|
|
65
|
+
query: [
|
|
66
|
+
"SELECT ?uri ?name ?kind WHERE {",
|
|
67
|
+
` ?uri a ${uriClass} ; ex:name ?name .`,
|
|
68
|
+
" OPTIONAL { ?uri ex:kind ?kind }",
|
|
69
|
+
"} ORDER BY ?name",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
columns: [
|
|
72
|
+
{ field: "uri", label: "IRI" },
|
|
73
|
+
{ field: "name", label: "Name" },
|
|
74
|
+
{ field: "kind", label: "Kind" },
|
|
75
|
+
],
|
|
76
|
+
filters: [{ param: "kind", variable: "kind", values: ["input", "display"] }],
|
|
77
|
+
search: { variables: ["name"] },
|
|
78
|
+
emptyRecovery: EMPTY_RECOVERY,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** Three rows — a POPULATED table, so a filter that misses really is the news. */
|
|
82
|
+
const WIDGET_PACK: PackDefinition = {
|
|
83
|
+
noun: "widget",
|
|
84
|
+
description: "List widgets.",
|
|
85
|
+
list: listShape("ex:Widget"),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** A declared class with no individuals — the genuinely empty population. */
|
|
89
|
+
const GADGET_PACK: PackDefinition = {
|
|
90
|
+
noun: "gadget",
|
|
91
|
+
description: "List gadgets.",
|
|
92
|
+
list: { ...listShape("ex:Gadget"), emptyRecovery: EMPTY_RECOVERY },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const listVerb = (pack: PackDefinition): VerbSpec =>
|
|
96
|
+
compilePack(pack, distributionSource("test:list"), PREFIXES).find(
|
|
97
|
+
(v) => verbKey(v.path) === `${pack.noun} list`,
|
|
98
|
+
) as VerbSpec;
|
|
99
|
+
|
|
100
|
+
const REAL = { dryRun: false, undo: false, yes: false };
|
|
101
|
+
|
|
102
|
+
const PLAIN: GlobalFlags = {
|
|
103
|
+
llm: false,
|
|
104
|
+
autoLlm: false,
|
|
105
|
+
format: "plain",
|
|
106
|
+
verbose: false,
|
|
107
|
+
};
|
|
108
|
+
const LLM: GlobalFlags = { ...PLAIN, llm: true, format: "llm" };
|
|
109
|
+
const JSON_FLAGS: GlobalFlags = { ...PLAIN, format: "json" };
|
|
110
|
+
|
|
111
|
+
let rt: PragmaRuntime;
|
|
112
|
+
/** The same runtime, reading in another format. */
|
|
113
|
+
const as = (flags: GlobalFlags): PragmaRuntime => ({
|
|
114
|
+
...rt,
|
|
115
|
+
globalFlags: flags,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
beforeAll(async () => {
|
|
119
|
+
({ rt } = await buildFixtureRuntime({ ttl: TTL, prefixes: PREFIXES }));
|
|
120
|
+
});
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
(await rt.store.get()).store.dispose();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("a zero-row answer names the filter and keeps the recovery", () => {
|
|
126
|
+
it("a search that matched nothing names the search, and still explains", async () => {
|
|
127
|
+
const outcome = await executeVerb(
|
|
128
|
+
listVerb(WIDGET_PACK),
|
|
129
|
+
{ search: "zzzznotreal" },
|
|
130
|
+
REAL,
|
|
131
|
+
as(PLAIN),
|
|
132
|
+
);
|
|
133
|
+
expect(outcome.exitCode).toBe(0);
|
|
134
|
+
const said = `${outcome.stdout ?? ""}${outcome.stderr ?? ""}`;
|
|
135
|
+
expect(said).toContain("No widget matches `--search zzzznotreal`.");
|
|
136
|
+
// The filter is the news; the story's account of where widgets come from is
|
|
137
|
+
// the other half, and a reader who mistyped nothing needs it.
|
|
138
|
+
expect(said).toContain("recorded by the catalogue build");
|
|
139
|
+
expect(said).toContain("pragma sources update");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("every filter in force is named", async () => {
|
|
143
|
+
const outcome = await executeVerb(
|
|
144
|
+
listVerb(WIDGET_PACK),
|
|
145
|
+
{ kind: "display", search: "zzzznotreal" },
|
|
146
|
+
REAL,
|
|
147
|
+
as(PLAIN),
|
|
148
|
+
);
|
|
149
|
+
const said = `${outcome.stdout ?? ""}${outcome.stderr ?? ""}`;
|
|
150
|
+
expect(said).toContain(
|
|
151
|
+
"No widget matches `--kind display` and `--search zzzznotreal`.",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("the llm body says the same thing", async () => {
|
|
156
|
+
const outcome = await executeVerb(
|
|
157
|
+
listVerb(WIDGET_PACK),
|
|
158
|
+
{ search: "zzzznotreal" },
|
|
159
|
+
REAL,
|
|
160
|
+
as(LLM),
|
|
161
|
+
);
|
|
162
|
+
expect(outcome.stdout).toContain(
|
|
163
|
+
"No widget matches `--search zzzznotreal`.",
|
|
164
|
+
);
|
|
165
|
+
expect(outcome.stdout).toContain("recorded by the catalogue build");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("json says the same thing on meta.notice", async () => {
|
|
169
|
+
const outcome = await executeVerb(
|
|
170
|
+
listVerb(WIDGET_PACK),
|
|
171
|
+
{ search: "zzzznotreal" },
|
|
172
|
+
REAL,
|
|
173
|
+
as(JSON_FLAGS),
|
|
174
|
+
);
|
|
175
|
+
const envelope = JSON.parse(outcome.stdout as string);
|
|
176
|
+
expect(envelope.data).toEqual([]);
|
|
177
|
+
expect(envelope.meta.notice).toContain(
|
|
178
|
+
"No widget matches `--search zzzznotreal`.",
|
|
179
|
+
);
|
|
180
|
+
expect(envelope.meta.notice).toContain("recorded by the catalogue build");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("an EMPTY POPULATION under a filter keeps the story's own recovery", async () => {
|
|
184
|
+
// The regression: `token consumers --symbol color.text` narrows a table
|
|
185
|
+
// that records nothing at all, and answering only "no match for that
|
|
186
|
+
// symbol" read as a typo while withholding the one account the verb has.
|
|
187
|
+
// No probe tells the two apart — the sentence does, by saying both.
|
|
188
|
+
const outcome = await executeVerb(
|
|
189
|
+
listVerb(GADGET_PACK),
|
|
190
|
+
{ kind: "input" },
|
|
191
|
+
REAL,
|
|
192
|
+
as(PLAIN),
|
|
193
|
+
);
|
|
194
|
+
expect(outcome.exitCode).toBe(0);
|
|
195
|
+
const said = `${outcome.stderr ?? ""}`;
|
|
196
|
+
expect(said).toContain("No gadget matches `--kind input`.");
|
|
197
|
+
expect(said).toContain("recorded by the catalogue build");
|
|
198
|
+
expect(said).toContain("pragma sources update");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("an EMPTY POPULATION unfiltered gets the kernel's own sentence", async () => {
|
|
202
|
+
const outcome = await executeVerb(
|
|
203
|
+
listVerb(GADGET_PACK),
|
|
204
|
+
{},
|
|
205
|
+
REAL,
|
|
206
|
+
as(PLAIN),
|
|
207
|
+
);
|
|
208
|
+
expect(outcome.exitCode).toBe(0);
|
|
209
|
+
expect(outcome.stderr).toContain("No gadget entries found.");
|
|
210
|
+
expect(outcome.stderr).toContain("recorded by the catalogue build");
|
|
211
|
+
expect(outcome.stderr).toContain("pragma sources update");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -31,7 +31,7 @@ import type {
|
|
|
31
31
|
PackLookup,
|
|
32
32
|
PackNestedExpand,
|
|
33
33
|
} from "../types.js";
|
|
34
|
-
import { isNestedExpand } from "../types.js";
|
|
34
|
+
import { expandIsSparql, isNestedExpand } from "../types.js";
|
|
35
35
|
import { MAX_PAGE_SIZE, pluralize, stripVerbPrefix } from "./nameMap.js";
|
|
36
36
|
|
|
37
37
|
type GraphQLSchema = import("graphql").GraphQLSchema;
|
|
@@ -367,16 +367,23 @@ export function buildLookupDocument(
|
|
|
367
367
|
),
|
|
368
368
|
),
|
|
369
369
|
),
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
370
|
+
// An expand that declares the SPARQL lane is not this document's — the
|
|
371
|
+
// entity fetcher runs its sub-SELECT beside this query and merges the rows
|
|
372
|
+
// in. Skipped rather than attempted: its terms are chosen for a lane where
|
|
373
|
+
// property paths read and every defined property is reachable, and asking
|
|
374
|
+
// for them here would either drop them in silence or raise.
|
|
375
|
+
...activeExpands(lookup, level)
|
|
376
|
+
.filter((expand) => !expandIsSparql(lookup, expand))
|
|
377
|
+
.map((expand, index) =>
|
|
378
|
+
buildScopedSelection(schema, fragmentType, (container) =>
|
|
379
|
+
buildCollectionSelection(
|
|
380
|
+
container,
|
|
381
|
+
expand,
|
|
382
|
+
`lookup.expand[${index}]`,
|
|
383
|
+
source,
|
|
384
|
+
),
|
|
377
385
|
),
|
|
378
386
|
),
|
|
379
|
-
),
|
|
380
387
|
].filter((selection): selection is Selection => selection !== undefined);
|
|
381
388
|
|
|
382
389
|
const body = selections.map((selection) => ` ${selection.text}`);
|
|
@@ -130,6 +130,158 @@ describe("source rule — zod refinements reject dual/ill-sourced lookups (PROTE
|
|
|
130
130
|
).toThrow(/single-hop|graphql/i);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
it("accepts a SPARQL-lane expand inside a graphql lookup", () => {
|
|
134
|
+
// The two lanes are not interchangeable and the block lookup needs both:
|
|
135
|
+
// a nested expand only a document can make, beside a table whose columns
|
|
136
|
+
// the compiled schema has no field for. So the lane is per-expand, and the
|
|
137
|
+
// rules that follow are about the lane that will actually resolve it.
|
|
138
|
+
const definition = {
|
|
139
|
+
noun: "x",
|
|
140
|
+
lookup: {
|
|
141
|
+
source: "graphql",
|
|
142
|
+
by: "ds:name",
|
|
143
|
+
type: "ds:Component",
|
|
144
|
+
graphqlType: "Component",
|
|
145
|
+
expand: [
|
|
146
|
+
{
|
|
147
|
+
name: "t",
|
|
148
|
+
relation: "ds:hasTokenBinding",
|
|
149
|
+
source: "sparql",
|
|
150
|
+
orderBy: ["node"],
|
|
151
|
+
note: "How to read the rows.",
|
|
152
|
+
select: [
|
|
153
|
+
{ name: "symbol", property: "ds:consumesSymbol/rdfs:label" },
|
|
154
|
+
{
|
|
155
|
+
name: "via",
|
|
156
|
+
property: "ds:viaBlock/ds:name",
|
|
157
|
+
blankWhenSelf: true,
|
|
158
|
+
},
|
|
159
|
+
{ name: "node", property: "ds:node" },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
expect(
|
|
166
|
+
parsePackDefinition(JSON.parse(JSON.stringify(definition)), "t"),
|
|
167
|
+
).toEqual(definition);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("rejects a nested expand on a SPARQL-lane expand of a graphql lookup", () => {
|
|
171
|
+
// The single-hop rule follows the EXPAND's lane, not the lookup's — it
|
|
172
|
+
// used to read the lookup's, so an expand that opted out of the document
|
|
173
|
+
// could still declare a second hop no sub-SELECT can make.
|
|
174
|
+
expect(() =>
|
|
175
|
+
parsePackDefinition(
|
|
176
|
+
{
|
|
177
|
+
noun: "x",
|
|
178
|
+
lookup: {
|
|
179
|
+
source: "graphql",
|
|
180
|
+
by: "ds:name",
|
|
181
|
+
type: "ds:Component",
|
|
182
|
+
graphqlType: "Component",
|
|
183
|
+
expand: [
|
|
184
|
+
{
|
|
185
|
+
name: "f",
|
|
186
|
+
relation: "ds:hasModifierFamily",
|
|
187
|
+
source: "sparql",
|
|
188
|
+
select: [
|
|
189
|
+
{
|
|
190
|
+
name: "v",
|
|
191
|
+
relation: "ds:hasModifier",
|
|
192
|
+
select: [{ name: "n", property: "ds:name" }],
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
"t",
|
|
200
|
+
),
|
|
201
|
+
).toThrow(/single-hop|graphql/i);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("rejects orderBy and blankWhenSelf on a lane that cannot express them", () => {
|
|
205
|
+
// A GraphQL collection's order is the schema's and a document cannot
|
|
206
|
+
// filter a cell against the entity's own name, so both are refused where
|
|
207
|
+
// they are written rather than ignored at runtime — a silent no-op is how
|
|
208
|
+
// a declaration that never applied survives review.
|
|
209
|
+
const base = {
|
|
210
|
+
noun: "x",
|
|
211
|
+
lookup: {
|
|
212
|
+
source: "graphql",
|
|
213
|
+
by: "ds:name",
|
|
214
|
+
type: "ds:Component",
|
|
215
|
+
graphqlType: "Component",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
expect(() =>
|
|
219
|
+
parsePackDefinition(
|
|
220
|
+
{
|
|
221
|
+
...base,
|
|
222
|
+
lookup: {
|
|
223
|
+
...base.lookup,
|
|
224
|
+
expand: [
|
|
225
|
+
{
|
|
226
|
+
name: "t",
|
|
227
|
+
relation: "ds:hasTokenBinding",
|
|
228
|
+
orderBy: ["node"],
|
|
229
|
+
select: [{ name: "node", property: "ds:node" }],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
"t",
|
|
235
|
+
),
|
|
236
|
+
).toThrow(/orderBy/);
|
|
237
|
+
expect(() =>
|
|
238
|
+
parsePackDefinition(
|
|
239
|
+
{
|
|
240
|
+
...base,
|
|
241
|
+
lookup: {
|
|
242
|
+
...base.lookup,
|
|
243
|
+
expand: [
|
|
244
|
+
{
|
|
245
|
+
name: "t",
|
|
246
|
+
relation: "ds:hasTokenBinding",
|
|
247
|
+
select: [
|
|
248
|
+
{ name: "via", property: "ds:viaBlock", blankWhenSelf: true },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
"t",
|
|
255
|
+
),
|
|
256
|
+
).toThrow(/blankWhenSelf/);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("rejects an orderBy naming a field the select does not declare", () => {
|
|
260
|
+
// It would order by an unbound variable, which SPARQL permits and which
|
|
261
|
+
// orders by nothing at all — the rows would come back in the store's order
|
|
262
|
+
// with a declaration claiming otherwise.
|
|
263
|
+
expect(() =>
|
|
264
|
+
parsePackDefinition(
|
|
265
|
+
{
|
|
266
|
+
noun: "x",
|
|
267
|
+
lookup: {
|
|
268
|
+
by: "ds:name",
|
|
269
|
+
type: "ds:Component",
|
|
270
|
+
expand: [
|
|
271
|
+
{
|
|
272
|
+
name: "t",
|
|
273
|
+
relation: "ds:hasTokenBinding",
|
|
274
|
+
orderBy: ["rank"],
|
|
275
|
+
select: [{ name: "node", property: "ds:node" }],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
"t",
|
|
281
|
+
),
|
|
282
|
+
).toThrow(/orders by "rank"/);
|
|
283
|
+
});
|
|
284
|
+
|
|
133
285
|
it("rejects a graphql field addressed by a property path", () => {
|
|
134
286
|
expect(() =>
|
|
135
287
|
parsePackDefinition(
|