node.jstypescriptmongodbtypeormdatabase-migration

First argument to $slice must be an array, but is of type: object


I have some MongoDB database records that have a nested Array, like this:

[
  {
    rootName: "AAA",
    outerItem: {
      innerItems: [
        {
          name: "A"
        },
        {
          name: null
        },
        {
          name: "C"
        }
      ]
    }
  }
]

And I have a query that will modify the records using operators like $concatArrays and $slice, which works fine in the MongoDB Playground:

db.collection.update({},
[
  {
    "$set": {
      "outerItem.innerItems": {
        $concatArrays: [
          {
            $slice: [
              "$outerItem.innerItems",
              0,
              1
            ]
          }
        ]
      }
    }
  }
],
{
  "multi": false,
  "upsert": false
})

It gives this expected result, where only a part of the original Array is used:

[
  {
    "_id": ObjectId("5a934e000102030405000000"),
    "outerItem": {
      "innerItems": [
        {
          "name": "A"
        }
      ]
    },
    "rootName": "AAA"
  }
]

MongoDB Playground link: https://mongoplayground.net/p/DhAjlLnN1xs

I tried to use the same code in a TypeScript Node.js project, using the TypeORM libraries:

import { MigrationInterface } from 'typeorm';
import { MongoQueryRunner } from 'typeorm/driver/mongodb/MongoQueryRunner';

export class MigrateTheDatabase implements MigrationInterface {
  public async up(queryRunner: MongoQueryRunner): Promise<void> {
    const result = await queryRunner.updateMany('records', {},
      [
        {
          $set: {
            'outerItem.innerItems': { $concatArrays: [{ $slice: ['$outerItem.innerItems', 0, 1] }] },
          }
        }
      ]);

      console.log(`Updated ${result.modifiedCount} records`);
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public async down(queryRunner: MongoQueryRunner): Promise<void> {
    // nothing
  }
}

But when this database migration runs, it just prints an error at runtime, and the query fails to update any records:

Closing migrations connection...
Error while connecting to the database! MongoServerError: First argument to $slice must be an array, but is of type: object
  at UpdateManyOperation.execute (/home/user/Project/node_modules/mongodb/src/operations/update.ts:184:32)
  at processTicksAndRejections (node:internal/process/task_queues:105:5)
  at executeOperation (/home/user/Project/node_modules/mongodb/src/operations/execute_operation.ts:155:14)
  at MigrateTheDatabase.up (/home/user/Project/dist/migrations/api1/MigrateTheDatabase.js:6:24)
  at MigrationExecutor.executePendingMigrations (/home/user/Project/src/migration/MigrationExecutor.ts:336:17)
  at DataSource.runMigrations (/home/user/Project/src/data-source/DataSource.ts:403:13)
  at runMigrations (/home/user/Project/dist/api1/main.js:824:9)
  at bootstrap (/home/user/Project/dist/api1/main.js:19224:5) {
    errorResponse: {
      index: 0,
      code: 28724,
      errmsg: 'First argument to $slice must be an array, but is of type: object'
    },
    index: 0,
    code: 28724,
    [Symbol(errorLabels)]: Set(0) {}
  }

I expected it to update all records just like it did on the Playground. What am I doing wrong? How do I get the Playground code to work in TypeScript with the MongoQueryRunner?


Solution

  • You have some documents where outerItem.innerItems is not an array. Like the second document in this example:

    [
      {
        _id: "this doc is okay",
        rootName: "AAA",
        outerItem: {
          innerItems: [
            { name: "A" },
            { name: null },
            { name: "C" }
          ]
        }
      },
      {
        _id: "this doc will cause an error",
        rootName: "AAA",
        outerItem: {
          innerItems: { name: "A" }
        }
      }
    ]
    

    You will either need to fix those documents or change the filter criteria to only update docs where it's an array:

    db.collection.update(
      { "outerItem.innerItems": { $type: "array" } },
      [ ... ]  // the same pipeline as before
    )
    

    Mongo Playground. Adding that filter will also handle cases of outerItem.innerItems being null, or missing, even if the parent field outerItem is null or missing.