diff --git a/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx index f12c9cc5dd..f23e352975 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx @@ -39,8 +39,9 @@ For example: export const oneToOneHighlights = [ ["5", "hasOne", "A user has one email."], - ["10", "belongsTo", "An email belongs to a user."], - ["11", `"email"`, "The relationship's name in the `User` data model."] + ["6", `"user"`, "The relationship's name in the `Email` data model."], + ["12", "belongsTo", "An email belongs to a user."], + ["13", `"email"`, "The relationship's name in the `User` data model."] ] ```ts highlights={oneToOneHighlights} @@ -48,7 +49,9 @@ import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), }) const Email = model.define("email", { @@ -63,15 +66,68 @@ In the example above, a user has one email, and an email belongs to one user. The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. +Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. ### Optional Relationship To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](../properties/page.mdx#make-property-optional). +### One-to-One Relationship in the Database + +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) + ### One-sided One-to-One Relationship -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one. + +You can do this either from the `hasOne` or the `belongsTo` side. + +#### hasOne Side + +By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table. + +If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method: + +- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model. +- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side. + +For example: + +export const oneToOneForeignKeyHighlights = [ + ["5", "hasOne", "A user has one email."], + ["6", "foreignKey", "Add the foreign key column to the `user` table."], + ["7", "mappedBy", "Set to `undefined` since the relationship is only defined on the `User` data model."], +] + +```ts highlights={oneToOneForeignKeyHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + foreignKey: true, + mappedBy: undefined, + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), +}) +``` + +In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model. + +The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model. + + +#### belongsTo Side + +To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter. For example: @@ -94,14 +150,9 @@ const Email = model.define("email", { }) ``` -### One-to-One Relationship in the Database +In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model. -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. - -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +The `User` data model does not have a relationship property pointing to the `Email` data model. --- @@ -118,8 +169,9 @@ For example: export const oneToManyHighlights = [ ["5", "hasMany", "A store has many products."], - ["10", "belongsTo", "A product has one store."], - ["11", `"products"`, "The relationship's name in the `Store` data model."] + ["6", `"store"`, "The relationship's name in the `Product` data model."], + ["12", "belongsTo", "A product has one store."], + ["13", `"products"`, "The relationship's name in the `Store` data model."] ] ```ts highlights={oneToManyHighlights} @@ -127,7 +179,9 @@ import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) const Product = model.define("product", { @@ -165,7 +219,7 @@ For example: export const manyToManyHighlights = [ ["5", "manyToMany", "An order is associated with many products."], - ["12", "manyToMany", "A product is associated with many orders."] + ["15", "manyToMany", "A product is associated with many orders."] ] ```ts highlights={manyToManyHighlights} @@ -278,43 +332,6 @@ The `OrderProduct` model defines, aside from the ID, the following properties: --- -## Set Relationship Name in the Other Model - -The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. - -This is useful if the relationship property’s name is different from that of the associated data model. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. - -For example: - -export const relationNameHighlights = [ - ["6", `"owner"`, "The relationship's name in the `Email` data model."], - ["13", `"email"`, "The relationship's name in the `User` data model."] -] - -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - ---- - ## Cascades When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index bb9d790602..839e5ebccb 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -36,7 +36,7 @@ export const generatedEditDates = { "app/learn/fundamentals/data-models/manage-relationships/page.mdx": "2025-04-25T14:16:41.124Z", "app/learn/fundamentals/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", "app/learn/fundamentals/modules/options/page.mdx": "2025-03-18T15:12:34.510Z", - "app/learn/fundamentals/data-models/relationships/page.mdx": "2025-03-18T07:52:07.421Z", + "app/learn/fundamentals/data-models/relationships/page.mdx": "2025-07-16T09:51:22.141Z", "app/learn/fundamentals/workflows/compensation-function/page.mdx": "2025-04-24T13:16:00.941Z", "app/learn/fundamentals/modules/service-factory/page.mdx": "2025-03-18T15:14:13.486Z", "app/learn/fundamentals/modules/module-links/page.mdx": "2024-09-30T08:43:53.126Z", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 351092e5a1..34c2d0e3de 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -9131,7 +9131,9 @@ import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), }) const Email = model.define("email", { @@ -9146,15 +9148,61 @@ In the example above, a user has one email, and an email belongs to one user. The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. +Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. ### Optional Relationship To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). +### One-to-One Relationship in the Database + +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) + ### One-sided One-to-One Relationship -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one. + +You can do this either from the `hasOne` or the `belongsTo` side. + +#### hasOne Side + +By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table. + +If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method: + +- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model. +- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side. + +For example: + +```ts highlights={oneToOneForeignKeyHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + foreignKey: true, + mappedBy: undefined, + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), +}) +``` + +In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model. + +The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model. + +#### belongsTo Side + +To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter. For example: @@ -9173,14 +9221,9 @@ const Email = model.define("email", { }) ``` -### One-to-One Relationship in the Database +In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model. -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. - -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +The `User` data model does not have a relationship property pointing to the `Email` data model. *** @@ -9200,7 +9243,9 @@ import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) const Product = model.define("product", { @@ -9328,38 +9373,6 @@ The `OrderProduct` model defines, aside from the ID, the following properties: *** -## Set Relationship Name in the Other Model - -The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. - -This is useful if the relationship property’s name is different from that of the associated data model. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. - -For example: - -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - -*** - ## Cascades When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. @@ -44063,12 +44076,14 @@ Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamen The following creates a one-to-one relationship between the `User` and `Email` data models: -```ts highlights={[["5", "hasOne"], ["10", "belongsTo"]]} +```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]} import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), }) const Email = model.define("email", { @@ -44085,12 +44100,14 @@ Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamen The following creates a one-to-many relationship between the `Store` and `Product` data models: -```ts highlights={[["5", "hasMany"], ["10", "belongsTo"]]} +```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]} import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) const Product = model.define("product", { @@ -44131,13 +44148,15 @@ Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamen To configure cascade on a data model: -```ts highlights={[["7", "cascades"]]} +```ts highlights={[["10", "cascades"]]} import { model } from "@medusajs/framework/utils" -// Product import +import Product from "./product" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) .cascades({ delete: ["products"], @@ -68162,7 +68181,9 @@ export const Wishlist = model.define("wishlist", { id: model.id().primaryKey(), customer_id: model.text(), sales_channel_id: model.text(), - items: model.hasMany(() => WishlistItem), + items: model.hasMany(() => WishlistItem, { + mappedBy: "wishlist", + }), }) .indexes([ { @@ -89532,7 +89553,9 @@ export const Restaurant = model.define("restaurant", { email: model.text(), address: model.text(), image_url: model.text().nullable(), - admins: model.hasMany(() => RestaurantAdmin), + admins: model.hasMany(() => RestaurantAdmin, { + mappedBy: "restaurant", + }), }) ``` @@ -92799,7 +92822,9 @@ const Vendor = model.define("vendor", { handle: model.text().unique(), name: model.text(), logo: model.text().nullable(), - admins: model.hasMany(() => VendorAdmin), + admins: model.hasMany(() => VendorAdmin, { + mappedBy: "vendor", + }), }) export default Vendor diff --git a/www/apps/resources/app/examples/page.mdx b/www/apps/resources/app/examples/page.mdx index b7ab3b3acf..a98330ca48 100644 --- a/www/apps/resources/app/examples/page.mdx +++ b/www/apps/resources/app/examples/page.mdx @@ -1245,12 +1245,14 @@ Learn more in [this documentation](!docs!/learn/fundamentals/data-models/propert The following creates a one-to-one relationship between the `User` and `Email` data models: -```ts highlights={[["5", "hasOne"], ["10", "belongsTo"]]} +```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]} import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), }) const Email = model.define("email", { @@ -1267,12 +1269,14 @@ Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relatio The following creates a one-to-many relationship between the `Store` and `Product` data models: -```ts highlights={[["5", "hasMany"], ["10", "belongsTo"]]} +```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]} import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) const Product = model.define("product", { @@ -1313,13 +1317,15 @@ Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relatio To configure cascade on a data model: -```ts highlights={[["7", "cascades"]]} +```ts highlights={[["10", "cascades"]]} import { model } from "@medusajs/framework/utils" -// Product import +import Product from "./product" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) .cascades({ delete: ["products"], diff --git a/www/apps/resources/app/plugins/guides/wishlist/page.mdx b/www/apps/resources/app/plugins/guides/wishlist/page.mdx index eb28fc67cc..bd4d278928 100644 --- a/www/apps/resources/app/plugins/guides/wishlist/page.mdx +++ b/www/apps/resources/app/plugins/guides/wishlist/page.mdx @@ -239,7 +239,9 @@ export const Wishlist = model.define("wishlist", { id: model.id().primaryKey(), customer_id: model.text(), sales_channel_id: model.text(), - items: model.hasMany(() => WishlistItem), + items: model.hasMany(() => WishlistItem, { + mappedBy: "wishlist", + }), }) .indexes([ { diff --git a/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx b/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx index 289031c6df..404c8635db 100644 --- a/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx @@ -122,7 +122,9 @@ export const Restaurant = model.define("restaurant", { email: model.text(), address: model.text(), image_url: model.text().nullable(), - admins: model.hasMany(() => RestaurantAdmin), + admins: model.hasMany(() => RestaurantAdmin, { + mappedBy: "restaurant", + }), }) ``` diff --git a/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx b/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx index fe8be711fe..39890cd358 100644 --- a/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx @@ -135,7 +135,9 @@ const Vendor = model.define("vendor", { handle: model.text().unique(), name: model.text(), logo: model.text().nullable(), - admins: model.hasMany(() => VendorAdmin), + admins: model.hasMany(() => VendorAdmin, { + mappedBy: "vendor", + }), }) export default Vendor diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 20b9fb366c..9c1278cd36 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -114,7 +114,7 @@ export const generatedEditDates = { "app/recipes/digital-products/examples/standard/page.mdx": "2025-04-24T15:41:05.364Z", "app/recipes/digital-products/page.mdx": "2025-05-20T07:51:40.719Z", "app/recipes/ecommerce/page.mdx": "2025-06-24T08:50:10.116Z", - "app/recipes/marketplace/examples/vendors/page.mdx": "2025-07-14T13:59:32.168Z", + "app/recipes/marketplace/examples/vendors/page.mdx": "2025-07-16T09:58:59.793Z", "app/recipes/marketplace/page.mdx": "2025-05-20T07:51:40.721Z", "app/recipes/multi-region-store/page.mdx": "2025-05-20T07:51:40.721Z", "app/recipes/omnichannel/page.mdx": "2025-05-20T07:51:40.722Z", @@ -569,7 +569,7 @@ export const generatedEditDates = { "app/medusa-cli/commands/start/page.mdx": "2025-04-08T11:56:15.522Z", "app/medusa-cli/commands/telemtry/page.mdx": "2025-01-16T09:51:24.323Z", "app/medusa-cli/commands/user/page.mdx": "2024-08-28T10:44:52.489Z", - "app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-07-14T10:32:58.301Z", + "app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-07-16T09:58:31.748Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateCustomerGroup/page.mdx": "2024-12-09T13:21:33.569Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateReservation/page.mdx": "2025-04-11T09:04:47.498Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminCustomerGroup/page.mdx": "2025-05-20T07:51:41.059Z", @@ -2166,7 +2166,7 @@ export const generatedEditDates = { "app/commerce-modules/sales-channel/links-to-other-modules/page.mdx": "2025-04-17T16:00:09.483Z", "app/commerce-modules/stock-location/links-to-other-modules/page.mdx": "2025-04-17T16:02:51.467Z", "app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-04-17T16:03:16.419Z", - "app/examples/page.mdx": "2025-04-17T08:50:17.036Z", + "app/examples/page.mdx": "2025-07-16T09:53:26.163Z", "app/medusa-cli/commands/build/page.mdx": "2024-11-11T11:00:49.665Z", "app/js-sdk/page.mdx": "2025-05-26T15:08:16.590Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.apiKey/page.mdx": "2025-05-20T07:51:40.924Z", @@ -5832,7 +5832,7 @@ export const generatedEditDates = { "references/core_flows/types/core_flows.ThrowUnlessPaymentCollectionNotePaidInput/page.mdx": "2025-06-25T10:11:33.516Z", "references/core_flows/types/core_flows.ValidatePaymentsRefundStepInput/page.mdx": "2025-06-25T10:11:34.185Z", "references/core_flows/types/core_flows.ValidateRefundStepInput/page.mdx": "2025-06-25T10:11:34.177Z", - "app/plugins/guides/wishlist/page.mdx": "2025-05-20T07:51:40.717Z", + "app/plugins/guides/wishlist/page.mdx": "2025-07-16T09:54:20.345Z", "app/plugins/page.mdx": "2025-02-26T11:39:25.709Z", "app/admin-components/components/data-table/page.mdx": "2025-03-03T14:55:58.556Z", "references/order_models/variables/order_models.Order/page.mdx": "2025-07-14T09:31:48.410Z",