Files
next.orly.dev/.claude/skills/cypher/references/common-patterns.md
2025-12-03 10:25:31 +00:00

9.0 KiB

Common Cypher Patterns for ORLY Nostr Relay

This reference contains project-specific Cypher patterns used in the ORLY Nostr relay's Neo4j backend.

Schema Overview

Node Types

Label Purpose Key Properties
Event Nostr events (NIP-01) id, kind, pubkey, created_at, content, sig, tags, serial
Author Event authors (for NIP-01 queries) pubkey
Tag Generic tags type, value
NostrUser Social graph users (WoT) pubkey, name, about, picture, nip05
ProcessedSocialEvent Social event tracking event_id, event_kind, pubkey, superseded_by
Marker Internal state markers key, value

Relationship Types

Type From To Purpose
AUTHORED_BY Event Author Links event to author
TAGGED_WITH Event Tag Links event to tags
REFERENCES Event Event e-tag references
MENTIONS Event Author p-tag mentions
FOLLOWS NostrUser NostrUser Contact list (kind 3)
MUTES NostrUser NostrUser Mute list (kind 10000)
REPORTS NostrUser NostrUser Reports (kind 1984)

Event Storage Patterns

Create Event with Full Relationships

This pattern creates an event and all related nodes/relationships atomically:

// 1. Create or get author
MERGE (a:Author {pubkey: $pubkey})

// 2. Create event node
CREATE (e:Event {
  id: $eventId,
  serial: $serial,
  kind: $kind,
  created_at: $createdAt,
  content: $content,
  sig: $sig,
  pubkey: $pubkey,
  tags: $tagsJson  // JSON string for full tag data
})

// 3. Link to author
CREATE (e)-[:AUTHORED_BY]->(a)

// 4. Process e-tags (event references)
WITH e, a
OPTIONAL MATCH (ref0:Event {id: $eTag_0})
FOREACH (_ IN CASE WHEN ref0 IS NOT NULL THEN [1] ELSE [] END |
  CREATE (e)-[:REFERENCES]->(ref0)
)

// 5. Process p-tags (mentions)
WITH e, a
MERGE (mentioned0:Author {pubkey: $pTag_0})
CREATE (e)-[:MENTIONS]->(mentioned0)

// 6. Process other tags
WITH e, a
MERGE (tag0:Tag {type: $tagType_0, value: $tagValue_0})
CREATE (e)-[:TAGGED_WITH]->(tag0)

RETURN e.id AS id

Check Event Existence

MATCH (e:Event {id: $id})
RETURN e.id AS id
LIMIT 1

Get Next Serial Number

MERGE (m:Marker {key: 'serial'})
ON CREATE SET m.value = 1
ON MATCH SET m.value = m.value + 1
RETURN m.value AS serial

Query Patterns

Basic Filter Query (NIP-01)

MATCH (e:Event)
WHERE e.kind IN $kinds
  AND e.pubkey IN $authors
  AND e.created_at >= $since
  AND e.created_at <= $until
RETURN e.id AS id,
       e.kind AS kind,
       e.created_at AS created_at,
       e.content AS content,
       e.sig AS sig,
       e.pubkey AS pubkey,
       e.tags AS tags,
       e.serial AS serial
ORDER BY e.created_at DESC
LIMIT $limit

Query by Event ID (with prefix support)

// Exact match
MATCH (e:Event {id: $id})
RETURN e

// Prefix match
MATCH (e:Event)
WHERE e.id STARTS WITH $idPrefix
RETURN e

Query by Tag (# filter)

MATCH (e:Event)
OPTIONAL MATCH (e)-[:TAGGED_WITH]->(t:Tag)
WHERE t.type = $tagType AND t.value IN $tagValues
RETURN DISTINCT e
ORDER BY e.created_at DESC
LIMIT $limit

Count Events

MATCH (e:Event)
WHERE e.kind IN $kinds
RETURN count(e) AS count

Query Delete Events Targeting an Event

MATCH (target:Event {id: $targetId})
MATCH (e:Event {kind: 5})-[:REFERENCES]->(target)
RETURN e
ORDER BY e.created_at DESC

Replaceable Event Check (kinds 0, 3, 10000-19999)

MATCH (e:Event {kind: $kind, pubkey: $pubkey})
WHERE e.created_at < $newCreatedAt
RETURN e.serial AS serial
ORDER BY e.created_at DESC

Parameterized Replaceable Event Check (kinds 30000-39999)

MATCH (e:Event {kind: $kind, pubkey: $pubkey})-[:TAGGED_WITH]->(t:Tag {type: 'd', value: $dValue})
WHERE e.created_at < $newCreatedAt
RETURN e.serial AS serial
ORDER BY e.created_at DESC

Social Graph Patterns

Update Profile (Kind 0)

MERGE (user:NostrUser {pubkey: $pubkey})
ON CREATE SET
  user.created_at = timestamp(),
  user.first_seen_event = $event_id
ON MATCH SET
  user.last_profile_update = $created_at
SET
  user.name = $name,
  user.about = $about,
  user.picture = $picture,
  user.nip05 = $nip05,
  user.lud16 = $lud16,
  user.display_name = $display_name

Contact List Update (Kind 3) - Diff-Based

// Mark old event as superseded
OPTIONAL MATCH (old:ProcessedSocialEvent {event_id: $old_event_id})
SET old.superseded_by = $new_event_id

// Create new event tracking
CREATE (new:ProcessedSocialEvent {
  event_id: $new_event_id,
  event_kind: 3,
  pubkey: $author_pubkey,
  created_at: $created_at,
  processed_at: timestamp(),
  relationship_count: $total_follows,
  superseded_by: null
})

// Get or create author
MERGE (author:NostrUser {pubkey: $author_pubkey})

// Update unchanged relationships to new event
WITH author
OPTIONAL MATCH (author)-[unchanged:FOLLOWS]->(followed:NostrUser)
WHERE unchanged.created_by_event = $old_event_id
  AND NOT followed.pubkey IN $removed_follows
SET unchanged.created_by_event = $new_event_id,
    unchanged.created_at = $created_at

// Remove old relationships for removed follows
WITH author
OPTIONAL MATCH (author)-[old_follows:FOLLOWS]->(followed:NostrUser)
WHERE old_follows.created_by_event = $old_event_id
  AND followed.pubkey IN $removed_follows
DELETE old_follows

// Create new relationships for added follows
WITH author
UNWIND $added_follows AS followed_pubkey
MERGE (followed:NostrUser {pubkey: followed_pubkey})
MERGE (author)-[new_follows:FOLLOWS]->(followed)
ON CREATE SET
  new_follows.created_by_event = $new_event_id,
  new_follows.created_at = $created_at,
  new_follows.relay_received_at = timestamp()
ON MATCH SET
  new_follows.created_by_event = $new_event_id,
  new_follows.created_at = $created_at

Create Report (Kind 1984)

// Create tracking node
CREATE (evt:ProcessedSocialEvent {
  event_id: $event_id,
  event_kind: 1984,
  pubkey: $reporter_pubkey,
  created_at: $created_at,
  processed_at: timestamp(),
  relationship_count: 1,
  superseded_by: null
})

// Create users and relationship
MERGE (reporter:NostrUser {pubkey: $reporter_pubkey})
MERGE (reported:NostrUser {pubkey: $reported_pubkey})
CREATE (reporter)-[:REPORTS {
  created_by_event: $event_id,
  created_at: $created_at,
  relay_received_at: timestamp(),
  report_type: $report_type
}]->(reported)

Get Latest Social Event for Pubkey

MATCH (evt:ProcessedSocialEvent {pubkey: $pubkey, event_kind: $kind})
WHERE evt.superseded_by IS NULL
RETURN evt.event_id AS event_id,
       evt.created_at AS created_at,
       evt.relationship_count AS relationship_count
ORDER BY evt.created_at DESC
LIMIT 1

Get Follows for Event

MATCH (author:NostrUser)-[f:FOLLOWS]->(followed:NostrUser)
WHERE f.created_by_event = $event_id
RETURN collect(followed.pubkey) AS pubkeys

WoT Query Patterns

Find Mutual Follows

MATCH (a:NostrUser {pubkey: $pubkeyA})-[:FOLLOWS]->(b:NostrUser)
WHERE (b)-[:FOLLOWS]->(a)
RETURN b.pubkey AS mutual_friend

Find Followers

MATCH (follower:NostrUser)-[:FOLLOWS]->(user:NostrUser {pubkey: $pubkey})
RETURN follower.pubkey, follower.name

Find Following

MATCH (user:NostrUser {pubkey: $pubkey})-[:FOLLOWS]->(following:NostrUser)
RETURN following.pubkey, following.name

Hop Distance (Trust Path)

MATCH (start:NostrUser {pubkey: $startPubkey})
MATCH (end:NostrUser {pubkey: $endPubkey})
MATCH path = shortestPath((start)-[:FOLLOWS*..6]->(end))
RETURN length(path) AS hops, [n IN nodes(path) | n.pubkey] AS path

Second-Degree Connections

MATCH (me:NostrUser {pubkey: $myPubkey})-[:FOLLOWS]->(:NostrUser)-[:FOLLOWS]->(suggested:NostrUser)
WHERE NOT (me)-[:FOLLOWS]->(suggested)
  AND suggested.pubkey <> $myPubkey
RETURN suggested.pubkey, count(*) AS commonFollows
ORDER BY commonFollows DESC
LIMIT 20

Schema Management Patterns

Create Constraint

CREATE CONSTRAINT event_id_unique IF NOT EXISTS
FOR (e:Event) REQUIRE e.id IS UNIQUE

Create Index

CREATE INDEX event_kind IF NOT EXISTS
FOR (e:Event) ON (e.kind)

Create Composite Index

CREATE INDEX event_kind_created_at IF NOT EXISTS
FOR (e:Event) ON (e.kind, e.created_at)

Drop All Data (Testing Only)

MATCH (n) DETACH DELETE n

Performance Patterns

Use EXPLAIN/PROFILE

// See query plan without running
EXPLAIN MATCH (e:Event) WHERE e.kind = 1 RETURN e

// Run and see actual metrics
PROFILE MATCH (e:Event) WHERE e.kind = 1 RETURN e

Batch Import with UNWIND

UNWIND $events AS evt
CREATE (e:Event {
  id: evt.id,
  kind: evt.kind,
  pubkey: evt.pubkey,
  created_at: evt.created_at,
  content: evt.content,
  sig: evt.sig,
  tags: evt.tags
})

Efficient Pagination

// Use indexed ORDER BY with WHERE for cursor-based pagination
MATCH (e:Event)
WHERE e.kind = 1 AND e.created_at < $cursor
RETURN e
ORDER BY e.created_at DESC
LIMIT 20