Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ManyToOne in composite primary key does not work if referenced entity also uses a composite key #2647

Closed
itsame-luigi opened this issue Jan 19, 2022 · 13 comments
Labels
bug Something isn't working

Comments

@itsame-luigi
Copy link
Contributor

Describe the bug
An entity that includes a @ManyToOne property in its primary key cannot be hydrated if the many-to-one reference points to another entity that uses a composite key.

Stack trace

TypeError: Cannot read property 'id' of undefined
    at <project>\node_modules\@mikro-orm\core\utils\Utils.js:387:63
    at Array.map (<anonymous>)
    at Function.getOrderedPrimaryKeys (<project>\node_modules\@mikro-orm\core\utils\Utils.js:384:33)
    at <project>\node_modules\@mikro-orm\core\utils\Utils.js:389:37
    at Array.map (<anonymous>)
    at Function.getOrderedPrimaryKeys (<project>\node_modules\@mikro-orm\core\utils\Utils.js:384:33)
    at <project>\node_modules\@mikro-orm\core\utils\Utils.js:389:37
    at Array.map (<anonymous>)
    at Function.getOrderedPrimaryKeys (<project>\node_modules\@mikro-orm\core\utils\Utils.js:384:33)
    at EntityFactory.createReference (<project>\node_modules\@mikro-orm\core\entity\EntityFactory.js:53:35)
    at eval (eval at createFunction (<project>\node_modules\@mikro-orm\core\utils\Utils.js:612:20), <anonymous>:102:37)
    at Function.callCompiledFunction (<project>\node_modules\@mikro-orm\core\utils\Utils.js:623:20)
    at ObjectHydrator.hydrate (<project>\node_modules\@mikro-orm\core\hydration\ObjectHydrator.js:24:23)
    at EntityFactory.hydrate (<project>\node_modules\@mikro-orm\core\entity\EntityFactory.js:82:27)
    at EntityFactory.create (<project>\node_modules\@mikro-orm\core\entity\EntityFactory.js:36:14)
    at SqlEntityManager.find (<project>\node_modules\@mikro-orm\core\EntityManager.js:100:52)

To Reproduce
Steps to reproduce the behavior:

  1. Define an entity relationship as follows:
    @Entity()
    export class A extends BaseEntity {
      @PrimaryKey()
      id: string;
    }
    
    @Entity()
    export class B extends BaseEntity {
      @PrimaryKey()
      id: string;
    }
    
    @Entity() 
    export class C extends BaseEntity {
      @PrimaryKey()
      id: string;
    }
    
    @Entity()
    export class AB extends BaseEntity {
      @ManyToOne(() => A, { eager: true, primary: true })
      a: A;
      
      @ManyToOne(() => B, { eager: true, primary: true })
      b: B;
    }
    
    @Entity()
    export class CAB extends BaseEntity {
      @ManyToOne(() => C, { eager: true, primary: true })
      c: C;
      
      @ManyToOne(() => AB, { eager: true, primary: true })
      ab: AB;
    }
  2. Create and persist one of each entity.
  3. Execute await em.find(CAB, {})

Expected behavior
Currently, metadata discovery will determine the primary keys of CAB are ["c", "ab"]. When EntityFactory..createReference is called during hydration, it receives a composite key of something like ["<c.id>", "<a.id>", "<b.id>"], which isn't properly handled by Utils.getPrimaryKeyCondFromArray or Utils.getOrderedPrimaryKeys.

I would expect createReference to be more thorough about expanding composite keys in the same way that filters, joins, and inserts do, without resulting in an exception.

Versions

Dependency Version
node v16.6.2
typescript ^4.5.4
mikro-orm ^4.5.9
your-driver @mikro-orm/sqlite@^4.5.9
@B4nan
Copy link
Member

B4nan commented Jan 19, 2022

Please provide reproduction as code, ideally as a test case. Entity definition looks good, but I can easily imagine you oversimplified it. Also have you tried it without eager: true?

