3

Let's say I have two objects: Product and Seller

Products can have multiple Sellers. A single Seller can sell multiple Products.

The goal is to write a seeding script that successfully seeds my MongoDB database such that Keystone.js's CMS recognizes the many-to-many relationship.

Schemas

Product.ts

import { text, relationship } from "@keystone-next/fields";
import { list } from "@keystone-next/keystone/schema";

export const Product = list({
  fields: {
    name: text({ isRequired: true }),
    sellers: relationship({
      ref: "Seller.products",
      many: true,
    }),
  },
});

Seller.ts

import { text, relationship } from "@keystone-next/fields";
import { list } from "@keystone-next/keystone/schema";

export const Product = list({
  fields: {
    name: text({ isRequired: true }),
    products: relationship({
      ref: "Product.sellers",
      many: true,
    }),
  },
});

KeystoneJS config

My keystone.ts config, shortened for brevity, looks like this:

import { insertSeedData } from "./seed-data"
...
db: {
  adapter: "mongoose",
  url: databaseURL,
  async onConnect(keystone) {
    console.log("Connected to the database!");
    if (process.argv.includes("--seed-data")) {
      await insertSeedData(keystone);
    }
  },
},
lists: createSchema({
  Product,
  Seller,
}),
...

Seeding Scripts (these are the files I expect to change)

I have a script that populates the database (seed-data/index.ts):

import { products } from "./data";
import { sellers } from "./data";

export async function insertSeedData(ks: any) {

  // setup code
  const keystone = ks.keystone || ks;
  const adapter = keystone.adapters?.MongooseAdapter || keystone.adapter;
  const { mongoose } = adapter;
  mongoose.set("debug", true);

  // adding products to DB
  for (const product of products) {
    await mongoose.model("Product").create(product);
  }

  // adding sellers to DB
  for (const seller of sellers) {
    await mongoose.model("Seller").create(seller);
  }
}

And finally, data.ts looks something like this:

export const products = [
  {
    name: "apple",
    sellers: ["Joe", "Anne", "Duke", "Alicia"],
  },
  {
    name: "orange",
    sellers: ["Duke", "Alicia"],
  },
  ...
];
export const sellers = [
  {
    name: "Joe",
    products: ["apple", "banana"],
  },
  {
    name: "Duke",
    products: ["apple", "orange", "banana"],
  },
  ...
];

The above setup does not work for a variety of reasons. The most obvious is that the sellers and products attributes of the Product and Seller objects (respectively) should reference objects (ObjectId) and not names (e.g. "apple", "Joe").

I'll post a few attempts below that I thought would work, but did not:

Attempt 1

I figured I'd just give them temporary ids (the id attribute in data.ts below) and then, once MongoDB assigns an ObjectId, I'll use those.

seed-data/index.ts

...
  const productIdsMapping = [];
...
  // adding products to DB
  for (const product of products) {
    const productToPutInMongoDB = { name: product.name };
    const { _id } = await mongoose.model("Product").create(productToPutInMongoDB);
    productIdsMapping.push(_id);
  }

  // adding sellers to DB (using product IDs created by MongoDB)
  for (const seller of sellers) {
    const productMongoDBIds = [];
    for (const productSeedId of seller.products) {
      productMongoDBIds.push(productIdsMapping[productSeedId]);
    const sellerToPutInMongoDB = { name: seller.name, products: productMongoDBIds };
    await mongoose.model("Seller").create(sellerToPutInMongoDB);
  }
...

data.ts

export const products = [
  {
    id: 0,
    name: "apple",
    sellers: [0, 1, 2, 3],
  },
  {
    id: 1,
    name: "orange",
    sellers: [2, 3],
  },
  ...
];
export const sellers = [
  {
    id: 0
    name: "Joe",
    products: [0, 2],
  },
  ...
  {
    id: 2
    name: "Duke",
    products: [0, 1, 2],
  },
  ...
];

Output (attempt 1):

It just doesn't seem to care about or acknowledge the products attribute.

Mongoose: sellers.insertOne({ _id: ObjectId("$ID"), name: 'Joe', __v: 0}, { session: null })
{
  results: {
    _id: $ID,
    name: 'Joe',
    __v: 0
  }
}

Attempt 2

I figured maybe I just didn't format it correctly, for some reason, so maybe if I queried the products and shoved them directly into the seller object, that would work.

seed-data/index.ts

...
  const productIdsMapping = [];
...
  // adding products to DB
  for (const product of products) {
    const productToPutInMongoDB = { name: product.name };
    const { _id } = await mongoose.model("Product").create(productToPutInMongoDB);
    productIdsMapping.push(_id);
  }

  // adding sellers to DB (using product IDs created by MongoDB)
  for (const seller of sellers) {
    const productMongoDBIds = [];
    for (const productSeedId of seller.products) {
      productMongoDBIds.push(productIdsMapping[productSeedId]);
    }
    const sellerToPutInMongoDB = { name: seller.name };
    const { _id } = await mongoose.model("Seller").create(sellerToPutInMongoDB);
    const resultsToBeConsoleLogged = await mongoose.model("Seller").findByIdAndUpdate(
      _id,
      {
        $push: {
          products: productMongoDBIds,
        },
      },
      { new: true, useFindAndModify: false, upsert: true }
    );
  }
...

data.ts

Same data.ts file as attempt 1.

Output (attempt 2):

Same thing. No luck on the products attribute appearing.

Mongoose: sellers.insertOne({ _id: ObjectId("$ID"), name: 'Joe', __v: 0}, { session: null })
{
  results: {
    _id: $ID,
    name: 'Joe',
    __v: 0
  }
}

So, now I'm stuck. I figured attempt 1 would Just Work™ like this answer:

https://stackoverflow.com/a/52965025

Any thoughts?

Molomby
  • 5,859
  • 2
  • 34
  • 27

1 Answers1

2

I figured out a solution. Here's the background:

When I define the schema, Keystone creates corresponding MongoDB collections. If there is a many-to-many relationship between object A and object B, Keystone will create 3 collections: A, B, and A_relationshipToB_B_relationshipToA.

That 3rd collection is the interface between the two. It's just a collection with pairs of ids from A and B.

Hence, in order to seed my database with a many-to-many relationship that shows up in the Keystone CMS, I have to seed not only A and B, but also the 3rd collection: A_relationshipToB_B_relationshipToA.

Hence, seed-data/index.ts will have some code that inserts into that table:

...
for (const seller of sellers) {
    const sellerToAdd = { name: seller.name };
    const { _id } = await mongoose.model("Seller").create(sellerToAdd);

    // Product_sellers_Seller_products Insertion
    for (const productId of seller.products) {
      await mongoose
        .model("Product_sellers_Seller_products")
        .create({
          Product_left_id: productIds[productId], // (data.ts id) --> (Mongo ID)
          Seller_right_id: _id,
        });
    }
  }
...