Part of Elody — the open semantic data platform.
The foundation module for every Elody GraphQL service. It boots an Apollo Server on top of Express, wires in the shared baseModule schema, handles Keycloak/OAuth session auth, ships the default REST endpoints (upload, download, export, health, version, app configs, auth, SEO, Prometheus), and exposes the AuthRESTDataSource base class that every other module extends.
Client GraphQL services import the start() function from this package, pass a customModuleConfig (with additional modules and data sources — e.g. from mediafile-module, import-module, advanced-filters-module), and get a fully wired server.
baseGraphql is a library, not a runnable service on its own — every real deployment is a client-side GraphQL service that imports start() and layers on modules, translations, permissions, etc. A minimal, runnable one lives in examples/starter-client-graphql-service — copy it as a starting point for a new client.
cd examples/starter-client-graphql-service
cp .env.dist .env
pnpm install
pnpm devIn development mode, APOLLO_SESSION_SECRET and APOLLO_CLIENT_SECRET are auto-defaulted to random hex values with a startup warning — no env setup required to see the server boot. In production (NODE_ENV=production) they are still required and boot throws if unset.
| Layer | What it adds |
|---|---|
| Bootstrapper | start({ customModuleConfig, appConfig, customTranslations, ... }) — creates Express + Apollo, applies auth, runs OIDC discovery, mounts default endpoints, serves the PWA (Vite in dev, static in prod), prints a startup banner |
| GraphQL schema | baseModule — Entity, Metadata, Relations, Form, User, Permissions, Menu, BulkOperations, Formatters, and many more shared types |
| GraphQL resolvers | Base resolvers for entities, metadata, initial values, context menu, map component, formatters |
| DataSources | CollectionAPI, TranscodeService, StorageAPI, OcrService, GraphQLAPI — extendable via declaration merging on DataSources |
| Auth | OIDC flow with optional .well-known/openid-configuration auto-discovery, session (in-memory or MongoDB), token refresh, static-token fallback, AuthRESTDataSource for downstream calls |
| Express endpoints | /api/graphql, /api/auth_code, /api/logout, /api/me, /api/upload/*, /api/download/csv, /api/download/zip/:id, /api/export/csv, /api/app-configs, /api/version, /api/health, /api/seo, /api/prom/query_range |
| Middleware | CORS, CSP, compression, JSON/urlencoded body limits, session cookies, depth-limit query validation |
On boot the server prints a titled banner listing mode, version, port, GraphQL path, collection-api URL, and the OIDC source (manual or discovery). The title reads ${APPLICATION_TITLE} GraphQL, so a client extending baseGraphql sets APPLICATION_TITLE=digipolis-dams (or similar) and sees its own name. Any env var that was auto-defaulted for dev is called out below the info rows.
CollectionAPI is the only required data source. If it's missing at request time the server responds with Missing required data sources: CollectionAPI. Extension modules (mediafileModule, importModule, ...) contribute optional data sources — if a future extension needs its data source to be mandatory, promote it to base rather than extending the required list from outside.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/auth_code |
Exchange an OAuth authorization code for a session (Keycloak PKCE flow). |
GET |
/api/logout |
Destroy the session and redirect to the Keycloak logout endpoint. |
GET |
/api/me |
Return the currently authenticated user's profile and token metadata. |
| Method | Path | Purpose |
|---|---|---|
POST |
/api/upload/xml |
Upload an XML payload; proxied to collection-api. |
POST |
/api/upload/csv |
Upload a CSV payload; proxied to collection-api. |
Batch/single file uploads for mediafiles live in
mediafileModule(/api/upload/batch,/api/upload/single).
| Method | Path | Purpose |
|---|---|---|
POST |
/api/download/csv |
Stream a CSV export of the requested entities. |
GET |
/api/download/zip/:id |
Trigger a zip build (via transcode-service) and stream the result. |
POST |
/api/export/csv |
Kick off a CSV export job on collection-api. |
| Method | Path | Purpose |
|---|---|---|
GET |
/api/app-configs |
Return runtime config the PWA needs at boot: translations, feature flags, type→URL mapping, OAuth endpoints, custom formatters. |
| Method | Path | Purpose |
|---|---|---|
GET |
/api/version |
Return the running service version. |
GET |
/api/health |
Liveness probe. |
GET |
/api/prom/query_range |
Proxy to Prometheus query_range (only mounted when api.promUrl is set). |
GET |
/api/seo |
Server-rendered SEO metadata (only mounted when features.SEO is true). |
Serves the PWA on all remaining routes. In non-production it mounts Vite in middleware mode with HMR on port 24678; in production it serves the built dashboard/ bundle.
| Query | Description |
|---|---|
Entity(id, type, preferredLanguage) |
Fetch a single entity. |
Entities(type, limit, skip, searchInputType, searchValue, advancedSearchValue, advancedFilterInputs, ...) |
Paginated, searchable, filterable entity list. |
EntitiesHistory(...) |
Same shape as Entities, scoped to history collection. |
EntitiesByAdvancedSearch(q, filter_by, query_by, ...) |
Typesense-style search against the search index. |
Tenants |
List tenants visible to the caller. |
User / UserPermissions / getElodyUser |
Current user info and permissions. |
Menu(name) |
Dynamic menu definition. |
EntityTypeSortOptions(entityType) |
Sortable field options for a type. |
PreviewComponents(entityType) / PreviewElement |
UI preview definitions. |
DropzoneEntityToCreate / PaginationLimitOptions |
UI defaults. |
BulkOperations(entityType) / CustomBulkOperations / BulkOperationsRelationForm |
Bulk-action definitions. |
BulkOperationCsvExportKeys(entityType) |
Fields available for CSV export. |
GraphData(id, graph) |
Data for the graph view. |
PermissionMapping(entities) / PermissionMappingPerEntityType(type) / PermissionMappingCreate(entityType) / PermissionMappingEntityDetail(id, entityType) |
Permission lookups. |
AdvancedPermission(permission, parentEntityId, childEntityId) / AdvancedPermissions(permissions, ...) |
Relation-aware permission checks. |
CustomFormattersSettings |
Client-supplied metadata formatter config. |
GetDynamicForm / GetRepetitiveForm |
Form definitions rendered by DynamicForm.vue. |
GetEntityDetailContextMenuActions |
Right-click / context-menu actions on the entity detail page. |
GeoFilterForMap |
Map-view filter definition. |
FilterMatcherMapping(keys) / EntityTypeFilters(type) / FilterOptions(input, limit, entityType) |
Filter metadata and dropdown option resolution. |
| Mutation | Description |
|---|---|
mutateEntityValues(id, formInput, collection, preferredLanguage) |
Persist a form submit. |
deleteData(id, path, deleteMediafiles) |
Delete an entity, optionally cascading to mediafiles. |
bulkDeleteEntities(ids, path, deleteEntities, skipItemsWithRelationDuringBulkDelete) |
Bulk delete. |
bulkAddRelations(entityIds, relationEntityId, relationType) |
Bulk add a relation to many entities. |
addEntityRelations(id, relations, collection) |
Add relations to a single entity. |
updateMetadataWithCsv(entityType, csv) |
Patch metadata from a CSV payload. |
setPrimaryMediafile(entityId, mediafileId) / setPrimaryThumbnail(entityId, mediafileId) |
Promote a mediafile. |
The full schema (2 600+ lines) lives in baseModule/baseSchema.schema.ts — types for Entity, Metadata, Relation, Form, FormTab, InputField, MenuItem, AdvancedFilter*, Formatter union, permission types, etc.
All extend AuthRESTDataSource and are injected into every resolver as context.dataSources.*. Downstream modules extend the DataSources interface via declaration merging.
| Source | Target | Purpose |
|---|---|---|
CollectionAPI |
api.collectionApiUrl |
The main REST client — entities, metadata, relations, permissions, user info, tenants, jobs, forms, bulk ops, search. See sources/collection.ts (~40 methods). |
TranscodeService |
api.transcodeService |
Base class for transcode calls; extended by mediafileModule. |
StorageAPI |
api.storageApiUrl |
Typed handle to the storage service; most storage traffic goes through the mediafile proxy endpoints. |
OcrService |
api.ocrService |
OCR trigger endpoint; extended by mediafileModule. |
GraphQLAPI |
api.collectionApiUrl |
GraphQL-over-REST helper for querying collection-api's GraphQL surface. |
Extends Apollo's RESTDataSource. Injects the current session's bearer token, refreshes it via fetchWithTokenRefresh on 401, falls back to the static JWT (STATIC_JWT) when configured, and forwards X-Tenant-ID, X-Client-IP, and Origin headers to downstream services.
baseGraphql is the entry point of every GraphQL service. Client services call start() and spread in any additional modules:
import start, { ElodyModuleConfig } from 'base-graphql';
import { mediafileElodyConfig } from 'mediafile-module';
import { importModule, ImportAPI } from 'import-module';
import { advancedFiltersElodyConfig } from 'advanced-filters-module';
import customTranslations from './translations';
import appConfig from './appConfig';
const customModuleConfig: ElodyModuleConfig = {
modules: [
...mediafileElodyConfig.modules,
...advancedFiltersElodyConfig.modules,
importModule,
],
dataSources: {
...mediafileElodyConfig.dataSources,
...advancedFiltersElodyConfig.dataSources,
ImportAPI,
},
endpoints: [
...(mediafileElodyConfig.endpoints ?? []),
],
};
start({
customModuleConfig,
appConfig,
customTranslations,
customEndpoints: [/* extra express handlers */],
customInputFields: {/* client-specific InputField definitions merged into baseFields */},
customTypeCollectionMapping: {/* entityType -> Collection */},
customPermissions: {/* PermissionRequestInfo per key */},
customFormatters: {/* metadata display formatters */},
customTypeUrlMapping: { mapping: {}, reverseMapping: {} },
customTypePillLabelMapping: {/* type -> pill label keys */},
customFilterMatchers: {/* filter key -> match strategies */},
});customModuleConfig.modules— extragraphql-modulesto merge into the schema.customModuleConfig.dataSources— extra data source classes; use declaration merging to type them onDataSources.customModuleConfig.endpoints— extra Express handler factories(app, environment) => void.customInputFields— register newInputFieldtypes available in dynamic forms.customFormatters— hook into metadata rendering (link/pill/regexp).customPermissions— declare custom permission keys resolvable viaAdvancedPermission(s).customTypeUrlMapping/customTypePillLabelMapping/customTypeCollectionMapping— per-client type mappings.customFilterMatchers— extend the advanced-filter matcher registry.
| Key | Type | Description |
|---|---|---|
graphqlPath |
string |
Where the GraphQL API mounts. Default /api/graphql. |
introspection |
boolean |
Enable GraphQL introspection. |
playground |
boolean |
Enable the GraphQL playground. |
tokenLogging |
'true' | 'false' |
Log user tokens in the container (string flag — legacy). |
maxQueryDepth |
number |
Query depth limit. Default 10 (falls back to 15 if unset). |
Endpoint defaults have Keycloak's URL shape but any OIDC-compliant IdP (Auth0, Okta, Cognito, Google, Azure AD, ...) works. Recommended path for a new deployment: set OIDC_DISCOVERY_URL only, and the four endpoints below are auto-populated from .well-known/openid-configuration at boot. Individual OAUTH_*_ENDPOINT env vars still take precedence over discovered values, so existing setups keep working unchanged.
| Key | Description |
|---|---|
discoveryUrl |
Optional. OIDC issuer URL (env: OIDC_DISCOVERY_URL). When set, base URL and endpoints are resolved from the issuer's discovery document at boot. Falls back to the manual env vars below on failure — non-fatal. |
baseUrl |
Backend base URL of the OAuth provider (env: OAUTH_BASE_URL). |
baseUrlFrontend |
Frontend-facing base URL (env: OAUTH_BASE_URL_FRONTEND). |
clientId |
OAuth client id (env: OAUTH_CLIENT_ID). |
tokenEndpoint |
Default /protocol/openid-connect/token (env: OAUTH_TOKEN_ENDPOINT). |
logoutEndpoint |
Default /protocol/openid-connect/logout (env: OAUTH_LOGOUT_ENDPOINT). |
authEndpoint |
Default /protocol/openid-connect/auth (env: OAUTH_AUTH_ENDPOINT). |
apiCodeEndpoint |
Default /api/auth_code (env: OAUTH_API_CODE_ENDPOINT). |
| Key | Env var | Description |
|---|---|---|
collectionApiUrl |
COLLECTION_API_URL |
Collection API base URL. |
csvImportServiceUrl |
CSV_IMPORTER_URL |
CSV import service. |
fileSystemImporterServiceUrl |
FILE_SYSTEM_IMPORTER_URL |
Filesystem importer service (opt-in per client). |
iiifUrl / iiifUrlFrontend |
IMAGE_API_URL / IMAGE_API_URL_EXT |
Internal & external IIIF service URLs. |
storageApiUrl / storageApiUrlExt |
STORAGE_API_URL / STORAGE_API_URL_EXT |
Internal & external storage-api URLs. |
transcodeService |
TRANSCODE_SERVICE_URL |
Transcode service base URL. |
ocrService |
OCR_SERVICE_URL |
OCR service base URL. |
promUrl |
— | Prometheus URL, or 'no-prom' to skip mounting /api/prom/*. |
| Key | Description |
|---|---|
hasPersistentSessions |
With Mongo configured, store sessions in Mongo instead of memory so they survive deploys. |
hasRedirectToExternalSites |
Sets cookies to { sameSite: 'none', secure: true } so sessions survive round-trips through external redirects. Requires X-Forwarded-Proto: https — force it in Traefik when the LB strips it:"traefik.http.middlewares.${job_name}-forcehttps.headers.customrequestheaders.X-Forwarded-Proto=https""traefik.http.routers.${job_name}-http.middlewares=${job_name}-forcehttps" |
ipWhiteListing |
Log & enforce client IP allow-list. |
domainWhiteListing |
Log & enforce client Origin allow-list. |
SEO |
Mount /api/seo. |
| Key | Description |
|---|---|
staticToken (STATIC_JWT) |
Fallback bearer for unauthenticated / server-to-server calls. |
glitchtipEnabled / glitchtipDsn |
Sentry/Glitchtip error reporting. |
maxUploadSize |
Body-parser limit for JSON & urlencoded requests. |
port |
HTTP port (default from env). |