Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fdf2d03
feat(search)!: unify the field model and add query IR, engine port an…
ddeboer Jun 28, 2026
11df992
feat(search-typesense): add collection-schema builder, query compiler…
ddeboer Jun 28, 2026
c64c909
feat(search-api-graphql): add the runtime-configured GraphQL surface
ddeboer Jun 28, 2026
a1dabb2
docs(search): reconcile ADRs 0003 and 0004 with the NDE stack docs
ddeboer Jun 29, 2026
a4e295a
feat(search): project number-kind fields
ddeboer Jun 29, 2026
79eaad3
docs(search): link ADR 3 to the published stack platform docs
ddeboer Jun 29, 2026
bda62cf
feat(search)!: keyed facet surface, range facets, label cache; remove…
ddeboer Jul 1, 2026
8e61bbd
build(deps): add @lde/search-api-graphql to the lockfile and refresh …
ddeboer Jul 1, 2026
32f3335
fix(search-typesense): narrow possibly-undefined facet buckets in the…
ddeboer Jul 1, 2026
ec7866f
docs(search): state ADR 3 design directly, without dated update annot…
ddeboer Jul 1, 2026
f486988
feat(search)!: rename the per-type SearchSchema to SearchType
ddeboer Jul 2, 2026
0fd49ed
docs(readme): add search packages to the packages table and diagram
ddeboer Jul 2, 2026
e146384
test(search-typesense): update autoUpdate line-coverage threshold
ddeboer Jul 2, 2026
0c31d67
feat(search): centralize shared helpers in the core, fix date-range f…
ddeboer Jul 3, 2026
b24b9dd
feat(search-api-graphql)!: build the GraphQL schema from the whole Se…
ddeboer Jul 3, 2026
c34608f
docs(search): state ADR 3 without historical references
ddeboer Jul 3, 2026
1fd53ae
docs(search): treat SHACL as an optional source of the field model, n…
ddeboer Jul 3, 2026
6ee6c00
feat(search): add engineFor to narrow an engine to one search type
ddeboer Jul 3, 2026
46958a2
docs(search): describe the query IR as a shared representation, not a…
ddeboer Jul 3, 2026
880fd3f
docs(search): point to the adapter and surface packages from the intro
ddeboer Jul 3, 2026
0c71656
docs(search): frame the search family as a generator for search engines
ddeboer Jul 3, 2026
6cdeefa
docs(search): rephrase the derived-artifacts sentence without dashes
ddeboer Jul 3, 2026
4cd78d4
docs(search): attribute each adapter tier to its port, drop list arti…
ddeboer Jul 3, 2026
92e45ee
docs(search): shorten the API-surfaces bullet to search(SearchQuery)
ddeboer Jul 3, 2026
9289f7c
docs(search): cut the sentence restating the diagram, name OpenSearch…
ddeboer Jul 3, 2026
55f2041
docs(search): name OpenSearch as the hypothetical second engine adapter
ddeboer Jul 3, 2026
cfb9db8
docs(search-api-graphql): document serving a subset of the schema
ddeboer Jul 3, 2026
5427afd
feat(search)!: declare the logical API name on the SearchType
ddeboer Jul 3, 2026
9e9bded
feat(search-typesense)!: deepen rebuild, explicit stemming locale, ig…
ddeboer Jul 3, 2026
03cc0a3
feat(search)!: always validate queries at the engine port
ddeboer Jul 3, 2026
7c0cda9
docs(search-api-graphql): sharpen the README
ddeboer Jul 3, 2026
02e573d
feat(search)!: order engineFor parameters value-first
ddeboer Jul 3, 2026
0d5b488
feat(search-typesense)!: unify the compiler options and rebuild's par…
ddeboer Jul 3, 2026
32d3e59
docs(search-api-graphql): state why runtime configuration benefits th…
ddeboer Jul 3, 2026
31e9c70
docs(search): document the family-wide API conventions
ddeboer Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ await pipeline.run();
<td><a href="https://www.npmjs.com/package/@lde/search"><img src="https://img.shields.io/npm/v/@lde/search" alt="npm"></a></td>
<td>Project RDF into engine-agnostic search documents (framing + a declarative field spec)</td>
</tr>
<tr>
<td><a href="packages/search-api-graphql">@lde/search-api-graphql</a></td>
<td><a href="https://www.npmjs.com/package/@lde/search-api-graphql"><img src="https://img.shields.io/npm/v/@lde/search-api-graphql" alt="npm"></a></td>
<td>Engine- and domain-agnostic GraphQL surface for search: builds an executable GraphQL schema from a SearchSchema at runtime, one root query field per type</td>
</tr>
<tr>
<td><a href="packages/search-typesense">@lde/search-typesense</a></td>
<td><a href="https://www.npmjs.com/package/@lde/search-typesense"><img src="https://img.shields.io/npm/v/@lde/search-typesense" alt="npm"></a></td>
Expand Down Expand Up @@ -229,6 +234,10 @@ graph TD
subgraph Publication
fastify-rdf
docgen
search --> text-normalization
search-api-graphql --> search
search-typesense --> search
search-typesense --> text-normalization
end

subgraph Monitoring
Expand Down
193 changes: 134 additions & 59 deletions docs/decisions/0003-search-api-core-query-model.md

Large diffs are not rendered by default.

297 changes: 154 additions & 143 deletions docs/decisions/0004-search-api-graphql-surface.md

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions packages/search-api-graphql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# @lde/search-api-graphql

The GraphQL surface for the [`@lde/search`](../search) core. **Both engine- and
domain-agnostic:** it builds an executable
[graphql-js](https://graphql.org/graphql-js/) `GraphQLSchema` from your whole
[`SearchSchema`](../search/README.md#terminology) at runtime — one root query
field per `SearchType`, each searchable in its own way. All root fields are
served by the same resolver implementation (no per-type code, no codegen);
each root field gets its own instance of it, bound to that field’s
`SearchType`, over any `SearchEngine`. It names neither your **domain** (each type’s GraphQL name
is the `SearchType`’s own logical `name` — `Dataset`, `Person`, `CreativeWork`,
…) nor your **engine** (the resolver calls `context.engine`, be it
[`@lde/search-typesense`](../search-typesense) or another adapter).

## Runtime configuration, not codegen

`buildGraphQLSchema(schema)` constructs the GraphQL schema once at startup from
the field model — no SDL artifact, no generated resolver stubs. For you that
means: no codegen step in the build, no generated files to commit and review,
and no stale artifact that can drift from the declaration — change the
`SearchType`, restart, and the API is current. (The flip side, no artifact
showing contract changes as diffs, is restored by the
[snapshot guard](#guarding-the-contract).) The field model
is the single source; the GraphQL contract is derived from it. Type names
come from each `SearchType`’s `name`; output types, the `where`/`orderBy`/facet
inputs, reference types and nullability are all derived from each field’s
`kind` and capability flags. The common case needs no options at all:

```ts
import { searchSchema } from '@lde/search';
import { buildGraphQLSchema } from '@lde/search-api-graphql';

const gqlSchema = buildGraphQLSchema(searchSchema(DATASET, PERSON));

// The API now serves `datasets(…)` and `persons(…)` root fields.
// Hand `gqlSchema` to any graphql-js server; populate the per-request context:
// { engine: SearchEngine, acceptLanguage: string[] }
```

Per-type options are pure fine-tuning, only for the types that need it: a
`queryField` when the default root field (the lowercased plural of the type’s
`name`) is wrong, and a `queryDefaults` policy applied to every query of that
type:

```ts
const gqlSchema = buildGraphQLSchema(searchSchema(DATASET, PERSON), {
types: {
[DATASET.type]: {
queryDefaults: (query) => ({
...query,
where: [...query.where, { field: 'status', in: ['valid'] }],
}),
},
[PERSON.type]: { queryField: 'people' },
},
});
```

Shared types (`LanguageString`, the facet buckets, filter inputs and reference
types such as a common `Agent`) are created once and reused across root types.

## Serving a subset of the schema

`types` never filters: every `SearchType` in the schema you pass gets a root
field (options for a type not in the schema are a build-time error). To expose
only part of what you index, narrow the **schema argument**
(`searchSchema(…)` is a cheap constructor):

```ts
// Index all three types…
projectGraph(quads, searchSchema(DATASET, PERSON, INTERNAL));

// …but serve only two.
const gqlSchema = buildGraphQLSchema(searchSchema(DATASET, PERSON));
```

## What it builds (per root type)

- **Output type** (the `SearchType`’s `name`): localized text → best-first `[LanguageString!]!`
(`[0].language` is the language actually served); references → named per-shape
types (`Organization`, `Term`) with a `name`; scalars/booleans per kind; `date`
→ ISO 8601 string; nullability from `required` / `array` / `kind`.
- **`where`** one input per `filterable` field (`StringFilter`, `IntRange` /
`FloatRange` / `DateRange`, or `Boolean`); omitted entirely for a type with no
filterable fields.
- **`orderBy`**: `RELEVANCE` plus every `sortable` field, as an enum.
- **Facets**: an enum of every `facetable` field; a bucket carries `value` +
`count` + a nullable `label` — the resolved data label for **reference** facets,
`null` for token/free-string facets whose display the consumer owns (its own
i18n, or the value itself).

## Guarding the contract

Why the API, the index and a future REST surface cannot drift apart is the
search family’s overall approach — one field model, one query IR — described
in [`@lde/search`](../search/README.md). Specific to this surface: the GraphQL
contract is **frozen** (breaking to change), yet generated rather than
handwritten, so nothing in the repo shows a contract change as a reviewable
diff. A _consumer_ restores that with one snapshot test over its **own**
search schema:

```ts
import { printGraphQLSchema } from '@lde/search-api-graphql';

it('keeps the public GraphQL contract stable', () => {
expect(printGraphQLSchema(searchSchema(DATASET, PERSON))).toMatchSnapshot();
});
```

The first run writes the emitted SDL to a committed snapshot file; every later
run re-emits and diffs against it. Any contract change — your own schema edit,
or a new version of this library emitting different GraphQL for the same
declaration — fails the test and shows the SDL diff, until you consciously
accept it (`vitest -u`) and the reviewer sees the contract change spelled out
in the PR.
22 changes: 22 additions & 0 deletions packages/search-api-graphql/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import baseConfig from '../../eslint.config.mjs';

export default [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs}',
'{projectRoot}/vite.config.{js,ts,mjs,mts}',
],
},
],
},
languageOptions: {
parser: await import('jsonc-eslint-parser'),
},
},
];
32 changes: 32 additions & 0 deletions packages/search-api-graphql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@lde/search-api-graphql",
"version": "0.1.0",
"description": "Engine- and domain-agnostic GraphQL surface for @lde/search: builds an executable GraphQLSchema from a whole SearchSchema at runtime (no codegen) — one root query field per SearchType — served by generic resolvers over any SearchEngine. You supply the schema and per-type typeNames; it names neither your domain nor your engine.",
"repository": {
"url": "git+https://github.com/ldelements/lde.git",
"directory": "packages/search-api-graphql"
},
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"!**/*.tsbuildinfo"
],
"dependencies": {
"@lde/search": "^0.1.2",
"graphql": "^15.8.0",
"tslib": "^2.3.0"
}
}
Loading