ZModel Language Reference
Overview​
ZModel, the modeling DSL of ZenStack, is the main concept you'll deal with when using this toolkit. The ZModel syntax is a superset of Prisma Schema. Therefore, every valid Prisma schema is a valid ZModel.
We made that choice to extend the Prisma schema for several reasons:
-
Creating a new ORM adds little value to the community. Instead, extending Prisma - the overall best ORM toolkit for Typescript - sounds more sensible.
-
Prisma's schema language is simple and intuitive.
-
Extending an existing popular language lowers the learning curve compared to inventing a new one.
However, the standard capability of Prisma schema doesn't allow us to build the functionalities we want in a natural way, so we made a few extensions to the language by adding the following:
- Custom attributes
- Custom attribute functions
- Built-in attributes and functions for defining access policies
- Built-in attributes for defining field validation rules
- Utility attributes like
@password
and@omit
- Multi-schema files support
Some of these extensions have been asked for by the Prisma community for some time, so we hope that ZenStack can be helpful even just as an extensible version of Prisma.
This section provides detailed descriptions of all aspects of the ZModel language, so you don't have to jump over to Prisma's documentation for extra learning.
Import​
ZModel allows to import other ZModel files. This is useful when you want to split your schema into multiple files for better organization. Under the hood, it will recursively merge all the imported schemas, and generate a single Prisma schema file for the Prisma CLI to consume.
Syntax​
import [File_PATH]
- [File_PATH]: Path to the ZModel file to be imported. Can be either a relative path or an absolute path, without .zmodel extension. Once a file is imported, all the declarations in that file will be included in the building process.
Examples​
// there is a file called "user.zmodel" in the same directory
import "user"
Data source​
Every model needs to include exactly one datasource
declaration, providing information on how to connect to the underlying database.
Syntax​
datasource [NAME] {
provider = [PROVIDER]
url = [DB_URL]
}
-
[NAME]:
Name of the data source. Needs to be a valid identifier matching regular expression
[A-Za-z][a-za-z0-9_]\*
. Name is only informational and serves no other purposes. -
[PROVIDER]:
Name of database connector. Valid values:
- sqlite
- postgresql
- mysql
- sqlserver
- cockroachdb
-
[DB_URL]:
Database connection string. Either a plain string or an invocation of
env
function to fetch from an environment variable.
Examples​
datasource db {
provider = "postgresql"
url = "postgresql://postgres:abc123@localhost:5432/todo?schema=public"
}
It's highly recommended that you not commit sensitive database connection strings into source control. Alternatively, you can load it from an environment variable:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Supported databases​
ZenStack uses Prisma to talk to databases, so all relational databases supported by Prisma are also supported by ZenStack.
Here's a list for your reference:
Database | Version |
---|---|
PostgreSQL | 9.6 |
PostgreSQL | 10 |
PostgreSQL | 11 |
PostgreSQL | 12 |
PostgreSQL | 13 |
PostgreSQL | 14 |
PostgreSQL | 15 |
MySQL | 5.6 |
MySQL | 5.7 |
MySQL | 8 |
MariaDB | 10 |
SQLite | * |
AWS Aurora | * |
AWS Aurora Serverless | * |
Microsoft SQL Server | 2022 |
Microsoft SQL Server | 2019 |
Microsoft SQL Server | 2017 |
Azure SQL | * |
CockroachDB | 21.2.4+ |
You can find the orignal list here.
Generator​
Generators are used for creating assets (usually code) from a Prisma schema. Check here for a list of official and community generators.
Syntax​
generator [GENERATOR_NAME] {
[OPTION]*
}
-
[GENERATOR_NAME]
Name of the generator. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression
[A-Za-z][a-za-z0-9_]\*
. -
[OPTION]
A generator configuration option, in form of "[NAME] = [VALUE]". A generator needs to have at least a "provider" option that specify its provider.
Example​
generator client {
provider = "prisma-client-js"
output = "./generated/prisma-client-js"
Plugin​
Plugins are ZenStack's extensibility mechanism. It's usage is similar to Generator. Users can define their own plugins to generate artifacts from the ZModel schema. Plugins differ from generators mainly in the following ways:
- They have a cleaner interface without the complexity of JSON-RPC.
- They use an easier-to-program AST representation than generators.
- They have access to language features that ZenStack adds to Prisma, like custom attributes and functions.
Syntax​
plugin [PLUGIN_NAME] {
[OPTION]*
}
-
[PLUGIN_NAME]
Name of the plugin. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression
[A-Za-z][a-za-z0-9_]\*
. -
[OPTION]
A plugin configuration option, in form of "[NAME] = [VALUE]". A plugin needs to have at least a "provider" option that specify its provider.
Example​
plugin swr {
provider = '@zenstackhq/swr'
output = 'lib/hooks'
}
Enum​
Enums are container declarations for grouping constant identifiers. You can use them to express concepts like user roles, product categories, etc.
Syntax​
enum [ENUM_NAME] {
[FIELD]*
}
-
[ENUM_NAME]
Name of the enum. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression
[A-Za-z][a-za-z0-9_]\*
. -
[FIELD]
Field identifier. Needs to be unique in the model. Needs to be a valid identifier matching regular expression
[A-Za-z][a-za-z0-9_]\*
.
Example​
enum UserRole {
USER
ADMIN
}
Model​
Models represent the business entities of your application. A model inherits all fields and attributes from extended abstract models. Abstract models are eliminated in the generated prisma schema file.
Syntax​
(abstract)? model [NAME] (extends [ABSTRACT_MODEL_NAME](,[ABSTRACT_MODEL_NAME])*)? {
[FIELD]*
}
-
[abstract]:
Optional. If present, the model is marked as abstract would not be mapped to a database table. Abstract models are only used as base classes for other models.
-
[NAME]:
Name of the model. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression
[A-Za-z][a-za-z0-9_]\*
. -
[FIELD]:
Arbitrary number of fields. See next section for details.
-
[ABSTRACT_MODEL_NAME]:
Name of an abstract model.
Note​
A model must include a field marked with @id
attribute. The id
field serves as a unique identifier for a model entity and is mapped to the database table's primary key.
See here for more details about attributes.
Example​
abstract model Basic {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User extends Basic {
name String
}
The generated prisma file only contains one User
model:
model User {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @id
}
Attribute​
Attributes decorate fields and models and attach extra behaviors or constraints to them.
Syntax​
Field attribute​
Field attribute name is prefixed by a single @
.
id String @[ATTR_NAME](ARGS)?
- [ATTR_NAME]
Attribute name. See below for a full list of attributes.
- [ARGS]
See attribute arguments.
Model attribute​
Field attribute name is prefixed double @@
.
model Model {
@@[ATTR_NAME](ARGS)?
}
- [ATTR_NAME]
Attribute name. See below for a full list of attributes.
- [ARGS]
See attribute arguments.
Arguments​
Attribute can be declared with a list of parameters and applied with a comma-separated list of arguments.
Arguments are mapped to parameters by position or by name. For example, for the @default
attribute declared as:
attribute @default(_ value: ContextType)
, the following two ways of applying it are equivalent:
published Boolean @default(value: false)
published Boolean @default(false)
Parameter types​
Attribute parameters are typed. The following types are supported:
-
Int
Integer literal can be passed as argument.
E.g., declaration:
attribute @password(saltLength: Int?, salt: String?)
application:
password String @password(saltLength: 10)
-
String
String literal can be passed as argument.
E.g., declaration:
attribute @id(map: String?)
application:
id String @id(map: "_id")
-
Boolean
Boolean literal or expression can be passed as argument.
E.g., declaration:
attribute @@allow(_ operation: String, _ condition: Boolean)
application:
@@allow("read", true)
@@allow("update", auth() != null) -
ContextType
A special type that represents the type of the field onto which the attribute is attached.
E.g., declaration:
attribute @default(_ value: ContextType)
application:
f1 String @default("hello")
f2 Int @default(1) -
FieldReference
References to fields defined in the current model.
E.g., declaration:
attribute @relation(
_ name: String?,
fields: FieldReference[]?,
references: FieldReference[]?,
onDelete: ReferentialAction?,
onUpdate: ReferentialAction?,
map: String?)application:
model Model {
...
// [ownerId] is a list of FieldReference
owner Owner @relation(fields: [ownerId], references: [id])
ownerId
} -
Enum
Attribute parameter can also be typed as predefined enum.
E.g., declaration:
attribute @relation(
_ name: String?,
fields: FieldReference[]?,
references: FieldReference[]?,
// ReferentialAction is a predefined enum
onDelete: ReferentialAction?,
onUpdate: ReferentialAction?,
map: String?)application:
model Model {
// 'Cascade' is a predefined enum value
owner Owner @relation(..., onDelete: Cascade)
}
An attribute parameter can be typed as any of the types above, a list of the above type, or an optional of the types above.
model Model {
...
f1 String
f2 String
// a list of FieldReference
@@unique([f1, f2])
}
Attribute functions​
Attribute functions are used for providing values for attribute arguments, e.g., current DateTime
, an autoincrement Int
, etc. They can be used in place of attribute arguments, like:
model Model {
...
serial Int @default(autoincrement())
createdAt DateTime @default(now())
}
You can find a list of predefined attribute functions here.
Predefined attributes​
Field attributes​
@id​
attribute @id(map: String?)
Defines an ID on the model.
Params:
Name | Description |
---|---|
map | The name of the underlying primary key constraint in the database |
@default​
attribute @default(_ value: ContextType)
Defines a default value for a field.
Params:
Name | Description |
---|---|
value | The default value expression |
@unique​
attribute @unique(map: String?)
Defines a unique constraint for this field.
Params:
Name | Description |
---|---|
map | The name of the underlying primary key constraint in the database |
@relation​
attribute @relation(
_ name: String?,
fields: FieldReference[]?,
references: FieldReference[]?,
onDelete: ReferentialAction?,
onUpdate: ReferentialAction?,
map: String?)
Defines meta information about a relation.
Params:
Name | Description |
---|---|
name | The name of the relationship |
fields | A list of fields defined in the current model |
references | A list of fields of the model on the other side of the relation |
onDelete | Referential action to take on delete. See details here. |
onUpdate | Referential action to take on update. See details here. |
@map​
attribute @map(_ name: String)
Maps a field name or enum value from the schema to a column with a different name in the database.
Params:
Name | Description |
---|---|
map | The name of the underlying column in the database |
@updatedAt​
attribute @updatedAt()
Automatically stores the time when a record was last updated.
@ignore​
attribute @ignore()
Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update).
@allow​
attribute @allow(_ operation: String, _ condition: Boolean)
Defines an access policy that allows the annotated field to be read or updated. Read more about access policies here.
Params:
Name | Description |
---|---|
operation | Comma separated list of operations to control, including "read" and "update" . Pass "all" as an abbreviation for including all operations. |
condition | Boolean expression indicating if the operations should be allowed |
@deny​
attribute @deny(_ operation: String, _ condition: Boolean)
Defines an access policy that denies the annotated field to be read or updated. Read more about access policies here.
Params:
Name | Description |
---|---|
operation | Comma separated list of operations to control, including "read" and "update" . Pass "all" as an abbreviation for including all operations. |
condition | Boolean expression indicating if the operations should be denied |
@password​
attribute @password(saltLength: Int?, salt: String?)
Indicates that the field is a password field and needs to be hashed before persistence.
NOTE: ZenStack uses the "bcryptjs" library to hash passwords. You can use the saltLength
parameter to configure the cost of hashing or use salt
parameter to provide an explicit salt. By default, a salt length of 12 is used. See here for more details.
Params:
Name | Description |
---|---|
saltLength | The length of salt to use (cost factor for the hash function) |
salt | The salt to use (a pregenerated valid salt) |
@omit​
attribute @omit()
Indicates that the field should be omitted when read from the generated services. Commonly used together with @password
attribute.
@prisma.passthrough​
attribute @prisma.passthrough(_ text: String)
A utility attribute for passing arbitrary text to the generated Prisma schema. This is useful as a workaround for dealing with discrepancies between Prisma schema and ZModel.
Params:
Name | Description |
---|---|
text | Text to passthrough to Prisma schema |
E.g., the following ZModel content:
model User {
id Int @id @default(autoincrement())
name String @prisma.passthrough("@unique")
}
wil be translated to the following Prisma schema:
model User {
id Int @id @default(autoincrement())
name String @unique
}
Model attributes​
@@id​
attribute @@id(_ fields: FieldReference[], name: String?, map: String?)
Defines a multi-field ID (composite ID) on the model.
Params:
Name | Description |
---|---|
fields | A list of fields defined in the current model |
name | The name that the Client API will expose for the argument covering all fields |
map | The name of the underlying primary key constraint in the database |
@@unique​
attribute @@unique(_ fields: FieldReference[], name: String?, map: String?)
Defines a compound unique constraint for the specified fields.
Params:
Name | Description |
---|---|
fields | A list of fields defined in the current model |
name | The name of the unique combination of fields |
map | The name of the underlying unique constraint in the database |
@@schema​
attribute @@schema(_ name: String)
Specifies the database schema to use in a multi-schema setup.
Params:
Name | Description |
---|---|
name | The name of the database schema |
@@index​
attribute @@index(_ fields: FieldReference[], map: String?)
Defines an index in the database.
Params:
Name | Description |
---|---|
fields | A list of fields defined in the current model |
map | The name of the underlying index in the database |
@@map​
attribute @@map(_ name: String)
Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database.
Params:
Name | Description |
---|---|
name | The name of the underlying table or enum in the database |
@@ignore​
attribute @@ignore()
Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update).
@@allow​
attribute @@allow(_ operation: String, _ condition: Boolean)
Defines an access policy that allows a set of operations when the given condition is true. Read more about access policies here.
Params:
Name | Description |
---|---|
operation | Comma separated list of operations to control, including "create" , "read" , "update" , and "delete" . Pass "all" as an abbriviation for including all operations. |
condition | Boolean expression indicating if the operations should be allowed |
@@deny​
attribute @@deny(_ operation: String, _ condition: Boolean)
Defines an access policy that denies a set of operations when the given condition is true. Read more about access policies here.
Params:
Name | Description |
---|---|
operation | Comma separated list of operations to control, including "create" , "read" , "update" , and "delete" . Pass "all" as an abbriviation for including all operations. |
condition | Boolean expression indicating if the operations should be denied |
@@auth​
attribute @@auth()
Specify the model for resolving auth()
function call in access policies. By default, the model named "User" is used. You can use this attribute to override the default behavior. A Zmodel can have at most one model with this attribute.
@@prisma.passthrough​
attribute @@prisma.passthrough(_ text: String)
A utility attribute for passing arbitrary text to the generated Prisma schema. This is useful as a workaround for dealing with discrepancies between Prisma schema and ZModel.
Params:
Name | Description |
---|---|
text | Text to passthrough to Prisma schema |
E.g., the following ZModel content:
model User {
id Int @id @default(autoincrement())
name String
@@prisma.passthrough("@@unique([name])")
}
wil be translated to the following Prisma schema:
model User {
id Int @id @default(autoincrement())
name String
@@unique([name])
}
Predefined attribute functions​
uuid()​
function uuid(): String {}
Generates a globally unique identifier based on the UUID spec.
cuid()​
function cuid(): String {}
Generates a globally unique identifier based on the CUID spec.
now()​
function now(): DateTime {}
Gets current date-time.
autoincrement()​
function autoincrement(): Int {}
Creates a sequence of integers in the underlying database and assign the incremented values to the ID values of the created records based on the sequence.
dbgenerated()​
function dbgenerated(expr: String): Any {}
Represents default values that cannot be expressed in the Prisma schema (such as random()).
auth()​
function auth(): User {}
Gets the current login user. The return type of the function is the User
model defined in the current ZModel.
future()​
function future(): Any {}
Gets the "post-update" state of an entity. Only valid when used in a "update" access policy. Read more about access policies here.
contains()​
function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean {}
Checks if the given field contains the search string. The search string is case-sensitive by default. Use caseInSensitive
to toggle the case sensitivity.
Equivalent to Prisma's contains operator.
search()​
function search(field: String, search: String): Boolean {}
Checks if the given field contains the search string using full-text-search.
Equivalent to Prisma's search operator.
startsWith()​
function startsWith(field: String, search: String): Boolean {}
Checks if the given field starts with the search string.
Equivalent to Prisma's startsWith operator.
endsWith()​
function endsWith(field: String, search: String): Boolean {}
Checks if the given field ends with the search string.
Equivalent to Prisma's endsWith operator.
has()​
function has(field: Any[], search: Any): Boolean {}
Check if the given field (list) contains the search value.
Equivalent to Prisma's has operator.
hasEvery()​
function hasEvery(field: Any[], search: Any[]): Boolean {}
Check if the given field (list) contains every element of the search list.
Equivalent to Prisma's hasEvery operator.
hasSome​
function hasSome(field: Any[], search: Any[]): Boolean {}
Check if the given field (list) contains at least one element of the search list.
Equivalent to Prisma's hasSome operator.
isEmpty​
function isEmpty(field: Any[]): Boolean {}
Check if the given field (list) is empty.
Equivalent to Prisma's isEmpty operator.
Examples​
Here're some examples on using field and model attributes:
model User {
// unique id field with a default UUID value
id String @id @default(uuid())
// require email field to be unique
email String @unique
// password is hashed with bcrypt with length of 16, omitted when returned from the CRUD services
password String @password(saltLength: 16) @omit
// default to current date-time
createdAt DateTime @default(now())
// auto-updated when the entity is modified
updatedAt DateTime @updatedAt
// mapping to a different column name in database
description String @map("desc")
// mapping to a different table name in database
@@map("users")
// use @@index to specify fields to create database index for
@@index([email])
// use @@allow to specify access policies
@@allow("create,read", true)
// use auth() to reference the current user
// use future() to access the "post-update" state
@@allow("update", auth() == this && future().email == email)
}