zenstackhq / zenstack

Fullstack TypeScript toolkit that enhances Prisma ORM with flexible Authorization layer for RBAC/ABAC/PBAC/ReBAC, offering auto-generated type-safe APIs and frontend hooks.
https://zenstack.dev
MIT License
2.07k stars 88 forks source link

`zenstack generate` JavaScript heap out of memory (v2 alpha) #1064

Closed andrewkucz closed 7 months ago

andrewkucz commented 7 months ago

Description and expected behavior

Hi again, I have continued playing with the v2 alpha and may have run into an accidental stress test causing a crash due to heap memory.

I will post my zschema below. It is perhaps too big or too complex with all the relations / polymorphic relationships and may be an edge case, but I just wanted to bring this to your attention.

Prisma and zod schemas generate successfully but the PrismaClient enhancer does not. It runs for a few minutes and then crashes with the output shown. I have tried removing various combinations of models and I could get it to work sometimes, but with this schema, I was able to reproduce a crash each time.

Output:

> zenstack generate

⌛️ ZenStack CLI v2.0.0-alpha.1, running plugins
✔ Generating Prisma schema
✔ Generating Zod schemas
⠙ Generating PrismaClient enhancer
<--- Last few GCs --->

[33877:0x140008000]   298372 ms: Mark-Compact (reduce) 4082.3 (4107.0) -> 4082.3 (4104.0) MB, 41.79 / 0.00 ms  (average mu = 0.128, current mu = 0.000) last resort; GC in old space requested
[33877:0x140008000]   298416 ms: Mark-Compact (reduce) 4082.3 (4104.0) -> 4082.3 (4104.0) MB, 43.46 / 0.00 ms  (average mu = 0.067, current mu = 0.001) last resort; GC in old space requested

<--- JS stacktrace --->

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 0x10491553c node::Abort() [/Users/andrew/.nvm/versions/node/v20.11.0/bin/node]
 2: 0x10491573c node::ModifyCodeGenerationFromStrings(v8::Local<v8::Context>, v8::Local<v8::Value>, bool) [/Users/andrew/.nvm/versions/node/v20.11.0/bin/node]
 3: 0x104a9a6c4 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/Users/andrew/.nvm/versions/node/v20.11.0/bin/node]
 4: 0x104c65194 v8::internal::MemoryController<v8::internal::V8HeapTrait>::MinimumAllocationLimitGrowingStep(v8::internal::Heap::HeapGrowingMode) [/Users/andrew/.nvm/versions/node/v20.11.0/bin/node]
 5: 0x104c49240 v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/Users/andrew/.nvm/versions/node/v20.11.0/bin/node]
datasource db {
    provider = 'sqlite'
    url = 'file:./dev.db'
}

generator client {
    provider = "prisma-client-js"
}

model Account {
    id                String  @id @default(cuid())
    userId            String
    type              String
    provider          String
    providerAccountId String
    refresh_token     String? // @db.Text
    access_token      String? // @db.Text
    expires_at        Int?
    token_type        String?
    scope             String?
    id_token          String? // @db.Text
    session_state     String?
    user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
    @@allow('all', auth().id == userId)
    @@unique([provider, providerAccountId])
}

model Session {
    id           String   @id @default(cuid())
    sessionToken String   @unique
    userId       String
    expires      DateTime
    user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
    @@allow('all', auth().id == userId)
}

model VerificationToken {
    identifier String
    token      String   @unique
    expires    DateTime

    @@allow('all', true)
    @@unique([identifier, token])
}

model User {
    id            String    @id @default(cuid())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String
    accounts      Account[]
    sessions      Session[]

    username    String    @unique @length(min: 4, max: 20)
    about       String?   @length(max: 500)
    location    String?   @length(max: 100)

    role        String @default("USER") @deny(operation: "update", auth().role != "ADMIN")

    inserted_at DateTime   @default(now())
    updated_at  DateTime   @updatedAt() @default(now())

    editComments  EditComment[]

    posts       Post[]
    rankings                UserRanking[]
    ratings                 UserRating[]
    favorites               UserFavorite[]

    people        Person[]
    studios       Studio[]
    edits         Edit[]
    attachments Attachment[]
    galleries Gallery[]

    uploads UserUpload[]

    maxUploadsPerDay Int @default(10)
    maxEditsPerDay Int @default(10)

    // everyone can signup, and user profile is also publicly readable
    @@allow('create,read', true)
    // only the user can update or delete their own profile
    @@allow('update,delete', auth() == this)
}

abstract model UserEntityRelation {
  entityId      String?
  entity        Entity?        @relation(fields: [entityId], references: [id], onUpdate: NoAction)
  userId   String
  user     User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)

  // everyone can read
  @@allow('read', true)
  @@allow('create,update,delete', auth().id == this.userId)

  @@unique([userId,entityId])
}

