prisma-util
prisma-util copied to clipboard
Support Multiple Inheritance
Problem
I have the following config.mjs
:
export default {
optionalFeatures: ["crossFileRelations"],
baseSchema: BaseSchema,
includeFiles: [...globModels(BaseSchema, "prisma/models/**/*.prisma")],
extended: {
"user.prisma:User": "vendor.prisma:Vendor"
},
}
I would like to be able to extend multiple models, as such:
extended: {
"user.prisma:User": ["vendor.prisma:Vendor", "vendor.prisma:Customer", ...]
},
This is currently not possible.
Suggested solution
The extended
config parameter should support a string or list of strings as the type, such that a model can extend multiple other models.
Alternatives
Honestly, the current DX for defining cross-file relations and extensions both are quite bad. This package should move long-term to support the following syntax in .prisma files:
# import './Project.prisma'
model User extends Vendor, Customer {
....
projects Project[]
}
The import statement can be completed by plugging in the graphql-import
NPM package or a similar alternative.
Additional context
This package is much needed, great job building it. There are multiple issues open on Prisma's Github for multiple years that this package resolves!
Hello @notadamking!
Thank you for this suggestion. I'm happy to tell you that this will be implemented in my Rust rewrite of Prisma Util, Turboprisma. I plan on deprecating Prisma Util when it has been fully replaced by Turboprisma, complete with a VSCode extension for auto-completion.
This is the syntax I had planned:
abstract model Dates {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Test extends Dates {
id Int @id @default(autoincrement())
}
// Results in:
model Test {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Turboprisma
Amazing! Please update this thread as the feature becomes available in Turboprisma, I will use it immediately.
In addition, I really think supporting cross-file imports, as opposed to defining relations in the config and requiring duplicate shims for each relation in each model file, would go a LONG way to improve the DX.
Of course, that is also planned, including the ability to define custom attribute and validators plus real-time database listeners and refined types. I'm also getting ready for the next release, which will bring the migration engine with fully type-safe data migrations written in TypeScript.
Of course, that is also planned, including the ability to define custom attribute and validators plus real-time database listeners and refined types. I'm also getting ready for the next release, which will bring the migration engine with fully type-safe data migrations written in TypeScript.
This is great to hear. Any chances there is a test build I could play around with?
Otherwise, if a turboprisma build is not on the near horizon, would it be simple to make the change as first described (support a string of list extensions) to this package? I have not dived deep into the codebase yet to see how difficult this would be.
Well, the migration part should be released in about ~4-5 days. After that, I estimate about a week or so for developing model inheritance. I think that it would be better to wait a bit for the Turboprisma release since it will be much better for the DX.
But even then, if it takes more than a week, I will make the change in this repository and notify you. How does that sound?
Test builds will also be available once the new Turboprisma Version Manager is released, so I can enable telemetry for those releases for feedback purposes.
Well, the migration part should be released in about ~4-5 days. After that, I estimate about a week or so for developing model inheritance. I think that it would be better to wait a bit for the Turboprisma release since it will be much better for the DX.
But even then, if it takes more than a week, I will make the change in this repository and notify you. How does that sound?
Perfect, you're awesome!
Well, the migration part should be released in about ~4-5 days. After that, I estimate about a week or so for developing model inheritance. I think that it would be better to wait a bit for the Turboprisma release since it will be much better for the DX.
But even then, if it takes more than a week, I will make the change in this repository and notify you. How does that sound?
bump :)
Hi @notadamking!
I apologize for the delay. We've had some issues with the type generation for Data Migrations and had to rewrite our implementation. But now it works and is 80% done.
Hi @notadamking!
I apologize for the delay. We've had some issues with the type generation for Data Migrations and had to rewrite our implementation. But now it works and is 80% done.
Great to hear! Do you have an updated ETA? Still very eager to use this.
I am currently using Zenstack, after testing pretty much every similar package I could find. It works good, but doesn't fully solve inheritance (abstract models can't define relations, extending models can't override optional parameters to be required, models can't use an abstract model as a field in another model, nested inheritance is buggy, etc.)
Though one thing that Zenstack solves, which would be great to see in turboprisma, is access control. Unfortunately Zenstack's access control doesn't support roles and permissions very well, and doesn't doesn't support field-level access control (i.e. only letting a user see their own phone number, while users in their organization can see their name/email).
Hi again @notadamking!
The model inheritance issues you've mentioned will definitely not be a problem in Turboprisma. Due to us interfacing with the schema at engine level (something that Zenstack doesn't do), we can offer more granular control. Our planned feature set for model inheritance includes the ability to override any field definition, have nested inheritance with ease just like you would in something like Java and the ability to use inheritance at database level for Postgres.
As for the relation part on abstract models, could you provide an example of how that would look like?
abstract model ProjectOwner extends Vendor, Customer {
...
scaffolds Scaffold[] @relation("ProjectOwnerScaffolds")
permissions Permission[] @relation("ProjectOwnerPermissions")
roles Role[] @relation("ProjectOwnerRoles")
}
in this case, Scaffold
is an abstract model as well, but it's a relation that will exist on all ProjectOwner
s.
How would this be represented in the database?
How would this be represented in the database?
I think the simplest way to represent this in a database would be to add the inherited class as a foreign key to the table of the extending model, then at runtime use joins to present the data from the database in the format expected in the schema.
CREATE TABLE project_owner (
id SERIAL PRIMARY KEY,
extends_vendor_id INTEGER REFERENCES extends_vendor(id),
extends_customer_id INTEGER REFERENCES extends_customer(id),
... -- other project_owner fields
);
CREATE TABLE user (
id SERIAL PRIMARY KEY,
extends_project_owner_id INTEGER REFERENCES extends_project_owner(id),
... -- other user fields
);
where in this example, User
extends ProjectOwner
, which extends Vendor
and Customer
.
PostgreSQL also supports a form of table inheritance natively, but I'm not entirely sure it works the same as the above description using SQL Join statements and separate tables.
https://www.linuxtopia.org/online_books/database_guides/Practical_PostgreSQL_database/PostgreSQL_x13546_002.htm
How would this be represented in the database?
I think the simplest way to represent this in a database would be to add the inherited class as a foreign key to the table of the extending model, then at runtime use joins to present the data from the database in the format expected in the schema.
CREATE TABLE project_owner ( id SERIAL PRIMARY KEY, extends_vendor_id INTEGER REFERENCES extends_vendor(id), extends_customer_id INTEGER REFERENCES extends_customer(id), ... -- other project_owner fields ); CREATE TABLE user ( id SERIAL PRIMARY KEY, extends_project_owner_id INTEGER REFERENCES extends_project_owner(id), ... -- other user fields );
where in this example,
User
extendsProjectOwner
, which extendsVendor
andCustomer
.
The problem is, if the referenced field has an abstract model as the type, it would be a bit harder to work with, leading to some other questions like what model should take precedence? Should it accept any model that inherits from that abstract model? Should it only accept one table for the relation?
How would this be represented in the database?
I think the simplest way to represent this in a database would be to add the inherited class as a foreign key to the table of the extending model, then at runtime use joins to present the data from the database in the format expected in the schema.
CREATE TABLE project_owner ( id SERIAL PRIMARY KEY, extends_vendor_id INTEGER REFERENCES extends_vendor(id), extends_customer_id INTEGER REFERENCES extends_customer(id), ... -- other project_owner fields ); CREATE TABLE user ( id SERIAL PRIMARY KEY, extends_project_owner_id INTEGER REFERENCES extends_project_owner(id), ... -- other user fields );
where in this example,
User
extendsProjectOwner
, which extendsVendor
andCustomer
.The problem is, if the referenced field has an abstract model as the type, it would be a bit harder to work with, leading to some other questions like what model should take precedence? Should it accept any model that inherits from that abstract model? Should it only accept one table for the relation?
I believe the following schema accurately represents what you are referring to:
abstract model ProjectOwner extends Vendor, Customer {
...
}
model User extends ProjectOwner {
...
}
model Organization extends ProjectOwner {
...
}
model Project {
...
owner: ProjectOwner
}
which would be represented in the database as follows:
CREATE TYPE project_owner_type AS ENUM ('user', 'organization');
CREATE TYPE user_type AS ENUM ('user');
CREATE TYPE organization_type AS ENUM ('organization');
CREATE TYPE project_type AS ENUM ('project');
CREATE TABLE project_owner (
id SERIAL PRIMARY KEY,
type project_owner_type,
extends_vendor_id INTEGER REFERENCES extends_vendor(id),
extends_customer_id INTEGER REFERENCES extends_customer(id),
... -- other project_owner fields
);
CREATE TABLE user (
id SERIAL PRIMARY KEY,
type user_type,
extends_project_owner_id INTEGER REFERENCES extends_project_owner(id),
... -- other user fields
);
CREATE TABLE organization (
id SERIAL PRIMARY KEY,
type organization_type,
extends_project_owner_id INTEGER REFERENCES extends_project_owner(id),
... -- other user fields
);
CREATE TABLE project (
id SERIAL PRIMARY KEY,
type project_type,
project_owner_id INTEGER REFERENCES project_owner(id),
... -- other user fields
);
In which case I believe the simplest solution is to store abstract relationships as simply a foreign key id (as above) referring to the top-level class (i.e. a User or Organization), and do the interpolation back to full class instance with joins. This would also require adding a type
field to every table (or at least every abstract table), such that if the model is later extended, the extended type can be recovered.
Then the answers to your questions becomes trivial:
-
Which model should take precedence? The top level model should always be restored, the defined model in the schema should always be the foreign relationship stored in the database.
-
Should it accept any model that inherits from that abstract model? Yes, because you would only store the reference to the abstract class in the database field, then restore back to the full class at runtime using joins using the
type
field on the abstract model. -
Should it only accept one table for the relation? Yes. Both in the Prisma schema and in the database, the relation key would be referring to the abstract (or extended) model.
Then I know you may be thinking, what about nested relations? In the above model, they just work. For example, if we added a new model in the schema:
model Admin extends User {
...
}
then we would simply update the database with the following:
CREATE TYPE user_type AS ENUM ('user', 'admin');
CREATE TYPE admin_type AS ENUM ('admin');
CREATE TABLE admin (
id SERIAL PRIMARY KEY,
type admin_type,
extends_user_id INTEGER REFERENCES user(id),
... -- other user fields
);
And so long as the join statements or model interpolation is correct, everything should be set up as expected and follow traditional object orientation, I believe.