Skip to content

feat(search-api-server): serve the search API as a bootable process and Docker image#646

Merged
ddeboer merged 3 commits into
mainfrom
feat/search-graphql-docker
Jul 24, 2026
Merged

feat(search-api-server): serve the search API as a bootable process and Docker image#646
ddeboer merged 3 commits into
mainfrom
feat/search-graphql-docker

Conversation

@ddeboer

@ddeboer ddeboer commented Jul 23, 2026

Copy link
Copy Markdown
Member

Part of #600 (layer 3 of 3: the prebuilt images; the handler, layer 2, shipped in #637).

Ships the search stack as two prebuilt, workspace-built Docker images that boot from the same mounted schema-declaration module, loaded by the shared @lde/search/module loader so the write side and the read side cannot disagree about the schema:

  • ghcr.io/ldelements/search-api-server (@lde/search-api-server, new): the served search API – mount a schema module, set TYPESENSE_* env vars, get /graphql (POST execution, self-contained playground, ?sdl) plus /health, with CORS and depth/cost limits on by default. The composition layer binding the deliberately engine-agnostic @lde/search-api-graphql handler to @lde/search-typesense.
  • ghcr.io/ldelements/search-indexer (@lde/search-indexer, new): the write side – a Commander CLI wrapping searchIndexerPipeline, configured entirely from environment variables. Dataset selection against a DCAT registry (explicit IRIs, search criteria, or the whole registry), in-place (default) or blue-green Typesense rebuild writers, optional file provenance, and an optional QLever import path behind QLEVER_IMAGE (ADR 16). --check validates config + schema module and exits; the Docker smoke boots the image with it.