model UserUpload {

    timestamp DateTime @default(now())

    key String @id
    url String @unique
    size Int

    userId String  
    user   User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)

    @@allow('create', auth().id == userId)
    @@allow('all', auth().role == "ADMIN")
}

model Post {
  id      Int    @id @default(autoincrement())
  title    String @length(max: 100)
  body     String @length(max: 1000)
  createdAt DateTime @default(now())

  authorId String  
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction)

  @@allow('read', true)
  @@allow('create,update,delete', auth().id == authorId && auth().role == "ADMIN")
}

model Edit extends UserEntityRelation {

  id      String          @id @default(cuid())
  status String @default("PENDING") @allow('update', auth().role in ["ADMIN", "MODERATOR"])
  type    String  @allow('update', false)
  timestamp DateTime   @default(now())
  note  String? @length(max: 300)
  // for creates - createPayload & updates - data before diff is applied
  data String?
  // for updates
  diff String?

  comments EditComment[]

}

model EditComment {
  id      Int          @id @default(autoincrement())
  timestamp DateTime @default(now())
  content  String  @length(max: 300)
  editId  String
  edit Edit @relation(fields: [editId], references: [id], onUpdate: Cascade)
  authorId  String
  author    User @relation(fields: [authorId], references: [id], onUpdate: Cascade)

  // everyone can read
  @@allow('read', true)
  @@allow('create,update,delete', auth().id == this.authorId || auth().role in ["ADMIN", "MODERATOR"])
}

model MetadataIdentifier {

  id  Int @default(autoincrement()) @id

  identifier String

  metadataSource String
  MetadataSource MetadataSource @relation(fields: [metadataSource], references: [slug], onUpdate: Cascade)

  entities Entity[]

  @@unique([identifier, metadataSource])

  @@allow('read', true)
  @@allow('create,update,delete', auth().role in ["ADMIN", "MODERATOR"])

}
model MetadataSource {
  slug String @id
  name String @unique
  identifierRegex String
  desc   String?
  url    String
  icon   String
  identifiers MetadataIdentifier[]

  @@allow('all', auth().role == "ADMIN")
}

model Attachment extends UserEntityRelation {
  id            String         @id @default(cuid())
  createdAt     DateTime   @default(now())
  key           String         @unique
  url           String          @unique
  galleries     Gallery[]
  @@allow('delete', auth().role in ["ADMIN", "MODERATOR"])
}

model Entity {

  id      String          @id @default(cuid()) 
  name    String
  desc  String?          

  attachments Attachment[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt @default(now())

  type String

  status  String @default("PENDING") // PENDING ON INITIAL CREATION
  verified Boolean @default(false)

  edits Edit[]  
  userRankings UserRanking[]
  userFavorites UserFavorite[]
  userRatings UserRating[]
  metaIdentifiers MetadataIdentifier[]

  @@delegate(type)

  @@allow('read', true)
  @@allow('create', auth() != null)
  @@allow('update', auth().role in ["ADMIN", "MODERATOR"])
  @@allow('delete', auth().role == "ADMIN")
}

model Person extends Entity {
  studios Studio[]
  owners    User[]
  clips     Clip[]
  events Event[]
  galleries Gallery[]
}

model Studio extends Entity {
  people Person[]
  owners User[]
  clips     Clip[]
  events Event[]
  galleries Gallery[]
}

model Clip extends Entity {
  url   String?
  people Person[]
  studios Studio[]
  galleries Gallery[]
}

model UserRanking extends UserEntityRelation {
  id      String       @id @default(cuid()) 
  rank     Int  @gte(1) @lte(100)
  note     String? @length(max: 300)
}

model UserFavorite extends UserEntityRelation {
  id      String       @id @default(cuid()) 
  favoritedAt DateTime @default(now())
}

model UserRating  extends UserEntityRelation  {
  id      String @id @default(cuid()) 
  rating    Int @gte(1) @lte(5)
  note     String?  @length(max: 500)  
  ratedAt DateTime @default(now())
}

model Event {
  id      Int       @id @default(autoincrement()) 
  name    String  @length(max: 100)  
  desc    String?  @length(max: 500)  
  location String?  @length(max: 100) 
  date     DateTime?
  people   Person[]
  studios  Studio[]

  @@allow('read', true)
  @@allow('create,update,delete', auth().role == "ADMIN")
}

model Gallery {
  id      String       @id @default(cuid()) 
  studioId String?  
  personId String?  
  timestamp DateTime  @default(now())
  authorId   String   
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction)
  people    Person[]
  studios   Studio[]
  clips     Clip[]
  attachments Attachment[]

  @@allow('read', true)
  @@allow('create,update,delete', auth().id == this.authorId && auth().role == "ADMIN")
}

Environment (please complete the following information):

ymc9 commented 7 months ago

Thanks for reporting this @andrewkucz , I'll debug and see what's going wrong.

ymc9 commented 7 months ago

Fixed in 2.0.0-alpha.6