Продолжая последний пост, я собираюсь написать о заполнении поддокумента в GraphQL.

Контекст

Итак, в прошлый раз я построил две схемы, а затем вложил одну из них в массив другой. Чтобы дать вам немного больше о контексте этого программного обеспечения, это программное обеспечение POS, в котором есть несколько продуктов, и каждый платеж имеет набор продуктов, оплаченных клиентом.

// models/Transaction.js
import mongoose from 'mongoose';

const Schema = mongoose.Schema;

const itemSchema = new Schema({
name: { type: String },
amount: { type: Number },
});

const transactionSchema = new Schema(
{
price: { type: Number },
method: { type: String, default: 'VISA' },
cardNumber: { type: String },
paidTime: { type: Date, default: new Date() },
items: [itemSchema], // we will change this part
}
);

export const Transaction = mongoose.model('Transaction', transactionSchema);

На этот раз я бы построил совершенно новую модель и ссылку на нее, которая будет действовать как внешний ключ.

Обзор

Что я собираюсь сделать, так это сохранить идентификаторы в массиве, а не хранить весь поддокумент. От чего-то вроде левого к правому.

  1. Создайте новую модель, Продукт
// models/Product.js
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const productSchema = new Schema({
  name: { type: String },
  price: { type: Number },
  category: { type: String },
});
export const Product = mongoose.model('Product', productSchema);
  1. Ссылка на дочерний элемент из родительского документа.
// models/Transaction.js
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const transactionSchema = new Schema(
  {
    price: { type: Number },
    method: { type: String, default: 'VISA' },
    cardNumber: { type: String },
    paidTime: { type: Date, default: new Date() },
    // it used to be items: [itemSchema],
    items: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
  }
);
export const Transaction = mongoose.model('Transaction', transactionSchema);

Поскольку нам больше не нужна itemSchema, я удалил их и сослался на модель Product по ее идентификатору, указав тип свойства, которое я собираюсь хранить в элементах, и его имя модели в качестве ссылки.

{type: id, ref: model name}

TypeDefs

Если мы изменили нашу модель с помощью mongoose, нам также нужно сообщить typeDefs об изменениях, чтобы это работало в graphQL.

// typeDefs.js
// Transaction type and input
type Transaction {
  id: ID!
  price: Float!
  method: String!
  cardNumber: String!
  paidTime: Date!
  items: [Product]
}
input TransactionInput {
  price: Float
  method: String
  cardNumber: String
  items: [ID]
}
// Product type
input ProductInput {
  name: String
  price: Float
  category: String
}
type Product {
  id: ID!
  name: String
  price: Float
  category: String
}
type Mutation {
createTransaction(TransactionInput: TransactionInput!): Transaction
deleteTransaction(transactionID: ID!): Transaction
// resolver functions for product and item, will explain soon.
createItem(productId: String, transactionId: ID): Transaction
deleteItem(productId: String, transactionId: ID): Transaction
createProduct(ProductInput: ProductInput): Product
deleteProduct(productId: ID!): Product
}

Модель продукта

Модель продукта будет иметь определенные продукты, которые у нас есть. Представьте, что вы работаете в магазине сэндвичей, и у него есть два продукта, и всякий раз, когда он заказывается, идентификатор элемента будет вставлен в массив в транзакции. Итак, что я собираюсь сделать, так это создать несколько продуктов, которые будут использоваться в поле элементов транзакции.

Продукт Resolver

Мы собираемся создать преобразователь продуктов, и не забудьте подключить преобразователь к вашему серверу apollo.

// resolvers/product.js
import { Product } from '../models/Product';
export const productResolver = {
  Query: { query for product },
  Mutation: {
    createProduct: async (_, { ProductInput }) => {
      try {
        const newProduct = await Product.create(ProductInput);
        await newProduct.save()
        return newProduct
      } catch (error) {
        console.error(error.message)
      }
    },
    deleteProduct: async (_, { productId }) => {
      const deleteProduct = await    Product.findByIdAndDelete(productId)
      if (deleteProduct) {
        console.log(`Product ${productId} deleted.`)
      } else {
        console.log(`Product ${productId} doesn't exist`)
      }
    }

Я сделал две мутации продукта, которые эквивалентны POST и DELETE.

В createProduct новый продукт будет создан с передачей ввода продукта и сохранен.

В deleteProduct он найдет продукт для удаления с его идентификатором, а затем будет удален.

Однако мы еще не закончили. Если вы попытаетесь получить транзакцию, в поле элементов останется пустой массив.

Почему не работает?

Причина в том, что транзакция не знает, что делать с полем items. В typeDefs мы объявили, что поле items в Transaction будет иметь объект Product, но только предоставили преобразователю с идентификатором!

Тип операции

Чтобы решить проблему с пустым массивом поля элемента, нам нужно сообщить транзакции, что они с ним делают, точно указав поле элементов!

// resolver/transaction.js
export const transactionResolver = {
  Transaction: { 
    items: async (transaction) => {
    return (await transaction.populate('items').execPopulate()).items;
    },
  },
  
  Mutation: {... some mutation functions ...},
  Query: {... some query functions ...},

Транзакция здесь такая же, как и у typeDefs.js, и мы собираемся указать ее поле items с помощью асинхронной функции. Эта функция распознавателя унаследована от сервера Apollo, и функция принимает 4 аргумента, (parent, args, context, info). Но я собираюсь использовать только аргумент parent, который возвращает значение преобразователя для родительского поля этого поля, в данном случае transaction, родительского поля элементов.

Теперь нам нужно создать преобразователь для заполнения элементов.

// resolver/transaction.js
const transactionResolver = {
  ... previous code ...
createItem: async (_, { productId, transactionId }) => {
  try{
    const transaction = await Transaction.findById(transactionId);
    await transaction.items.push(productId);
    const savedTransaction = await transaction.save();
    return savedTransaction;
  }catch(error){
    console.error(error.message)
  }    
},

Для заполнения:

  1. найти транзакцию для заполнения по ее идентификатору
  2. вставить идентификатор товара в поле товаров
  3. затем сохраните изменение
  4. вернуть сохранить транзакцию

Вот как заполнить вложенный документ, на который ссылается его идентификатор. Мне нужно будет привыкнуть к этому, поскольку мне пришлось внести немало изменений. Спасибо за чтение, я надеюсь, что это также помогло тем из вас, кто хочет реализовать подобную архитектуру схемы.