Design (ADR 15, ADR 16)

  • Schema input: a plain-data declaration module, not SHACL. Batteries-included served GraphQL search API + self-contained playground #600 assumed mounted SHACL + search: definitions, but that generator is GraphQL search API #495’s still-open scope. The mount is an .mjs default-exporting SearchType declarations, loaded and validated at boot by the new @lde/search/module entry point (Node-only, like ./adapter and ./testing) – the “declarations built outside TypeScript” path the runtime validation exists for. Functions (derive, transform) remain expressible: the indexer projects with them, the server ignores them – one module drives both images. When GraphQL search API #495 lands, mounted SHACL becomes a second source for the same schema.
  • Separate composition packages, because a Typesense-bound server cannot live in @lde/search-api-graphql without breaking its engine-agnosticism.
  • The images are built from the workspace’s own outputs, never from npm – adopting the pattern the Dataset Register converged on (build: stop bundling the api and crawler node apps netwerk-digitaal-erfgoed/dataset-register#2130): docker:build (inferred by the new @nx/docker plugin) stages the compiled package, the same-commit builds of its @lde/* dependencies (@nx/js:copy-workspace-modules) and a pruned lockfile (@nx/js:prune-lockfile + an npm install --package-lock-only repair step, because Nx’s pruning cannot map this workspace’s graph and falls back to the root lockfile); the Dockerfiles are a plain npm ci --omit=dev + COPY. No bundling, deliberately: bundling hid missing transitives until boot in the Dataset Register (#2128).
  • docker:smoke gates CI for both images: every affected PR builds the image and boots it (the server probed over real HTTP on /health + /graphql?sdl; the indexer via --check), so runtime-only failures – the ERR_MODULE_NOT_FOUND class – cannot pass a green build. The smoke step runs after the test suites so image builds cannot starve timing-sensitive tests on the shared runner.
  • Releases are registry-independent: an @lde/<package>@<version> tag triggers a workflow that derives the package from the tag, checks out that commit, rebuilds, smoke-tests and pushes :<version> + :latest. No waiting on the npm publish; image tags stay aligned with the package semver. nx release’s own Docker support (experimental, calendar-versioned, cannot npm- and Docker-publish one project) is deliberately not used.
  • The read-only server image omits @lde/search-typesense’s write-side peers (@lde/pipeline, @lde/dataset): every runtime import is import type, and the smoke test guards that assumption.

Also

  • feat(search): validateSearchType now rejects an unknown field kind (new unknown-kind issue reason). Previously a typo’d kind in a plain-JS declaration passed every kind-dependent rule vacuously – exactly the path the mounted schema module relies on.

Acceptance criteria covered (of #600)

After merge

@lde/search-api-server and @lde/search-indexer are new on npm: each first version needs the manual Trusted-Publishing bootstrap for npm consumers. The Docker images are unaffected – they build from the workspace.

@ddeboer
ddeboer force-pushed the feat/search-graphql-docker branch 2 times, most recently from 1a3a7af to 41e2650 Compare July 23, 2026 14:06
@ddeboer
ddeboer force-pushed the feat/search-graphql-docker branch from 41e2650 to fcb4c9a Compare July 24, 2026 11:05
@ddeboer ddeboer closed this Jul 24, 2026
@ddeboer ddeboer reopened this Jul 24, 2026
- validateSearchType silently accepted a kind outside the FieldKind union:
  every kind-dependent rule passed vacuously, deferring the failure to
  whatever consumer read the declaration first
- guards the plain-JS declaration path (a mounted schema module, a SHACL
  generator) that the runtime validation exists for
- adds the 'unknown-kind' issue reason
@ddeboer
ddeboer force-pushed the feat/search-graphql-docker branch 3 times, most recently from dbf5cd6 to fa245f9 Compare July 24, 2026 12:19
…nd Docker image

- new composition package binding the engine-agnostic search-api-graphql
  handler to a Typesense engine: environment config, a mounted plain-data
  schema-declaration module (validated at boot), and a node:http server
  answering /graphql, /health and a root redirect
- the search-api-server bin boots from environment variables alone and
  reports every configuration problem in one error
- the image is built from the workspace's own outputs, never from npm:
  docker:build (inferred by @nx/docker) stages the compiled package, the
  same-commit builds of its @lde/* dependencies (copy-workspace-modules)
  and a pruned lockfile (prune-lockfile plus an npm --package-lock-only
  repair step for the Nx pruning fallback); the Dockerfile is npm ci + COPY
- docker:smoke boots each affected image in CI with a fixture schema and
  probes /health and the SDL over HTTP, so runtime-only failures (the
  ERR_MODULE_NOT_FOUND class) cannot pass a green build
- each release tag rebuilds, smoke-tests and pushes
  ghcr.io/ldelements/search-api-server:<version>, independent of the npm
  publish
- the read-only image omits the write-side peers (@lde/pipeline,
  @lde/dataset): every runtime import is 'import type'
- recorded as ADR 15
…ge (#655)

* feat(search): add the schema-module loader as @lde/search/module

- loadSchemaModule() loads and validates a mounted schema-declaration
  module (an .mjs default-exporting SearchType declarations as plain
  data) and returns the validated schema plus the raw exports, so each
  consumer validates its own optional exports
- Node-only, so it lives in a separate entry point like ./adapter and
  ./testing, keeping the main entry point runtime-agnostic
- The one loader both the indexer image and the served-API image boot
  from, so the write and the read side cannot disagree about the schema

* refactor(search-api-server): load the schema module via @lde/search/module

- The generic load-and-validate logic moves to the shared loader; what
  stays here is validating the read side's optional exports
  (schemaOptions, engineOptions)
- Add a test for the non-object optional export failure mode

* feat(search-indexer): ship the search indexer as a CLI and Docker image

- New @lde/search-indexer package: a Commander CLI wrapping
  searchIndexerPipeline, configured entirely from environment variables
  (boot-or-fail, all problems reported in one error)
- Mounts the same schema-declaration module as the search-api-server
  image, loaded with the shared @lde/search/module loader
- Dataset selection against a DCAT registry: explicit IRIs (DATASETS),
  search criteria (DATASET_CRITERIA), or the whole registry
- Typesense rebuild writers per root type: in-place (default) or
  blue-green, with an optional derived-collection-name prefix
- Optional file provenance (PROVENANCE_FILE + PIPELINE_VERSION);
  rejected with blue-green rebuilds, which would drop skipped datasets
- Optional QLever import path behind QLEVER_IMAGE: data dumps are
  imported into pipeline-controlled sibling containers over the Docker
  socket (see ADR 16 for why a static QLever service cannot work)
- --check validates configuration and schema module, then exits; the
  Docker smoke test boots the image with it to catch pruned-image
  module-resolution failures in CI
- The docker.yml release workflow now derives the package from the tag,
  serving both search-api-server and search-indexer images
@ddeboer
ddeboer merged commit 153ef29 into main Jul 24, 2026
2 checks passed
@ddeboer
ddeboer deleted the feat/search-graphql-docker branch July 24, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant