Skip to content

Instantly share code, notes, and snippets.

@lantos1618
Created January 2, 2025 13:56
Show Gist options
  • Select an option

  • Save lantos1618/1a918b64977b88ef1a790abac5cd880f to your computer and use it in GitHub Desktop.

Select an option

Save lantos1618/1a918b64977b88ef1a790abac5cd880f to your computer and use it in GitHub Desktop.
One shoting nim orm with claud
import macros, strutils, sequtils, jsony, db_sqlite
from typetraits import name
type
Relation = object
kind: RelationKind
foreignKey: string
throughTable: string
sourceField: string
targetField: string
modelType: string
RelationKind = enum
rkOneToOne
rkOneToMany
rkManyToMany
Hook = enum
beforeCreate
afterCreate
beforeUpdate
afterUpdate
beforeDelete
afterDelete
Model = ref object of RootObj
id: int
createdAt: string
updatedAt: string
template relation(kind: RelationKind, foreignKey="", throughTable="",
sourceField="id", targetField="", modelType=""): Relation =
Relation(
kind: kind,
foreignKey: foreignKey,
throughTable: throughTable,
sourceField: sourceField,
targetField: targetField,
modelType: modelType
)
template one_to_one(T: typed, foreignKey="", sourceField="id", targetField=""): Relation =
relation(rkOneToOne, foreignKey, "", sourceField, targetField, name(T))
template one_to_many(T: typed, foreignKey="", sourceField="id", targetField=""): Relation =
relation(rkOneToMany, foreignKey, "", sourceField, targetField, name(T))
template many_to_many(T: typed, throughTable: string): Relation =
relation(rkManyToMany, "", throughTable, "id", "id", name(T))
var modelHooks = newTable[string, Table[Hook, seq[NimNode]]]()
macro model*(head, body: untyped): untyped =
## Main macro to define models
result = newStmtList()
# Extract model name and parent
var
modelName = head
parentType = ident("Model")
if head.kind == nnkInfix and head[0].strVal == "of":
modelName = head[1]
parentType = head[2]
# Create type definition
var typeFields = newNimNode(nnkRecList)
var relations: seq[Relation]
var hooks = initTable[Hook, seq[NimNode]]()
# Process model body
for node in body:
case node.kind
of nnkCall:
if node[0].strVal == "field":
typeFields.add(newIdentDefs(
node[1],
node[2],
newEmptyNode()
))
of nnkCommand:
# Handle relation definitions
case node[0].strVal:
of "belongs_to":
# Add foreign key field
let fkName = node[1].strVal.toLowerAscii & "Id"
typeFields.add(newIdentDefs(
ident(fkName),
ident("int"),
newEmptyNode()
))
relations.add(one_to_one(node[1].strVal.capitalizeAscii, fkName))
of "has_many":
relations.add(one_to_many(node[1].strVal.capitalizeAscii))
of "many_to_many":
relations.add(many_to_many(node[1].strVal.capitalizeAscii,
node[2].strVal))
of "hook":
let hookName = parseEnum[Hook](node[1].strVal)
hooks[hookName] = @[node[2]]
else: discard
# Store hooks
modelHooks[$modelName] = hooks
# Create the type
result.add quote do:
type `modelName` = ref object of `parentType`
`typeFields`
# Generate relationship accessors
for rel in relations:
let
targetType = ident(rel.modelType)
methodName = ident(rel.modelType.toLowerAscii)
case rel.kind
of rkOneToOne:
result.add quote do:
proc `methodName`*(m: `modelName`): `targetType` =
let query = "SELECT * FROM " & `rel.modelType` &
" WHERE id = ?"
queryOne(`targetType`, query, m.`ident(rel.foreignKey)`)
of rkOneToMany:
result.add quote do:
proc `methodName`*(m: `modelName`): seq[`targetType`] =
let query = "SELECT * FROM " & `rel.modelType` &
" WHERE " & `rel.foreignKey` & " = ?"
queryMany(`targetType`, query, m.id)
of rkManyToMany:
result.add quote do:
proc `methodName`*(m: `modelName`): seq[`targetType`] =
let query = """
SELECT t.* FROM """ & `rel.modelType` & """ t
JOIN """ & `rel.throughTable` & """ j
ON j.""" & `rel.modelType.toLowerAscii` & """_id = t.id
WHERE j.""" & `modelName.strVal.toLowerAscii` & "_id = ?"
queryMany(`targetType`, query, m.id)
# Generate CRUD methods
result.add quote do:
proc save*(m: `modelName`) =
if m.id == 0:
# Insert
runHooks(`modelName`, beforeCreate)
m.createdAt = now()
m.updatedAt = now()
m.id = insertInto(`modelName`, m)
runHooks(`modelName`, afterCreate)
else:
# Update
runHooks(`modelName`, beforeUpdate)
m.updatedAt = now()
update(`modelName`, m)
runHooks(`modelName`, afterUpdate)
proc delete*(m: `modelName`) =
runHooks(`modelName`, beforeDelete)
deleteFrom(`modelName`, m.id)
runHooks(`modelName`, afterDelete)
proc find*[T: `modelName`](typ: typedesc[T], id: int): T =
queryOne(T, "SELECT * FROM " & name(T) & " WHERE id = ?", id)
proc all*[T: `modelName`](typ: typedesc[T]): seq[T] =
queryMany(T, "SELECT * FROM " & name(T))
proc runHooks(T: typedesc, hook: Hook) =
if $T in modelHooks:
let hooks = modelHooks[$T]
if hook in hooks:
for h in hooks[hook]:
h.treerepr.templ
# Example usage:
when isMainModule:
model User:
field name: string
field email: string
posts: one_to_many(Post, on: "id", "userId")
roles: many_to_many(Role, through: "user_roles")
hook beforeCreate:
validate(email, "email")
setDefaults()
model Post:
field title: string
field content: string
user: one_to_one(User, on: "userId", "id")
hook beforeSave:
validate(title, "required")
slugify(title)
# Usage
let user = User.new(name: "John", email: "john@test.com")
user.save()
let post = Post.new(
title: "Hello World",
userId: user.id
)
post.save()
# Queries
let johns_posts = user.posts()
for post in johns_posts:
echo post.user().name # Access relations
# Find existing
let found = User.find(1)
let all = User.all()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment