Migrating from @neo4j/graphql
@loradb/lora-graphql reads a similar annotated SDL and keeps many
directive names (@node, @relationship, @cypher, @authorization),
so most schemas port with a handful of edits. The generated API is
deliberately smaller, though, and some behaviour differs in ways clients
will notice. Read the comparison before you move a production API.
At a glance
| @neo4j/graphql | lora-graphql |
|---|---|
| Every type gets every operation | Reads by default; mutations and subscriptions opt in with @mutation and @subscription |
| Every field filterable by every operator | @filterable(byValue:), checked against the type |
| Every field sortable | @sortable |
Offset cursors (arrayconnection:N) | Keyset cursors tagged with their sort, optionally signed |
update / delete with an optional where | By @key, or bulk with a required, bounded where |
connect: { where }, silently a no-op when nothing matches | Connect by key; a missing target is NOT_FOUND |
| Single relationships not enforced; required ones refused | Enforced from both sides; required ones supported |
{ viewers: { add: 1 } } in update | adjust: { viewers: { add: 1 } } |
@id | @key(generate: true) |
@populatedBy, @timestamp | Same names |
@authorization rules evaluated in Cypher | Claim checks folded in JavaScript at compile time; node rules compiled into the statement |
| JWT decoded and verified by the library | Your server verifies the JWT; the library reads verified claims |
| You create indexes | Inferred from the API, created by assertSchema(), verified with explain() |
| Unbounded lists; query complexity left to you | Every list bounded; a cost limit per operation |
| Subscriptions over CDC | @subscription, from library writes, or every committed write with changeFeed: true |
Interfaces and unions with UNION | Per-member subqueries merged by sort, each using its own indexes |
connectOrCreate | upsert<Plural> |
| Federation | Not supported |
Run the migration command
lora-graphql migrate neo4j schema.graphql --operations src/operations
lora-graphql migrate neo4j <schema.graphql> [--operations <file|dir>]
prints the rewritten SDL on standard output, preceded by one
# TODO(migrate): ... line per decision left to you:
@idbecomes@key(generate: true), and@nodeis added where it is missing. A type with no@idfield, or with several, gets a TODO to choose its key, and a@uniquebeside@idis dropped as redundant.@fulltext(itsindexNamebecomesname),@subscription(events:)(relationship events becomerelationships: true) and the@relationshiparguments are translated.queryDirectionis kept only where it was undirected.@mutation,@filterableand@sortableare opt-in here. With--operations(your client operations, in either the neo4j 5title_CONTAINSor the neo4j 6{ title: { contains } }form), they follow what the operations use. Without it, every type keeps its mutations, as in@neo4j/graphql, and a TODO says to narrow them.- What has no equivalent (
@coalesce,@exclude,@subscriptionsAuthorization, the federation directives,connectOrCreate, type-level@vector) is removed and listed as a TODO.@authorizationand@authenticationrules are kept, with a TODO to check their filter operators.
The output is a draft, not a finished schema. Save it
(lora-graphql migrate neo4j schema.graphql > schema.lora.graphql), diff
it against the original, resolve every TODO, and then run the checks
below.
Then check it
lora-graphql print schema.lora.graphql # the public API clients will see
lora-graphql check schema.lora.graphql --operations src/operations
print shows the generated API without directives, so you can compare it
with the old one. check compiles every operation against the new schema
and plans it, so an operation that uses a filter you did not opt in, or a
query that would scan a label, fails here instead of in production.
What changes for clients
These are the differences clients see, roughly in order of how often they bite:
- Fewer filters and sorts. Only what the schema opts in. Operations
that use anything else fail validation. Collect your operations and run
checkagainst them. - Keyset pagination. Cursors are opaque and tied to their sort.
Replaying a cursor under another sort is
INVALID_CURSOR, and there is no offset. - Bounded lists.
limitandfirstdefault to 25 and fail above 100 (both configurable per type with@limit, and globally). A client that relied on unbounded lists must page. - Keys, not
where, for single writes.updateFestival(key:)anddeleteFestival(key:)address one node. BulkupdateFestivalsanddeleteFestivalsneed a non-emptywhereand fail, writing nothing, when more thanlimitnodes match. - Connect fails loudly. Connecting to a key that does not exist, or
that the caller cannot see, is
NOT_FOUNDand rolls the mutation back. - Math and list operators move to
adjust. - Errors carry codes.
extensions.codeis one ofBAD_USER_INPUT,INVALID_CURSOR,LIMIT_EXCEEDED,COST_EXCEEDED,UNAUTHENTICATED,TIMEOUT,FORBIDDEN,NOT_FOUND,CONSTRAINT_VIOLATIONorDATABASE_ERROR, plusPERSISTED_QUERY_ONLYandWRONG_OPERATION_TYPEfromexecute()andsubscribe(). See errors.
What changes on the server
- JWT verification moves to your server.
@neo4j/graphqlcan decode and verify tokens; lora-graphql never does. Verify the token and put the claims in the context asjwt. See authorization. - Claims should be declared. Add a
@jwttype so rules that reference an unknown claim fail at startup. - Indexes come from the API. Drop hand-written index DDL for what the
API uses and run
assertSchema({ create: true }), or applylora-graphql requirements --ddlin your migrations. - Subscriptions follow library writes unless you opt into the feed.
By default events come from mutations made through the
LoraGraphQLinstance, and a@cyphermutation produces a broad change unless its statement returns what it wrote ascreatedorupdated. WithchangeFeed: true(lora-node) they come from the engine's committed change feed instead, which covers every committed write to the database from the process that owns it, raw Cypher included. It is not CDC: a database is open in one process at a time, and the feed starts at the current commit, so writes made while the server was down are not replayed.migrateadds a TODO to every@subscriptionit rewrites. @cypheris the same idea with stricter checks. Parameters must be field arguments or$jwt, and statements onQueryfields may not write.checkplans every statement.
Not planned
These @neo4j/graphql features are left out on purpose:
- Offset pagination and
arrayconnectioncursors. updateanddeletewith an optional, unboundedwhere.- Every operation on every type, every filter on every field, and the deprecated flat filter aliases.
unsafeEscapeOptions.- JWT verification in the library.
- Vector providers that call external embedding APIs from the database layer.
- A single relationship that silently returns the first of several. Cardinality is enforced instead.
connectOrCreate, which neo4j 7 removed;upsertcovers the need.- Federation, unless there is demand for it.