What does ^4.5.9 mean? are you on 4.5.9 or 4.5.10? You should put actual versions you use in there. If you are not using 4.5.10, then upgrade, there were tons of bug fixes. Ideally go for v5, as there are even more fixes (and v5 is pretty much ready to be used, just need to polish the docs and do few minor changes).

Currently, metadata discovery will determine the primary keys of CAB are ["c", "ab"]

The PKs are c and ab - those are property names, not database columns. This works correctly.

When EntityFactory.createReference is called during hydration, it receives a composite key of something like ["<c.id>", "<a.id>", "<b.id>"],

That sounds a bit wrong, maybe that's where the bug is, createReference should be again getting property names, not column names.

@itsame-luigi
Copy link
Contributor Author

Also have you tried it without eager: true?

I will check, but I don't think changing it has any effect.

What does ^4.5.9 mean? are you on 4.5.9 or 4.5.10? You should put actual versions you use in there. If you are not using 4.5.10, then upgrade, there were tons of bug fixes. Ideally go for v5, as there are even more fixes (and v5 is pretty much ready to be used, just need to polish the docs and do few minor changes).

I'm sorry, I should have written 4.5.9.

When EntityFactory.createReference is called during hydration, it receives a composite key of something like ["<c.id>", "<a.id>", "<b.id>"],

That sounds a bit wrong, maybe that's where the bug is, createReference should be again getting property names, not column names.

I apologize for the confusion. by <c.id> I meant "whatever value you used for the id of c", so the composite key might be something like ["3", "1", "2"] if a.id === "1", b.id === "2", and c.id === "3".

I'll amend my issue with an example test case shortly.

@itsame-luigi
Copy link
Contributor Author

itsame-luigi commented Jan 20, 2022

I'm running into the same issue in 5.0.0-dev.645, and I'm working on a more comprehensive test case to illustrate the issue.

As part of my investigation, I'm also finding that there's an issue in CriteriaNode when there is a nested composite key. In the constructor it calls Utils.splitPrimaryKeys(key).forEach and then sets this.prop = meta.props.find(...). This results in a criteria node whose prop is only one of the multiple properties associated with a composite key. This results in an invalid SQL query:

DriverException: select `s0`.* from `cab` as `s0` where (`s0`.`ab_a_id`, `s0`.`ab_b_id`) in 
( values ('933569619573211136', '874812707751215184', '846302692724047893')) order by `s0`.`ab_a_id` asc, 
`s0`.`ab_b_id` asc - SQLITE_ERROR: sub-select returns 3 columns - expected 2

Here, the composite key contains three values:

[
  '933569619573211136', // the value of `cab.c.id` in entity (`c_id` in table)
  '874812707751215184', // the value of `cab.ab.a.id` in entity (`ab_a_id` in table)
  '846302692724047893'  // the value of `cab.ab.b.id` in entity (`ab_b_id` in table)
]

In constructor, key is c~~~ab, so Utils.splitPrimaryKeys(key).forEach invokes the callback twice:

  1. Sets this.prop to the property metadata for c
  2. Overwrites this.prop to the property metadata for ab

When the query is constructed, there doesn't seem to be any reference to c so its not added to the resulting SQL.

@B4nan
Copy link
Member

B4nan commented Jan 22, 2022

Not reproducible, here is a passing test case with entity definition you provided:

import { Entity, ManyToOne, MikroORM, PrimaryKey } from '@mikro-orm/core';

@Entity()
export class A {

  @PrimaryKey()
  id!: number;

}

@Entity()
export class B {

  @PrimaryKey()
  id!: number;

}

@Entity()
export class C {

  @PrimaryKey()
  id!: number;

}

@Entity()
export class AB {

  @ManyToOne(() => A, { primary: true })
  a!: A;

  @ManyToOne(() => B, { primary: true })
  b!: B;

}

@Entity()
export class CAB {

  @ManyToOne(() => C, { primary: true })
  c!: C;

  @ManyToOne(() => AB, { primary: true })
  ab!: AB;

}

describe('GH #2647', () => {

  let orm: MikroORM;

  beforeAll(async () => {
    orm = await MikroORM.init({
      entities: [A, B, C, AB, CAB],
      dbName: `:memory:`,
      type: 'sqlite',
    });
    await orm.getSchemaGenerator().createSchema();
  });

  afterAll(async () => {
    await orm.close(true);
  });

  it('working with complex composite entities', async () => {
    const a = new A();
    const b = new B();
    const c = new C();
    const ab = new AB();
    ab.a = a;
    ab.b = b;
    await orm.em.persistAndFlush([ab, c]);
    const cab = new CAB();
    cab.ab = ab;
    cab.c = c;
    await orm.em.persistAndFlush(cab);
    orm.em.clear();

    const res = await orm.em.find(CAB, {}, { populate: true });
    expect(res).toHaveLength(1);
  });

});

So please instead of more investigation, provide actual reproduction, that is always the first step.

I am also waiting for repro of the ESM issue - that should really be a one minute thing for you, I just need to see how the stack trace look like.

@B4nan
Copy link
Member

B4nan commented Jan 25, 2022

Closing as not reproducible, will reopen if you provide failing repro.

@B4nan B4nan closed this as completed Jan 25, 2022
@itsame-luigi
Copy link
Contributor Author

I was able to repro this with this test:

import { Entity, ManyToOne, MikroORM, PrimaryKey } from '@mikro-orm/core';

@Entity()
export class A {
    @PrimaryKey()
    id: number;

    constructor(id: number) {
        this.id = id;
    }
}

@Entity()
export class B {
    @PrimaryKey()
    id: number;

    constructor(id: number) {
        this.id = id;
    }
}

@Entity()
export class C {
    @PrimaryKey()
    id: number;

    constructor(id: number) {
        this.id = id;
    }
}

@Entity()
export class D {
    @PrimaryKey()
    id: number;

    constructor(id: number) {
        this.id = id;
    }
}

@Entity()
export class AB {
    @ManyToOne(() => A, { eager: true, primary: true })
    a: A;

    @ManyToOne(() => B, { eager: true, primary: true })
    b: B;

    constructor(a: A, b: B) {
        this.a = a;
        this.b = b;
    }
}

@Entity()
export class CAB {
    @ManyToOne(() => C, { eager: true, primary: true })
    c: C;

    @ManyToOne(() => AB, { eager: true, primary: true })
    ab: AB;

    constructor(c: C, ab: AB) {
        this.c = c;
        this.ab = ab;
    }
}

@Entity()
export class DCAB {
    @ManyToOne(() => D, { eager: true, primary: true })
    d: D;

    @ManyToOne(() => CAB, { eager: true, primary: true })
    cab: CAB;

    constructor(d: D, cab: CAB) {
        this.d = d;
        this.cab = cab;
    }
}

describe('GH #2647', () => {
    let orm: MikroORM;

    beforeAll(async () => {
        orm = await MikroORM.init({
            entities: [A, B, C, D, AB, CAB, DCAB],
            dbName: `:memory:`,
            type: 'sqlite'
        });
        await orm.getSchemaGenerator().createSchema();
    });

    afterAll(async () => {
        await orm.close(true);
    });

    it('should be able to find entity with nested composite key', async () => {
        await orm.em.nativeDelete(DCAB, {});
        await orm.em.nativeDelete(CAB, {});
        await orm.em.nativeDelete(AB, {});
        await orm.em.nativeDelete(D, {});
        await orm.em.nativeDelete(C, {});
        await orm.em.nativeDelete(B, {});
        await orm.em.nativeDelete(A, {});

        const a = new A(1);
        const b = new B(2);
        const c = new C(3);
        const d = new D(4);
        const ab = new AB(a, b);
        const cab = new CAB(c, ab);
        const dcab = new DCAB(d, cab);

        await orm.em.persistAndFlush([a, b, c, d, ab, cab, dcab]);
        orm.em.clear();

        const res = await orm.em.find(DCAB, { d, cab });
        expect(res).toHaveLength(1);
    });
});

@B4nan B4nan added bug Something isn't working and removed needs clarification labels Feb 1, 2022
@B4nan
Copy link
Member

B4nan commented Feb 1, 2022

Thanks, will look into!

@B4nan B4nan reopened this Feb 1, 2022
@B4nan
Copy link
Member

B4nan commented Feb 2, 2022

Oh damn, this is very complex, every issue I fix leads to something else.

@B4nan B4nan closed this as completed in 14dcff8 Feb 2, 2022
@itsame-luigi
Copy link
Contributor Author

I'm not quite sure this is fully resolved, as I'm getting the following error in 5.0.1-dev.23:

DriverException: Expected 3 bindings, saw 2
     at SqliteExceptionConverter.convertException (C:\dev\project\node_modules\@mikro-orm\core\dist\platforms\ExceptionConverter.js:8:16)
     at SqliteExceptionConverter.convertException (C:\dev\project\node_modules\@mikro-orm\sqlite\dist\SqliteExceptionConverter.js:46:22)
     at SqliteDriver.convertException (C:\dev\project\node_modules\@mikro-orm\core\dist\drivers\DatabaseDriver.js:180:54)
     at C:\dev\project\node_modules\@mikro-orm\core\dist\drivers\DatabaseDriver.js:184:24
     at async SqliteDriver.find (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\@mikro-orm\knex\dist\AbstractSqlDriver.js:46:24)
     at async SqliteDriver.findOne (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\@mikro-orm\knex\dist\AbstractSqlDriver.js:60:21)
     at async SqlEntityManager.findOne (C:\dev\project\node_modules\@mikro-orm\core\dist\EntityManager.js:282:22)
     at async WrappedEntity.init (C:\dev\project\node_modules\@mikro-orm\core\dist\entity\WrappedEntity.js:55:9)
     at async Reference.load (C:\dev\project\node_modules\@mikro-orm\core\dist\entity\Reference.js:66:13)
     at SessionLogic._SessionLogic_updateDeviceControlMessage (C:\dev\project\src\logic\sessionLogic.ts:536:84)
 
 previous Error: Expected 3 bindings, saw 2
     at replaceRawArrBindings (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\formatter\rawFormatter.js:27:11)
     at Raw.toSQL (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\raw.js:77:13)
     at unwrapRaw (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\formatter\wrappingFormatter.js:116:19)
     at Client_SQLite3.parameter (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\client.js:384:12)
     at Client_SQLite3.values (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\dialects\sqlite3\index.js:219:23)
     at QueryCompiler_SQLite3.whereIn (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\query\querycompiler.js:950:32)
     at QueryCompiler_SQLite3.where (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\query\querycompiler.js:521:34)
     at C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\query\querycompiler.js:127:69
     at Array.map (<anonymous>)
     at QueryCompiler_SQLite3.select (C:\dev\project\node_modules\@mikro-orm\sqlite\node_modules\knex\lib\query\querycompiler.js:127:35)

If I set a breakpoint on rawFormatter.js:27 I see the following:

  • Variable sql has the string values (?, ?) (i.e., expecting two bindings)
  • Variable values (from raw.bindings) has the value: ['933569619573211136', '874812707751215184', '846302692724047893'] (three bindings)

If this isn't showing up in the test for this, I'll see if I can put together a test that triggers this behavior.

@itsame-luigi
Copy link
Contributor Author

I stepped through into processObjectSubCondition in @mikro-orm/knex/dist/query/QueryBuilderHelper.js and here's what I found:

  • key is s0.session~~~member — This is a composite key "hash" for the Participant entity, where the Member entity also has a composite key derived from a User entity and a Provider entity.
  • cond[key] is { $in: ['933569619573211136', '874812707751215184', '846302692724047893'] } — The first is the session id, the second is the user id and the third is the provider id (where the 2nd and 3rd make up the composite key for the member)
  • At line 312, Utils.splitPrimaryKeys(key) is called, resulting in s0.session and member (only two entries). The following lines generate the sql fragment values (?, ?) with one ? for each entry in the primary key.

@B4nan
Copy link
Member

B4nan commented Feb 10, 2022

Thanks for debugging, but I really need to see reproductions first.

@itsame-luigi
Copy link
Contributor Author

itsame-luigi commented Feb 11, 2022

Repro (tested locally in the mikro-orm codebase):

import { Entity, ManyToOne, MikroORM, PrimaryKey } from '@mikro-orm/core';

@Entity()
export class Provider {

  @PrimaryKey()
  id: number;

  constructor(id: number) {
    this.id = id;
  }
}

@Entity()
export class User {

  @PrimaryKey()
  id: number;

  constructor(id: number) {
    this.id = id;
  }

}

@Entity()
export class Member {

  @ManyToOne(() => Provider, { eager: true, primary: true })
  provider: Provider;

  @ManyToOne(() => User, { eager: true, primary: true })
  user: User;

  constructor(a: Provider, b: User) {
    this.provider = a;
    this.user = b;
  }

}

@Entity()
export class Session {

  @PrimaryKey()
  id: number;

  @ManyToOne(() => Member, { eager: true })
  owner: Member;

  @ManyToOne(() => Participant, { nullable: true, default: null, eager: true })
  lastActionBy: Participant | null = null;

  constructor(id: number, owner: Member) {
    this.id = id;
    this.owner = owner;
  }

}

@Entity()
export class Participant {

  @ManyToOne(() => Session, { eager: true, primary: true })
  session: Session;

  @ManyToOne(() => Member, { eager: true, primary: true })
  member: Member;

  constructor(session: Session, member: Member) {
    this.session = session;
    this.member = member;
  }

}

describe('GH #2647-2', () => {

  let orm: MikroORM;

  beforeAll(async () => {
    orm = await MikroORM.init({
      entities: [Provider, User, Member, Session, Participant],
      dbName: `:memory:`,
      type: 'sqlite',
      // NOTE: this just shows the query is executed repeatedly
      debug: true
    });
    await orm.getSchemaGenerator().createSchema();
  });

  afterAll(async () => {
    await orm.close(true);
  });

  beforeEach(async () => {
    await orm.em.nativeDelete(Participant, {});
    await orm.em.nativeDelete(Member, {});
    await orm.em.nativeDelete(Session, {});
    await orm.em.nativeDelete(User, {});
    await orm.em.nativeDelete(Provider, {});
  });

  function createEntities(pks: [providerId: number, userId: number, sessionId: number]) {
    const provider = new Provider(pks[0]);
    const user = new User(pks[1]);
    const member = new Member(provider, user);
    const session = new Session(pks[2], member);
    const participant = new Participant(session, member);
    session.lastActionBy = participant;
    orm.em.persist([provider, user, member, session, participant]);

    return { provider, user, member, session, participant };
  }

  it('should be able to populate circularity', async () => {
    const { session, member } = createEntities([1, 2, 3]);
    await orm.em.flush();
    orm.em.clear();

    // this results in infinite recursion until memory is exhausted
    const res = await orm.em.find(Participant, { session, member });
    expect(res).toHaveLength(1);
  });
});

Running this test results in this output (because I passed debug: true to MikroORM.init):

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 1 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 1 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 1 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 1 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 1 ms]

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log                                                                                                                                                                                                                                                   
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]                               

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

  console.log
    [query] select `p0`.* from `participant` as `p0` where (`p0`.`session_id`, `p0`.`member_provider_id`, `p0`.`member_user_id`) in ( values (3, 1, 2)) order by `p0`.`session_id` asc, `p0`.`member_provider_id` asc [took 0 ms]

      at DefaultLogger.log (packages/core/src/logging/DefaultLogger.ts:35:10)

...

And continues until either:

  • test times out
  • you run out of memory
  • you kill the test process

@B4nan
Copy link
Member

B4nan commented Feb 11, 2022

Ok so composite keys with cycles from PKs to the owning entity, and eager loading. Now I am not surprised it's broken, that's like the worst combination I can imagine :]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants