Skip to main content

Schema (schema.prisma)

GASsma generates client code from a Prisma-format schema file. Sheet structures, relations, default values, and more are all written in schema.prisma and reflected into a typed client by npx gassma generate.

generator client {
provider = "prisma-client-js"
output = "./src/generated/gassma"
}

model User {
id Int @id
name String
email String?
age Int
}

Prisma's schema syntax works as-is, so if you know Prisma there is almost nothing new to learn.

Where Schema Files Live

By default, .prisma files under the ./gassma directory are searched.

my-project/
├── gassma/
│ └── schema.prisma ← Write your schema here
├── gassma.config.ts
├── package.json
└── ...

The search location can be changed with the --schema option or the schema setting in gassma.config.ts (see Config File).

The generator Block

Specify the output directory in the generator block's output field. output is required.

generator client {
provider = "prisma-client-js"
output = "./src/generated/gassma"
}

previewFeatures

You can enable opt-in features by specifying previewFeatures in the generator block (same syntax as Prisma's previewFeatures).

generator client {
provider = "prisma-client-js"
output = "./src/generated/gassma"
previewFeatures = ["strictUndefinedChecks"]
}

Currently supported features:

FeatureDescriptionReference
strictUndefinedChecksTurns explicit undefined in query inputs into runtime errorsstrictUndefinedChecks / Gassma.skip

When enabled, strictUndefinedChecks: true is embedded into the generated client JS, and the generated type definitions also accept Gassma.skip.

Type Mapping

Prisma types are converted to the following TypeScript types:

Prisma TypeTypeScript Type
Intnumber
Floatnumber
Decimalnumber
BigIntnumber
Stringstring
Booleanboolean
DateTimeDate
Jsonstring
Bytesstring

Adding ? makes the field optional (null is allowed).

Relations

Using Prisma's @relation attribute, relation information is automatically extracted and injected into the generated client.

generator client {
provider = "prisma-client-js"
output = "./src/generated/gassma"
}

model User {
id Int @id
name String
posts Post[]
}

model Post {
id Int @id
title String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
}

The following relation settings are auto-generated from the above definition:

  • User.posts: oneToMany (User -> Post, onDelete: Cascade)
  • Post.author: manyToOne (Post -> User)
note

As in Prisma, you write onDelete / onUpdate on the side that holds the FK (the side with @relation), but in the generated relation config they land on the referenced side (oneToMany, or the non-FK side of a oneToOne). That is the side on which GASsma fires referential actions (onDelete).

For what each relation type means and how to use them with include / where, see Relation Definitions. Referential actions are covered in onDelete / onUpdate.

Implicit Many-to-Many

When bidirectional array references exist, an implicit Many-to-Many relation is automatically detected.

model Post {
id Int @id
tags Tag[]
}

model Tag {
id Int @id
name String
posts Post[]
}

In the generated client, the junction table name is automatically resolved as _PostToTag (model names in alphabetical order). If you name the relation like @relation("PostTags"), the relation name is used as the junction table name (_PostTags) — the same rule as Prisma.

note

The junction table (sheet) itself can be created automatically with migrate / db push (you can also prepare a sheet with the same name manually in the spreadsheet). If you want to change the junction table name, give the relation a name with @relation.

enum

Literal union types are auto-generated from Prisma's enum definitions.

enum Role {
ADMIN
USER
MODERATOR
}

model User {
id Int @id
role Role
}

Generated type:

"role": "ADMIN" | "USER" | "MODERATOR"

enum @map

Adding @map to enum members allows you to map between the name used in code and the value stored in the spreadsheet.

enum Role {
admin @map("ADMIN")
user @map("USER")
moderator @map("MODERATOR")
}

Generated constant:

const Role = {
admin: "ADMIN",
user: "USER",
moderator: "MODERATOR",
} as const;

The @map values are used in the type definition:

"role": "ADMIN" | "USER" | "MODERATOR"

Attributes

For details on the feature each attribute drives, see the linked page.

AttributeEffectReference
@idPrimary key-
@relation(...)Relation definitionRelation Definitions
@default(...)Default value on createdefaults
@default(autoincrement())Auto-incrementautoincrement
@updatedAtAuto-set timestamps on create/updateupdatedAt
@ignoreExclude a field from all operationsignore
@@ignoreExclude an entire sheetignore
@map("name")Map a field name to a spreadsheet headermap
@@map("name")Map a model name to a sheet namemap
@unique / @@uniqueNo effect in GASsma (Prisma uses them to validate relations)@unique / @@unique

For attributes that are not listed here, see Unsupported Attributes.

@default

Fields with @default() become optional (?) in the generated Create input type.

model User {
id Int @id @default(autoincrement())
name String
isActive Boolean @default(true)
createdAt DateTime @default(now())
}

Generated type:

"isActive"?: boolean   // @default(true) -> Optional
"createdAt"?: Date // @default(now()) -> Optional

The defaults settings are automatically embedded in the generated client JS.

@default()Generated JS
@default(true) / @default(false)true / false
@default(0) (number)0
@default("USER") (string)"USER"
@default(ADMIN) (enum value)"ADMIN"
@default(active) (enum value with active @map("ACTIVE"))"ACTIVE" (the @map-applied value)
@default(now())() => new Date()
@default(uuid())() => Utilities.getUuid()
@default(autoincrement())Generated separately as an autoincrement setting

@updatedAt

Fields with @updatedAt become optional in the Create input type, and the updatedAt setting is embedded in the generated client JS.

model Post {
id Int @id
title String
updatedAt DateTime @updatedAt
}

@ignore

Fields with @ignore are completely excluded from the type definition, and the ignore setting is embedded in the generated client JS.

model User {
id Int @id
name String
secret String @ignore // Not included in the type definition
}

@map

@map("name") defines a field name mapping. The map setting is embedded in the generated client JS.

model User {
id Int @id
firstName String @map("名前")
lastName String @map("名字")
}

In code, you work with firstName / lastName, which correspond to the columns named "名前" and "名字" in the spreadsheet.

@@ignore

The model-level @@ignore excludes an entire sheet. The ignoreSheets setting is embedded in the generated client JS.

model Logs {
id Int @id
message String

@@ignore
}

@@map

The model-level @@map("name") maps a sheet name.

model Users {
id Int @id
name String

@@map("ユーザー一覧")
}

In code, you access it as Users, which corresponds to the sheet named "ユーザー一覧" in the spreadsheet.

@unique / @@unique

You can write @unique / @@unique, but GASsma does not enforce uniqueness. Writing a duplicate value raises no error, so check for duplicates in your own code when you need to reject them.

They are still accepted because Prisma uses them to validate the shape of a relation. In the four cases below, Prisma rejects the schema without them.

CaseRequired
One-to-one with a single foreign key@unique on the foreign key field
One-to-many that references a field other than @id@unique on the referenced field
One-to-many with a composite foreign key@@unique([k1, k2]) on the referenced model
One-to-one with a composite foreign key@@unique([r1, r2]) on the model holding the foreign key
model User {
id Int @id
profile Profile?
}

model Profile {
id Int @id
user User @relation(fields: [userId], references: [id])
userId Int @unique // A one-to-one needs @unique
}
note

The composite foreign keys required by the last two cases cannot be used in GASsma yet (see Composite Foreign Keys).

Unsupported Attributes

The attributes below make npx gassma generate / npx gassma validate fail with a GASsmaUnsupportedAttributeError.

AttributeWhy it is rejected
@@idA composite primary key cannot be declared on a spreadsheet. Prisma never requires @@id -- a single @id always covers it -- and GASsma's implicit Many-to-Many assumes the join columns are named id, so a composite primary key would point at a column that does not exist.
@@index / @@fulltextAn index cannot be created on a spreadsheet. Every query reads the whole sheet either way, so it has no effect.
Native types such as @db.VarChar(255)GASsma cannot control how a spreadsheet stores a value, so the native type has no effect.
model User {
id Int @id
name String @db.VarChar(255) // Error

@@index([name]) // Error
}

Violations are reported grouped by attribute.

GASsmaUnsupportedAttributeError: `@db.VarChar` on User.name is not supported.
GASsma cannot control how a spreadsheet stores a value, so the native type has no effect.
Remove it.

`@@index` on User (name) is not supported.
GASsma cannot create an index on a spreadsheet.
Remove it; every query reads the whole sheet either way.

Composite Foreign Keys

A composite foreign key -- fields / references on @relation listing more than one column -- cannot be used yet (support is planned). It makes npx gassma generate / npx gassma validate fail with a GASsmaCompositeRelationError.

model A {
k1 Int
k2 Int
b B[]

@@unique([k1, k2])
}

model B {
id Int @id
r1 Int
r2 Int
a A @relation(fields: [r1, r2], references: [k1, k2]) // Error
}
GASsmaCompositeRelationError: `@relation` over more than one column is not supported yet.
- B.a (fields: [r1, r2], references: [k1, k2])
GASsma matches a relation on a single column for now, so the columns after the first are dropped and rows that agree on the first column alone would match.
Please narrow the relation to one column until composite keys are supported.

Extending Types

@gassma.addType

By writing @gassma.addType in a Prisma field comment (///), you can add union types to the field's type.

model User {
/// @gassma.addType string
id Int @id // Generated type: number | string

/// @gassma.addType string, boolean
score Int // Generated type: number | string | boolean

name String // Generated type: string (normal when no comment)
}

@gassma.replaceType

While @gassma.addType creates a union with the base type, @gassma.replaceType replaces the base type and generates only the specified types.

model User {
/// @gassma.replaceType "admin", "user", "moderator"
role String
}

Generated type:

"role": "admin" | "user" | "moderator"  // Does not include string
note

Priority: enum > replaceType > addType. When an enum exists, replaceType / addType are ignored.

The datasource Block

You can specify the target spreadsheet by writing a datasource block in the schema file.

datasource db {
provider = "google-spreadsheet"
url = "https://docs.google.com/spreadsheets/d/XXXXX/edit"
}

url can also be specified as datasource.url in gassma.config.ts. The datasource block in the schema takes precedence (see Config File).

Multi-file Schema

When you place multiple .prisma files in the same directory (and subdirectories), they are automatically merged into a single schema. This is equivalent to Prisma's Multi-file schema feature.

gassma/
├── schema.prisma ← Write the generator block here
├── models/
│ ├── user.prisma ← User, Profile models
│ └── post.prisma ← Post, Comment models

The generator block only needs to be written in one file and is shared across all files. All models are consolidated into a single client output.

Multiple Schemas (Multiple Spreadsheets)

When working with different spreadsheets, separate schemas into different directories and generate them individually. Type names are prefixed with the schema name, so there are no conflicts even with models of the same name.

schemas/
├── user/
│ └── schema.prisma → userClient.js, user.d.ts
└── order/
└── schema.prisma → orderClient.js, order.d.ts
import { GassmaClient as UserClient } from "./generated/user/schemaClient";
import { GassmaClient as OrderClient } from "./generated/order/schemaClient";

const userGassma = new UserClient();
const orderGassma = new OrderClient();