Say hello to the Graph Model Domain Specific Language (GMDSL), created with the help of Github’s Copilot.
I was ready to iterate on the implementation of BrainExpanded, especially the backend graph data model. I wanted to explore the use of Neo4j as my graph store and also start getting ready for a live deployment to real alpha-testing users. As I was learning Cypher (Neo4j’s graph langauge), it occured to me that I had to iterate on many aspects of the end-to-end, before I could finalize the graph data model for the graph store.
I also realized that the node types and relationships in the graph store will evolve differently for different users so the OpenAPI contract will have to evolve as well or be appropriately generic to support such an evolution. Stay tuned on this topic.
So what do software engineers do in such a case? They build a tool to automate things. Well, at least this is what I decided to do. I have been using Github’s Copilot a lot for BrainExpanded but for this little project, Copilot wrote 90-95% of the code with me supervising the iteration process as we were adding more features.
The result is a very simple DSL that helps me describe a simple graph data model. The tool then generates the Cypher, OpenAPI, and C# classes. The Python and Swift classes will be generated from the OpenAPI using existing tools.
Given that this is a generic tool and not tightly coupled with BrainExpanded, I am opening up the repository to everyone in case there is interest in adding more plugins or evolving its functionality. You can find it at https://github.com/savasp/gmdsl.
Here’s a simple example of a graph data model description:
namespace Example
// Import some core types for properties
import GM.Core
// New types can be declared for properties
type Address {
street: String
city: String
state: String
zipcode: Integer
country: String
}
// Declare some graph nodes
node Person {
firstName: String
lastName: String
dateOfBirth: DateTime
placeOfBirth: Location
}
node Company {
name: String
address: Address
}
// Department graph node
node Department {
name: String
}
// Declare some edges. As per Neo4j's data model, edges may have
// properties
edge PartOf(Department -> Company)
edge Friend(Person <-> Person) {
metOn: DateTime
}
edge Works(Person -> Company) {
role: String
}
I can now generate Cypher schema declarations, C# classes, and the OpenAPI contract.
Here’s the generated Cypher schema (with the generated comments removed). Over time, I will add the necessary logic to the cypher plugin to generate additional statements.
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.firstName IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.lastName IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.dateOfBirth IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.placeOfBirth IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Company) REQUIRE n.name IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Company) REQUIRE n.address IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Department) REQUIRE n.name IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR ()-[r:Friend]-() REQUIRE r.metOn IS NOT NULL;
CREATE CONSTRAINT IF NOT EXISTS FOR ()-[r:Works]-() REQUIRE r.role IS NOT NULL;
Here are two of the classes generated by the C# plugin. Relationships are represented as objects. This plugin has a bug. It assumes that nodes have IDs. I am working on a fix which will indeed require all nodes and relationships to have IDs since that’s necessary for my use case.
namespace Example
{
public class Example.Person
{
public string firstName { get; set; }
public string lastName { get; set; }
public System.DateTime dateOfBirth { get; set; }
public None placeOfBirth { get; set; }
}
public class Example.Friend
{
public string SourceId { get; set; }
public string TargetId { get; set; }
public System.DateTime metOn { get; set; }
}
}
And here’s an example of a generated OpenAPI contract (only parts of it are shown). Both graph nodes and relationships are represented as HTTP resources with the usual HTTP verbs.
paths:
/people:
post:
summary: Create Person
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/person'
responses:
'201':
description: Created
/people/{node-id}:
get:
summary: Get Person by ID
responses:
'200':
description: OK
put:
summary: Update Person by ID
...
delete:
summary: Delete Person by ID
...
/companies:
post:
...
/companies/{node-id}:
get:
...
put:
...
...
/people/{node-id}/friends:
post:
summary: Create Friend from Person
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/friend'
...
/people/{node-id}/friends/{relationship-id}:
get:
...
put:
...
...
...
components:
schemas:
person:
type: object
properties:
firstName:
type: string
lastName:
type: string
dateOfBirth:
type: string
placeOfBirth:
type: string
...
friend:
type: object
properties:
metOn:
type: string
This was a fun few hours-long distraction from the main work on BrainExpanded. I enjoyed it and learned even more about how to use Github’s Copilot as a coding companion. How do the kids call it these days? “Vibe coding”? I don’t like the term but here we are 🙂
As I wrote in previous posts, the manual recording of memories for BrainExpanded is just…
Imagine a world where your memory is enhanced by a team of intelligent agents, working…
As part of the BrainExpanded project, I’m building an iOS app that lets users easily…
Artificial Intelligence (AI) has rapidly evolved over the past few decades, becoming an integral part…
Happy New Year everyone! I was planning for my next BrainExpanded post to be a…
See "BrainExpanded - Introduction" for context on this post. Notes and links Over the years,…