Source-of-truth guide for AI agents (Claude Code, Codex, etc.) working in this repository.
CLAUDE.md is a thin pointer to this file; keep substantive changes here.
db/schema-simple.json is the authoritative dump of the live Neo4j graph schema —
every node label, its properties (with types), every relationship type with its valid
start/end label sets. Before writing or reviewing any Cypher query (in *-db-queries.go,
a migration, or ad-hoc), consult it to confirm:
- the exact label spelling and which properties exist on a node,
- the relationship type name and which labels it can connect (some types are reused
across many start/end labels — e.g.
WAS_UPDATED_BY,BELONGS_TO_FACILITY), - whether a property is
String,Long,Double,Boolean,DateTime, orStringArray.
The file is regenerated by running db/schema-simple-query.cypher against the live DB
(requires the APOC plugin). Re-dump it after any schema-changing migration.
make install—go mod download && go mod verifymake swagger—swag init -g server.gothen copydocs/swagger.yamltoopen-api-specification/panda-api.yaml. Run after any handler / Swagger annotation change.make run— regenerates Swagger thengo run server.gomake build— regenerates Swagger then builds the binary with-ldflags "-s -w"make test—go test ./...- Single test:
go test ./services/<pkg> -run TestFunctionName - Local stack (API + Neo4j + MinIO):
docker-compose -f docker-compose-local.yml up -d --build/... down - Debug build:
swag init -g server.go && dlv debug . --build-flags "-tags=dev"(needsdlv) - Swagger build tags:
-tags=dev|test|prodselectswagger_dev.go/swagger_test.go/swagger_prod.gobase config
Test DB connection is initialized once via services/testsetup/testsetup.go's init() —
it loads .env (project root, two levels up from a service test dir) and opens a shared
Neo4j driver/session. There is no automatic per-test cleanup (CleanTestDatabase is
intentionally a no-op); tests must not leave durable state or must clean up themselves.
REST API gateway for the PANDA database (Go 1.22 + Echo + Neo4j community edition). Vertical-slice service layout designed to allow future microservices extraction.
- Sets
time.LocaltoEurope/Prague. - Loads config from
.envviaconfig/config.go(godotenv autoload). - Wires Echo middlewares: static (Swagger), CORS, request logger, panic recover.
- Runs Neo4j migrations (
db.MigrateNeo4jMainInstance) usinggolang-migrateover files indb/neo4j/migrations/— multi-statement.up.cypher/.down.cypher. Migrations run on every startup; failing migration leaves theSchemaMigrationnode indirty: trueand blocks subsequent boots until manually resolved. - Creates the shared
*neo4j.Driverand theUserStatusValidator(with a TTL cache). - Builds
JwtMiddleware(secret, userStatusValidator). services.InitializeServicesAndMapRoutes(...)— registers every service.- Starts HTTP on
:API_PORT, graceful shutdown on SIGINT (10 s timeout).
Each domain lives in services/{name}-service/ with these conventional files:
{name}-service.go— service struct, publicI{Name}Serviceinterface, business logic{name}-handlers.go— Echo handlers, returnecho.HandlerFuncper route{name}-routes.go—Map{Name}Routes(e, handlers, jwtMiddleware)registers HTTP routes{name}-db-queries.go— Cypher queries (some services inline them in the service file)models/(optional) — request/response/db structs
Registration in services/init.go follows the same three-line pattern for every service
(New...Service → New...Handlers → Map...Routes). Initialization order matters:
codebook-service depends on catalogue, security, systems, orders, and
publications — publications must be initialized before codebook. Adjust order if you
add cross-service dependencies.
Custom JWT claims (see helpers/jwt.go): Roles []string, FacilityCode string,
Subject string (user UID), Id string (username).
Route shape:
e.GET("/v1/resource", m.Authorization(h.GetResource(), shared.ROLE_SYSTEMS_VIEW), jwtMiddleware)middlewares.Authorization (in middlewares/auth.go) runs after JWT validation,
reads claims via helpers.GetUserFromJWT(c), and sets context values
facilityCode, userUID, userName, userRoles. Access granted if the user has any
one of the listed roles. Role constants live in shared/roles.go — use them, never
string literals.
middlewares.UserStatusValidator caches per-user isEnabled lookups against Neo4j with
TTL AUTH_USER_STATUS_CACHE_TTL_SECONDS to avoid hitting the DB on every authenticated
request.
helpers/database.go—NewNeo4jSession,DatabaseQuerystruct, generic readersGetNeo4jSingleRecordAndMapToStruct,GetNeo4jArrayOfNodes, writerWriteNeo4jAndReturnSingleValue. Tag struct fields withneo4j:"prop,<name>"/neo4j:"key,<name>"to drive the experimentalCreateOrUpdateNodeQueryreflector.helpers/change_tracking.go— audit-trail helpers for PATCH endpoints.AppendIfChanged(entries, field, ChangeType, old, new)only appends when values differ; codebook values compare byUIDfield, others viareflect.DeepEqual; typed-nil pointers normalize to JSONnull.MarshalChanges(entries)produces the string stored onWAS_UPDATED_BY.changes. Use these for any new mutating endpoint that should record what changed.helpers/field_projection.go— partial-update field projection for PATCH semantics.helpers/echo.go— HTTP error helpers, e.g.BadRequest(message).helpers/jwt.go— token parsing +GetUserFromJWT(c)claims extractor.
- POST (create):
201with object,400invalid request (with details),409conflict (plain string),202async. - PUT/PATCH (update):
200with updated object,204no content,400,404,409. - DELETE:
200on success or non-existent entity,409conflict,202async. - GET:
200with results or empty array;404only for specific-entity lookups. - Generic:
401auth,403authorization,405method not allowed,500internal.
Copy example.env → .env. Key vars:
API_JWT_SECRET— JWT signing secret (must be strong in non-local envs)API_PORT— HTTP port (default50000)BCRYPT_SALT_ROUNDS— password hash costAUTH_USER_STATUS_CACHE_TTL_SECONDS— UserStatusValidator cache TTLNEO4J_HOST/NEO4J_PORT/NEO4J_USER/NEO4J_PASSWORD/NEO4J_SCHEMA(e.g.bolt://)API_INTEGRATION_B_OKBASE_*— OKBase HR system (employees sync)API_INTEGRATION_B_WOS_STARTER_*— Web of Science (publications import)
Local Neo4j ports (per docker-compose-local.yml mapping):
- Browser UI:
http://localhost:7470 - Bolt from API/host tools:
localhost:7600 - Bolt from inside Neo4j Browser:
neo4j://localhost:7680(browser-internal mapping, not the default7687)
Swagger UI (local): http://localhost:50000/swagger/index.html
- Files in
db/neo4j/migrations/named<timestamp>_<name>.{up,down}.cypher. - New migration:
db/neo4j/create-new-migration.sh <name>(creates the up/down pair). - Manual up/down:
db/neo4j/migrate-up.sh/migrate-down.sh. - Multi-statement Cypher is supported.
- Do not put leading
//comments at the very top of migration files — thegolang-migrateNeo4j driver mis-parses them. Inline//comments inside a statement are fine. - Rename refactors that need APOC: use
apoc.refactor.rename.*procedures. - Dirty migration recovery: connect to Neo4j and manually
MATCH (n:SchemaMigration) SET n.dirty = false, n.version = <last-good-version>after fixing the offending Cypher, then restart.
- Live next to their packages,
_test.gosuffix,testify/assert. - Shared Neo4j connection from
services/testsetup(driver is opened byinit()). - Single test:
go test ./services/<pkg> -run TestFunctionName -v. - Many tests assume the local Neo4j stack is up (
docker-compose-local.yml) and may rely on migrated schema + seed data — start the stack before running them.
- Package names: lowercase, single word.
- Functions/methods:
CamelCase; variables:camelCase. - Interfaces prefixed
I(e.g.ISecurityService). - Imports grouped: standard library, internal (
panda/apigateway/...), third-party. - Error handling: explicit checks; for HTTP 400 use
helpers.BadRequest(...). - Logging:
github.com/rs/zerolog/logwith structured fields. - Run
gofmton touched files (tabs for indentation). - Add Swagger annotations on every handler; regenerate with
make swagger.
- Imperative subject lines; Conventional Commit prefixes are common (
feat:,fix:,refactor:,docs:,test:, with optional scope likefeat(catalogue): ...). - Include issue IDs when applicable (e.g.
ELIPANDA-455). - Branch from
dev(feat/<short-description>), PR back todev. - PR description must cover: what changed, why, how tested, whether Swagger was regenerated, and any migration impact.
- Skills directory:
.claude/skills/is a symlink to.agents/skills/— edit skills under.agents/, the symlink keeps Claude Code's discovery working. - Do not add Claude/AI attribution to commits or PR bodies.
- Commit per logical step, not one big commit at the end.
- Never auto-merge PRs.