One of the feedback items for Nexus was the ability to define models with Nim code, particularly Nim objects. Defining with Nim object types isn't ideal, because there's no validation or any way to provide defaults, so for the prototype code below I went with procs that help you define objects.
Any feedback on this method would be very helpful. The current way of doing this is by defining the models with YAML.
import options
import schema_types
proc setupModes*(context: var MyContext) =
contexts.models.add(
newModel(
name = "Account User",
description = "This is to manage users in the system, by account.",
tableOptions = newTableOptions(
namingCase = snakeCase),
fields = @[
newField(
name = "Id",
fieldType = stringType,
constraints = @[
autoGenerateConstraint,
notNullConstraint
],
autoGenerate = some(ulidGenerator)),
newField(
name = "Account Id",
fieldType = stringType,
fkReference =
some(newFkReference(
model = "Account",
refCol = "Id"))),
newField(
name = "Name",
fieldType = stringType),
newField(
name = "Email",
fieldType = stringType,
constraints = @[
notNullConstraint
])
],
indexes = @[
newIndex(
name = some("Account User Ix 1"),
columns = @[
"Email"
]
)
]))
Are snakeCase , stringType , the various constraints etc. all enums?
I think choosing to make this data available at runtime (by having it like this instead of as a type) may open you up to a lot of interesting possibilities. Could the declaration also be a const ?
Here's the schema_types.nim file, sorry I should have included this in my original post. The consts could be enums.
import options
const
snakeCase* = "snakeCase"
stringType* = "string"
ulidGenerator* = "ulid"
autoGenerateConstraint* = "autoGenerate"
notNullConstraint* = "notNull"
type
MyContext* = object
name*: string
FkReference* = object
model*: string
refCol*: string
Field* = object
name*: string
fieldType*: string
constraints*: seq[string]
autoGenerate*: Option[string]
fkReference*: Option[FkReference]
Fields* = seq[Field]
Index* = object
name*: Option[string]
columns*: seq[string]
Indexes* = seq[Index]
TableOptions* = object
namingCase*: string
Model* = object
name*: string
description*: string
tableOptions*: TableOptions
fields*: Fields
indexes*: Indexes
It sacrifices a lot of clarity, compared to the yaml version.
A model macro is the way to go, imho, and would e.g. let you use Nim syntax for that whole fields section. At the very least you could get rid of the extra boiler plate.
model:
## this is to manage users
type User{.cacheable.} = ref object
id{.autoGen:ulidGenerator,notNull.}: string
account_id{.fieldRef:Account.id.}: string
name: string
email{.notNull.}: string
indices(unique=true):
"Account User Ix 1":
email@example.com
other
"Account User Ix 2":
foo@qux.net
other
Here's simpler way to write the models in Nim:
import schema_types
proc setupModels*() =
var model: Model
model.name = "Account User"
model.description = "Users by account"
model.tableOptions.namingCase = snakeCase
var
idField: Field
accountIdField: Field
nameField: Field
emailField: Field
idField.name = "Id"
idField.fieldType = stringType
idField.constraints = @[
autoGenerateConstraint,
notNullConstraint
]
idField.autoGenerate = ulidGenerator
accountIdField.name = "Account Id"
accountIdField.fieldType = stringType
accountIdField.constraints = @[ notNullConstraint ]
accountIdField.fkReference.model = "Account"
accountIdField.fkReference.refCol = "Id"
nameField.name = "Name"
nameField.fieldType = stringType
nameField.constraints = @[ notNullConstraint ]
emailField.name = "Email"
emailField.fieldType = stringType
emailField.constraints = @[ notNullConstraint ]
model.fields = @[
idField,
accountIdField,
nameField,
emailField
]
var emailIndex: Index
emailIndex.name = "Account User Ix 1"
emailIndex.columns = @[ "Email" ]
model.indexes = @[ emailIndex ]
echo $model
# Main
setupModels()
In this approach you generate the foreign keys from the YAML definitions. Everything is defined in YAML first.
However I would like to eventually write a tool to reverse engineer an existing schema into YAML.