@maykonpaulo/maestro-provider-neo4j
v1.0.3
Published
Neo4j provider for @maykonpaulo/maestro-core — the first graph-family provider, per ADR 0007. Real CRUD (a node label is the entity), schema-catalog-with-sampling-fallback introspection, and missing-label creation (a documented no-op — Neo4j has no notion
Readme
@maykonpaulo/maestro-provider-neo4j
📖 Guia completo — criar o admin de qualquer sistema: está no README do pacote
@maykonpaulo/maestro-serverno npm → https://www.npmjs.com/package/@maykonpaulo/maestro-server
A Neo4j provider for @maykonpaulo/maestro-core — the first graph-family provider, continuing ADR 0007 — Universal Provider Strategy. It implements all three of the core's provider contracts against a real Neo4j database:
IntrospectionProvider— discovers node labels and reports each one's property shape, preferring Neo4j's real property catalog (db.schema.nodeTypeProperties()) and falling back to sampling when the catalog is unavailable. Every entity is reported withschemaless: true.DatasourceProvider— reallist/findById/create/update/delete/count, translated to parameterized Cypher.SchemaWriteProvider—ensureEntityis a documented no-op (see below).
Why a node label is the "table"
A node label (e.g. :User) is the closest analogue to an entity/table in a property graph — the same choice this monorepo's other schema-less providers make for their own closest structural unit (a Mongo collection, a Redis key prefix). Multi-label nodes are not attempted in v1: only the first/primary label configured per entity is targeted by CRUD.
Installation
npm install @maykonpaulo/maestro-provider-neo4j @maykonpaulo/maestro-core@maykonpaulo/maestro-core and neo4j-driver are peer dependencies — install the versions your project already uses.
Usage
import { runIntrospectionProvider } from '@maykonpaulo/maestro-core';
import { createNeo4jProvider, ELEMENT_ID_SENTINEL } from '@maykonpaulo/maestro-provider-neo4j';
const provider = createNeo4jProvider({ uri: 'bolt://localhost:7687', username: 'neo4j', password: 'password' });
// Introspection — catalog-first, sampling-fallback, validated/normalized via the core
const { result } = await runIntrospectionProvider(provider);
// Real CRUD against the same database — elementId() as the default identity
const created = await provider.create({ table: 'User', primaryKey: ELEMENT_ID_SENTINEL, data: { name: 'Ada' } });
const page = await provider.list({ table: 'User', primaryKey: ELEMENT_ID_SENTINEL, pagination: { strategy: 'page', page: 1, pageSize: 20 } });Configuration
export interface Neo4jProviderOptions {
uri?: string; // a bolt://... or neo4j://... connection string
username?: string;
password?: string;
driver?: Driver; // an already-connected driver — for tests/embedding; caller owns its lifecycle
database?: string; // Neo4j database name within the DBMS; unset uses the server default
maxNodes?: number; // caps nodes read per label on the sampling-fallback introspection path only — unset by default (full scan)
}Exactly one of uri or driver must be provided.
id handling
Neo4j assigns every node an internal, always-present elementId() automatically — analogous to Mongo's auto-ObjectId. This provider treats it as the safest zero-config default for a pre-existing real database:
primaryKey: '_elementId'(the exportedELEMENT_ID_SENTINELconstant) selects Neo4j's ownelementId()as identity.create()never sets it itself — Neo4j assigns it at creation. If the caller'sdatahappens to include an_elementIdproperty anyway, it is written as an ordinary node property (a caller error, not specially handled).- Any other
primaryKeyvalue is treated as a normal node property.create()generates arandomUUID()string when the caller doesn't supply one, matching every other provider in this monorepo.
What is introspected
- Fields — preferably Neo4j's real property catalog for the label (
CALL db.schema.nodeTypeProperties(), available on Neo4j 4.3+), which reflects every property ever indexed for that label — more complete than a sample window. When the procedure is unavailable (older server, or a restricted managed-service tier) or returns nothing for the label, a full-scan sampling fallback infers the shape the same wayprovider-mongodbdoes (field union, most-recently-seen type, nullable when absent from some nodes). schemaless: trueon every entity, in both code paths — Neo4j never enforces a property schema on a label regardless of how the properties were discovered; a node written a moment later can still introduce a property this result never saw.
What is not introspected (known limitations)
- Relations — deliberately deferred,
relationsis always[].db.schema.visualization()reports label-to-label relationship types present anywhere in the graph, not per-instance{ table, field }pairs theRelationIntrospectionSchemacontract expects (Neo4j relationships are edges, not foreign-key-style fields) — translating one to the other would require inventing a synthetic field name with no real graph analog. This keeps graph-native relation introspection available as a clearly-scoped future enhancement rather than baking in a half-solution now, and keeps this provider consistent with every other NoSQL provider in this monorepo, none of which claims inferred relations from a sampled/inferred source. - Multi-label nodes — only the first/primary label configured per entity is targeted by CRUD.
- Large integers — a stored integer outside JS's safe integer range loses precision when normalized to a plain
number(seesrc/neo4jValues.ts), the same lossy-by-default tradeoffprovider-cassandramakes for its driver'sLong/BigDecimalwrapper types. - Temporal properties (
Date,DateTime,LocalDateTime,Time,LocalTime,Duration) are stringified via their owntoString(), not parsed back into a typed value.
Schema creation (ensureEntity)
Node labels are created implicitly by writing the first node that carries them — there is no CREATE LABEL DDL, and there is no way to "create" a label that doesn't also require writing a node. ensureEntity therefore always reports created: false; the label "exists" the moment its first node is written via create(). See src/Neo4jSchemaWrite.ts for the full reasoning.
Testing
Unit tests (tests/neo4j-provider-construction.test.ts) cover constructor validation only, no Docker required. Integration tests (tests/neo4j-integration.test.ts) run against a real Neo4j server started per test run via @testcontainers/neo4j — run them with pnpm test:integration (requires a running Docker daemon).
Turnkey admin (point at your database, get a UI)
This provider plugs straight into the Maestro turnkey layer: install
@maykonpaulo/maestro-server and
@maykonpaulo/maestro-admin, point them at your database and get a
full governed admin (list/CRUD/RBAC/audit + a metadata-driven UI) with no
per-entity code.
import { createNeo4jProvider } from '@maykonpaulo/maestro-provider-neo4j';
import { createMaestroServer, createIntrospectedEngine } from '@maykonpaulo/maestro-server';
const provider = createNeo4jProvider({ uri: 'bolt://localhost:7687', username: 'neo4j', password: 'password' });
const engine = await createIntrospectedEngine({ provider, access: 'full' });
await createMaestroServer({ engine }).listen(3000);See the Turnkey admin guide.
