objection.js icon indicating copy to clipboard operation
objection.js copied to clipboard

knex.migrate.latest() fails when ...knexSnakeCaseMappers is part of the knex config

Open cauboy opened this issue 3 years ago • 5 comments

I'm having the following issue:

When trying to do migration with an "knexSnakeCaseMappers"-ized knex instance using "objection": "3.0.1" and knex": "^0.95.15", the migration fails with the following error:

migration file "1.ts" failed
migration failed with error: Cannot read properties of undefined (reading 'lastIndexOf')

 TypeError {
    message: 'Cannot read properties of undefined (reading \'lastIndexOf\')',
  }

  › node_modules/objection/lib/utils/identifierMapping.js:139:21
  › node_modules/objection/lib/utils/identifierMapping.js:13:16
  › Object.wrapIdentifier (node_modules/objection/lib/utils/identifierMapping.js:180:23)
  › Client_SQLite3.customWrapIdentifier (node_modules/knex/lib/client.js:177:26)
  › TableCompiler_SQLite3.primary (node_modules/knex/lib/dialects/sqlite3/schema/sqlite-tablecompiler.js:192:34)
  › TableCompiler_SQLite3.alterTable (node_modules/knex/lib/schema/tablecompiler.js:245:32)
  › TableCompiler_SQLite3.create (node_modules/knex/lib/schema/tablecompiler.js:59:10)
  › TableCompiler_SQLite3.toSQL (node_modules/knex/lib/schema/tablecompiler.js:39:22)
  › TableBuilder.toSQL (node_modules/knex/lib/schema/tablebuilder.js:49:44)
  › SchemaCompiler_SQLite3.build (node_modules/knex/lib/schema/compiler.js:143:23)

How to reproduce:

The error is thrown when running ts-node migrate.ts

// migrate.ts
import { db } from '../src/db
db.migrate.latest() // <--- This command fails
// db.ts
import { knexSnakeCaseMappers } from 'objection'
export const db = knex({
  client: 'sqlite3',
  migrations: { tableName: 'knex_migrations', },
  connection: { filename: './sqlite.db' },
  useNullAsDefault: true,
  ...knexSnakeCaseMappers(),
})
// migrations/1.ts
import { Knex } from 'knex'
export function up(knex: Knex) {
  return knex.schema.createTable('example', (table) => {
    table.uuid('id').primary()
  })
}

export function down(knex: Knex) {
  return knex.schema.dropTable('example')
}

Possible solution

The issue is that undefined is passed as the value for the str parameter to the function that is returned from mapLastPart. I could fix the issue by returning the identity whenever str is not a String

// node_modules/objection/lib/utils/identifierMapping.js
function mapLastPart(mapper, separator) {
  return (str) => {
    // if (typeof str !== 'string' ) { return str } // <--- Potential fix
    const idx = str.lastIndexOf(separator);
    const mapped = mapper(str.slice(idx + separator.length));
    return str.slice(0, idx + separator.length) + mapped;
  };
}

cauboy avatar Jan 14 '22 19:01 cauboy

I have the same problem when I use knexSnakeCaseMappers with sqlite3 and better-sqlite3 and this error throw on setting non increment primary key like:

    table.string('id').notNullable();
    table.string('name').notNullable();
    table.integer('channel_id');
    table.integer('project_id').notNullable();
    table.specificType('status', 'account_statuses');

    table.primary(['id', 'name']);
    table.unique(['id', 'name']);

    table.foreign('project_id').references('id').inTable('projects');
    table.foreign('channel_id').references('id').inTable('channels');

IoannP avatar May 26 '22 06:05 IoannP

I have the same problem when I use knexSnakeCaseMappers with sqlite3 and better-sqlite3 and this error throw on setting non increment primary key like:

    table.string('id').notNullable();
    table.string('name').notNullable();
    table.integer('channel_id');
    table.integer('project_id').notNullable();
    table.specificType('status', 'account_statuses');

    table.primary(['id', 'name']);
    table.unique(['id', 'name']);

    table.foreign('project_id').references('id').inTable('projects');
    table.foreign('channel_id').references('id').inTable('channels');

Have the same issue with the same use case. Any update on this?

DomenicoCasillo avatar Jul 12 '22 15:07 DomenicoCasillo

@DomenicoCasillo In my case, the solution was to set the primary key constraint name like table.primary(['id', 'channel_id'], 'accounts_pkey');

IoannP avatar Jul 12 '22 19:07 IoannP

@DomenicoCasillo In my case, the solution was to set the primary key constraint name like table.primary(['id', 'channel_id'], 'accounts_pkey');

Unfortunately this didn't work for me. My final solution to the issue is add knexSnakeCaseMappers to knexfile conditionally. For instance it's not added only when migrate is running (you can use an env var for this).

DomenicoCasillo avatar Jul 13 '22 20:07 DomenicoCasillo

Also having this issue. Applying this patch fixes it for me:

diff --git a/node_modules/objection/lib/utils/identifierMapping.js b/node_modules/objection/lib/utils/identifierMapping.js
index afec0bc..9b6e8d5 100644
--- a/node_modules/objection/lib/utils/identifierMapping.js
+++ b/node_modules/objection/lib/utils/identifierMapping.js
@@ -135,6 +135,7 @@ function isDigit(char) {
 // If no separators are found, `mapper` is called for the entire string.
 function mapLastPart(mapper, separator) {
   return (str) => {
+    if (typeof str !== 'string') return str;
     const idx = str.lastIndexOf(separator);
     const mapped = mapper(str.slice(idx + separator.length));
     return str.slice(0, idx + separator.length) + mapped;

vostrnad avatar Sep 11 '22 12:09 vostrnad

This was fixed in #2333

lehni avatar Apr 13 '23 21:04 lehni