UNPKG

undeexcepturi

Version:

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.

859 lines (740 loc) 31.4 kB
import { Constructor, EntityRepository, EntitySchema, OptionalProps, ref, wrap } from '@mikro-orm/core'; import type { BaseEntity, Ref, Reference, Collection, EntityManager, EntityName, RequiredEntityData } from '@mikro-orm/core'; import type { Has, IsExact } from 'conditional-type-checks'; import { assert } from 'conditional-type-checks'; import type { ObjectId } from 'bson'; import type { EntityData, EntityDTO, FilterQuery, FilterValue, Loaded, OperatorMap, Primary, PrimaryKeyProp, ExpandQuery, } from '../packages/core/src/typings'; import type { Author2, Book2, BookTag2, Car2, FooBar2, FooParam2, Publisher2, User2 } from './entities-sql'; import type { Author, Book } from './entities'; type IsAssignable<T, Expected> = Expected extends T ? true : false; describe('check typings', () => { test('Primary', async () => { assert<IsExact<Primary<Book2>, string>>(true); assert<IsExact<Primary<Book2>, number>>(false); assert<IsExact<Primary<Author2>, number>>(true); assert<IsExact<Primary<{ id?: number }>, number>>(true); assert<IsExact<Primary<Author2>, string>>(false); // PrimaryKeyProp symbol has priority type Test = { _id: ObjectId; id: string; uuid: number; foo: Date; [PrimaryKeyProp]?: 'foo' }; assert<IsExact<Primary<Test>, Date>>(true); assert<IsExact<Primary<Test>, ObjectId>>(false); assert<IsExact<Primary<Test>, string>>(false); assert<IsExact<Primary<Test>, number>>(false); // object id allows string assert<IsExact<Primary<Author>, ObjectId | string>>(true); assert<IsExact<Primary<Author>, number>>(false); // bigint support assert<IsExact<Primary<BookTag2>, bigint>>(true); assert<IsExact<Primary<BookTag2>, number>>(false); }); test('EntityData', async () => { let b: EntityData<Book2>; b = {}; b = {} as Author2; b = { author: { name: 'a' } }; b = { author: { name: 'a', books: [] } }; b = { author: { name: 'a', books: [{ title: 'b', tags: { name: 't' } }] } }; b = { publisher: null }; b = { publisher: { name: 'p' } }; b = { publisher: {} as Publisher2 }; b = { publisher: {} as Ref<Publisher2> }; // @ts-expect-error b = { name: 'a' }; // @ts-expect-error b = { author: { title: 'a' } }; // @ts-expect-error b = { author: { name: 'a', books: [{ name: 'b' }] } }; // @ts-expect-error b = { author: { name: 'a', books: [{ title: 'b', tags: { title: 't' } }] } }; let c: EntityData<Car2>; c = {}; c = {} as Car2; c = { name: 'n', price: 123, year: 2021 }; c = { name: 'n', price: 123, year: 2021, users: { firstName: 'f', lastName: 'l' } }; c = { name: 'n', price: 123, year: 2021, users: [{ firstName: 'f', lastName: 'l' }] }; c = { name: 'n', price: 123, year: 2021, users: [{} as User2] }; c = { name: 'n', price: 123, year: 2021, users: [['f', 'l']] }; type T = Primary<User2>; // @ts-expect-error c = { name: 'n', price: 123, year: '2021' }; // @ts-expect-error c = { name: 'n', price: 123, year: 2021, users: { firstName: 321, lastName: 'l' } }; // @ts-expect-error c = { name: 'n', price: 123, year: 2021, users: [{ firstName: 'f', lastName: 123 }] }; // @ts-expect-error c = { name: 'n', price: 123, year: 2021, users: [{} as Car2] }; // @ts-expect-error c = { name: 'n', price: 123, year: 2021, users: [['f', 1]] }; }); test('EntityDTO', async () => { const b = { author: { books: [{}], identities: [''] } } as unknown as EntityDTO<Loaded<Book2, 'publisher'>>; const b1 = b.author.name; const b2 = b.test?.name; const b3 = b.test?.book?.author.books2; const b4 = b.author.books[0].tags; const b5 = b.publisher?.name; const b6 = b.publisher?.tests; const b7 = b.author.favouriteBook?.tags[0].name; const b8: number = b.author.identities!.length; const b9: string[] = b.author.identities!.slice(); const b10: string[] = b.author.identities!.filter(i => i); // @ts-expect-error b.author.afterDelete?.(); // @ts-expect-error b.author.title; // @ts-expect-error b.author.favouriteBook?.tags[0].title; // @ts-expect-error b.test?.getConfiguration?.(); const a = { books: [{ tags: [{}] }] } as unknown as EntityDTO<Loaded<Author2, 'books.tags' | 'books.publisher'>>; const a11 = a.books; const a12 = a.books[0]; const a1 = a.books[0].tags; const a2 = a.books[0].publisher?.type; const a3 = a.books; const a4 = a.books.map(b => b.title); const a5 = a.books[0].tags.map(t => t.name); const a6 = a.books[0].tags[0].name; // @ts-expect-error a.books.map(b => b.name); // @ts-expect-error a.books[0].publisher?.title; // @ts-expect-error a.books[0].tags.map(t => t.title); }); test('FilterValue', async () => { assert<Has<FilterValue<string>, RegExp | string | null | never[] | OperatorMap<string>>>(true); // strings allow regexps assert<Has<FilterValue<number>, number | null | never[] | OperatorMap<number>>>(true); assert<Has<FilterValue<string>, number>>(false); assert<Has<FilterValue<Date>, Date | null | never[] | OperatorMap<Date>>>(true); assert<Has<FilterValue<RegExp>, RegExp | null | never[] | OperatorMap<RegExp>>>(true); assert<Has<FilterValue<string>, number>>(false); // require specific type assert<Has<FilterValue<number>, string>>(false); assert<Has<FilterValue<Date>, string>>(true); // allows string dates assert<Has<FilterValue<Date>, number>>(false); // allows collection item // assert<Has<Query<Collection<Book2>>, string>>(true); // assert<Has<Query<Collection<Author2>>, number>>(true); // assert<Has<Query<Collection<Author2>>, string>>(false); // assert<Has<Query<Collection<Author2>>, Author2>>(true); // assert<Has<Query<Collection<Author2>>, string[]>>(false); // assert<Has<Query<Collection<Author2>>, Book2[]>>(false); // assert<Has<Query<Author['books']>, ObjectId>>(true); // assert<Has<Query<Collection<Book2>>, string>>(true); // assert<Has<Query<Collection<Book>>, ObjectId>>(true); // allows entity/pk and arrays of entity/pk assert<Has<FilterValue<Author2>, Author2>>(true); assert<Has<FilterValue<Author2>, number>>(true); // date requires date assert<Has<FilterValue<Author['born']>, Date>>(false); assert<Has<FilterValue<Author['born']>, number>>(false); assert<Has<FilterValue<Author['born']>, string>>(true); }); test('Query', async () => { // assert<Has<FilterQuery<Author['born']>, Date>>(true); assert<Has<ExpandQuery<Author['born']>, number>>(false); // assert<Has<Query<Author['born']>, string>>(true); assert<Has<ExpandQuery<Author>, { born?: Date }>>(false); assert<Has<ExpandQuery<Author>, { born?: number }>>(false); assert<Has<ExpandQuery<Author>, { born?: string }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: string }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: null }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: number }>>(false); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: { author: number } }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: { author: null } }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: { author: string } }>>(false); // assert<Has<Query<Author2>, Author2>>(true); assert<Has<ExpandQuery<Author2>, number>>(true); assert<Has<ExpandQuery<Author2>, string>>(false); assert<Has<ExpandQuery<Author2>, { books: { author: { born?: string } }; favouriteBook: null }>>(false); assert<Has<ExpandQuery<Author2>, { books: { author: { born?: number } }; favouriteBook: null }>>(false); // assert<Has<Query<Book2>, { author: { born?: Date } }>>(true); assert<Has<ExpandQuery<Book2>, { author: { born?: string } }>>(false); assert<Has<ExpandQuery<Book2>, { author: { born?: number } }>>(false); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: { author: { born: Date } } }>>(false); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: { author: { born: string } } }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: { author: { books: string[] } } }>>(true); assert<IsAssignable<ExpandQuery<Book2>, { author: { books: string[] } }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: string } } }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: Date } } }>>(false); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: null } } }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: null }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { favouriteBook: string }>>(true); assert<Has<ExpandQuery<Author2>, { favouriteBook: number }>>(false); assert<Has<ExpandQuery<Book2>, { author: { born?: Date }; favouriteBook: string }>>(false); // favouriteBook does not exist on Book2 assert<IsAssignable<ExpandQuery<Book2>, { author: { books: { publisher: number } } }>>(true); assert<IsAssignable<ExpandQuery<Book2>, { author: { books: { publisher: null } } }>>(true); assert<Has<ExpandQuery<Author2>, { favouriteBook?: ExpandQuery<Book2> }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: FilterValue<Book2> }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: string }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: string[] }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: Book2 }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: Book2[] }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: null }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: Author2 }; favouriteBook: Book2 }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: string } }; favouriteBook: null }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: string } }; favouriteBook: Book2 }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: string } }; favouriteBook: null }>>(true); assert<IsAssignable<ExpandQuery<Author2>, { books: { author: { born: string } }; favouriteBook: { title: null } }>>(true); let t1: ExpandQuery<Book2>; t1 = { author: { books: { publisher: 1 } } }; // ok // @ts-expect-error t1 = { author: { books: { publisher: '1' } } }; // should fail let t2: ExpandQuery<Author2>; t2 = { age: { $gte: 1 } }; t2 = { born: '1' }; t2 = { books: { author: { born: '2020-11-11' } }, favouriteBook: null }; // accepts string date // @ts-expect-error t2 = { books: { author: { born: 1 } }, favouriteBook: null }; t2 = { books: { author: { born: '1' } }, favouriteBook: null }; // accepts string date }); test('FilterQueryOrPrimary', async () => { // assert<Has<FilterQueryOrPrimary<Author2>, number>>(true); assert<Has<FilterQuery<Author2>, string>>(false); assert<IsAssignable<FilterQuery<Book2>, { author: 123 }>>(true); assert<IsAssignable<FilterQuery<Author2>, { favouriteBook: null }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { author: { name: 'asd' } } }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { author: Author2 } }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { author: 123 } }>>(true); // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { books: { author: '123' } }>>(false); // hard to test failures assert<IsAssignable<FilterQuery<Author2>, { books: { title: '123' }; favouriteBook: null }>>(true); // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { books: { title: 123 }; favouriteBook: null }>>(false); // hard to test failures // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { books: { title: Date }; favouriteBook: null }>>(false); // hard to test failures assert<IsAssignable<FilterQuery<Author2>, { born: string }>>(true); // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { born: number }>>(false); // hard to test failures // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { born: string }>>(false); // hard to test failures assert<IsAssignable<FilterQuery<Author2>, { age: { $in: [1] } }>>(true); // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { age: { $in: ['1'] } }>>(false); // hard to test failures // assert<IsAssignable<FilterQueryOrPrimary<Author2>, { age: { $gta: ['1'] } }>>(false); // hard to test failures assert<IsAssignable<FilterQuery<Author2>, { age: { $gte: number } }>>(true); assert<IsAssignable<FilterQuery<Author2>, { age: { $gte: number }; born: { $lt: string }; $and: [{ name: { $ne: 'John' } }, { name: { $in: ['Ben', 'Paul'] } }] }>>(true); assert<Has<FilterQuery<Author2>, { favouriteBook?: Book2 }>>(true); assert<IsAssignable<FilterQuery<Author2>, { $and: [{ favouriteBook: Book2 }, { name: string }] }>>(true); assert<IsAssignable<FilterQuery<Author2>, { $and: [{ favouriteBook: { title: string } }, { name: string }] }>>(true); assert<IsAssignable<FilterQuery<Author2>, { $and: [{ favouriteBook: string }, { name: string }] }>>(true); // assert<Has<FilterQuery<Author2>, Author2>>(true); assert<Has<FilterQuery<Author2>, number>>(true); assert<Has<FilterQuery<Author2>, { favouriteBook?: ExpandQuery<Book2> }>>(true); // assert<Has<FilterQuery<Book2>, { author: { favouriteBook?: Query<Book2> } }>>(true); // assert<Has<FilterQuery<Book2>, { author: { favouriteBook?: { title?: string } } }>>(true); assert<IsAssignable<FilterQuery<Book2>, { author: { favouriteBook: { tags: FilterValue<BookTag2> } } }>>(true); assert<IsAssignable<FilterQuery<Book2>, { author: { favouriteBook: { tags: BookTag2[] } } }>>(true); assert<IsAssignable<FilterQuery<Book2>, { author: { favouriteBook: { tags: string[] } } }>>(true); assert<IsAssignable<FilterQuery<Book2>, { tags: string[] }>>(true); assert<IsAssignable<FilterQuery<Book2>, { tags: string }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { tags: number[] } }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { tags: bigint[] } }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { tags: string[] } }>>(true); assert<IsAssignable<FilterQuery<Author2>, { books: { tags: boolean[] } }>>(false); }); test('assignment to naked relation, generic reference and identified reference', async () => { interface Publisher { id: number; name: string; } interface Book { id: number; name: string; publisher?: Publisher; publisherRef?: Reference<Publisher>; publisherIdRef?: Ref<Publisher>; } // simulate usage of ORM base entity so `wrap` will return its parameter const book = { __baseEntity: true, toReference: () => ({} as any) } as unknown as Book; const publisher = { __baseEntity: true, toReference: () => ({} as any) } as unknown as Publisher; book.publisher = publisher; // @ts-expect-error book.publisher = wrap(publisher).toReference(); const id = book.publisherIdRef?.id; // @ts-expect-error book.publisherRef = publisher; book.publisherRef = wrap(publisher).toReference(); // @ts-expect-error book.publisherIdRef = publisher; book.publisherIdRef = wrap(publisher).toReference(); // composite keys const compositePks: Primary<FooParam2> = [1, 2]; const compositeRef = {} as Ref<FooParam2>; const bar = compositeRef.bar; const baz = compositeRef.baz; }); test('ObjectQuery with readonly properties (#3836)', async () => { interface Publisher { readonly id: string; readonly name: string; } interface User { readonly id: number; readonly name: string; readonly age?: number; readonly born?: Date; readonly rel?: Publisher; readonly relRef?: Reference<Publisher>; readonly relIdRef?: Ref<Publisher>; readonly rels?: Collection<Publisher>; } let ok01: FilterQuery<User>; ok01 = {}; ok01 = { name: 'foo' }; ok01 = { born: { $gte: new Date() } }; ok01 = { age: { $gte: 1 } }; ok01 = { age: 1 }; ok01 = { rel: 'abc' }; ok01 = { rel: ['abc'] }; ok01 = { relRef: 'abc' }; ok01 = { relRef: ['abc'] }; ok01 = { relIdRef: 'abc' }; ok01 = { relIdRef: ['abc'] }; ok01 = { rels: 'abc' }; ok01 = { rels: ['abc'] }; }); test('FilterQuery with ', async () => { enum ABC { A, B, C, } enum DEF { D = 'd', E = 'e', F = 'f', } interface Publisher { id: string; enum1: 'a' | 'b' | 'c'; enum2: ABC; enum3: DEF; } let query: FilterQuery<Publisher>; query = { enum1: 'a' }; query = { enum1: 'b' }; // @ts-expect-error query = { enum1: 'd' }; query = { enum2: ABC.A }; // @ts-expect-error query = { enum2: 'a' }; query = { enum2: ABC.A }; // @ts-expect-error query = { enum2: DEF.D }; // @ts-expect-error query = { enum3: 'd' }; // @ts-expect-error query = { enum3: ABC.A }; query = { enum3: DEF.D }; }); test('AutoPath with optional nullable properties', async () => { interface MessageRecipient { id: string; message?: Message | null; } interface Message { id: string; phoneService?: PhoneService | null; messageRecipients: Collection<MessageRecipient>; } interface PhoneService { id: string; phoneServiceVendor?: PhoneServiceVendor | null; messages: Collection<Message>; } interface PhoneServiceVendor { id: string; phoneService?: PhoneService; } const em = { findOne: jest.fn() as any } as EntityManager; await em.findOne('MessageRecipient' as EntityName<MessageRecipient>, '1', { populate: ['message', 'message.phoneService', 'message.phoneService.phoneServiceVendor'], }); }); test('RequiredEntityData requires required properties, and allows null only if explicitly used in the property type', async () => { interface User { id: number; email: string; foo?: string; bar: string | null; } let email: RequiredEntityData<User>['email']; email = ''; // @ts-expect-error should not allow `null` on required props email = null; let foo: RequiredEntityData<User>['foo']; foo = ''; // @ts-expect-error should not allow `null` on required props foo = null; let bar: RequiredEntityData<User>['bar']; bar = ''; bar = null; }); test('FilterQuery ok assignments', async () => { let ok01: FilterQuery<Author2>; ok01 = {}; ok01 = { born: '2020-01-01' }; ok01 = { born: { $gte: '2020-01-01' } }; ok01 = { age: { $gte: 1 } }; ok01 = { age: 1 }; ok01 = { favouriteBook: '1' }; ok01 = { favouriteBook: ['1', '2'] }; ok01 = { favouriteBook: null }; ok01 = { books: { author: { born: '2020-01-01' } }, favouriteBook: null }; ok01 = { books: { author: { born: '2020-01-01' } } }; ok01 = { books: { author: { born: '2020-01-01' } }, favouriteBook: {} as Book2 }; ok01 = { books: { author: { born: '2020-01-01' } }, favouriteBook: {} as Book2 }; ok01 = { books: { tags: { name: 'asd' } } }; ok01 = { books: { tags: '1' } }; ok01 = { books: { tags: 1 } }; ok01 = { books: { tags: 1n } }; ok01 = { books: { tags: { books: { title: 'asd' } } } }; ok01 = { name: 'asd' }; ok01 = { $or: [{ name: 'asd' }, { age: 18 }] }; ok01 = [1, 2, 3]; ok01 = [{} as Author2, {} as Author2, {} as Author2]; let ok02: FilterQuery<Book2>; ok02 = { publisher: { $ne: undefined } }; ok02 = { publisher: { name: 'test' } }; ok02 = { author: { born: { $or: ['123'] } } }; ok02 = ['1', '2', '3']; ok02 = [{} as Book2, {} as Book2, {} as Book2]; let ok03: FilterQuery<FooParam2>; ok03 = { bar: 1, baz: 2 }; ok03 = { bar: { name: '1' }, baz: { name: '2' } }; let ok04: FilterQuery<Book2>; ok04 = { publisher: 1 }; ok04 = { publisher: { name: 'name' } }; ok04 = { publisher: { name: /name/ } }; ok04 = { publisher: { name: { $like: 'name' } } }; ok04 = { $and: [{ author: { age: { $gte: 123 } } }] }; let ok05: FilterQuery<FooBar2>; ok05 = { name: '1', array: 1 }; ok05 = { name: '1', array: [1, 2, 3] }; ok05 = { name: '1', array: { $in: [1, 2, 3] } }; const ok06: FilterQuery<Author2> = { name: '...' }; ok06.age = 10; const ok07: FilterQuery<Author2> = {}; ok07.age = 10; ok07.$or = [{ name: '231' }]; }); test('FilterQuery bad assignments', async () => { let fail01: FilterQuery<Author2>; // @ts-expect-error fail01 = { born: 123 }; // @ts-expect-error fail01 = { books: { author: { born: 123 } }, favouriteBook: null }; // @ts-expect-error fail01 = { born: true }; // @ts-expect-error fail01 = { age: { $gta: 1 } }; // @ts-expect-error fail01 = { ago: { $gte: 1 } }; // @ts-expect-error fail01 = { ago: { $gta: 1 } }; // @ts-expect-error fail01 = { favouriteBook: 1 }; // @ts-expect-error fail01 = { favouriteBook: 1 }; // @ts-expect-error fail01 = { favouriteBook: [1, '2'] }; // @ts-expect-error fail01 = { favouriteBook: [1, 2] }; // @ts-expect-error fail01 = { books: { tags: { name: 1 } } }; // @ts-expect-error fail01 = { books: { tags: true } }; // @ts-expect-error fail01 = { books: { tags: { books: { title: 123 } } } }; let fail02: FilterQuery<Book2>; // @ts-expect-error fail02 = { author: { born: 123 } }; // @ts-expect-error fail02 = { author: { born: [123] } }; // @ts-expect-error fail02 = { author: { born: { $in: [123] } } }; }); test('Loaded<T> type is assignable to T', async () => { let b1 = {} as Book2; b1 = {} as Loaded<Book2>; let b2 = {} as Book2; b2 = {} as Loaded<Book2, 'publisher'>; function test<T>() { let b3 = {} as T; b3 = {} as Loaded<T, 'publisher'>; } }); function createEntity<T>(): T { const ret = {} as any; ret.__helper = ret; ret.toObject = () => ({}); return ret; } test('Loaded type with EntityDTO (no base entities)', async () => { const b1 = createEntity<Loaded<Book2>>(); const o1 = wrap(b1).toObject(); // @ts-expect-error o1.publisher is now just number, as it's not populated const id1 = o1.publisher?.id; const b2 = createEntity<Loaded<Book2, 'publisher'>>(); const o2 = wrap(b2).toObject(); const id2 = o2.publisher?.id; // @ts-expect-error Book2 should not have methods from base entity const o22 = b2.toObject(); }); test('Loaded type with EntityDTO (with ORM base entities)', async () => { const b1 = createEntity<Loaded<Book>>(); const o11 = wrap(b1).toObject(); const o12 = b1.toObject(['id']); // @ts-expect-error const id10 = o12.id; // @ts-expect-error o11.publisher is now just number, as it's not populated const id11 = o11.publisher?.id; // @ts-expect-error o12.publisher is now just number, as it's not populated const id12 = o12.publisher?.id; const b2 = createEntity<Loaded<Book, 'publisher'>>(); const o21 = wrap(b2).toObject(); const o22 = b2.toObject(); const id21 = o21.publisher?.id; const id22 = o22.publisher?.id; assert<IsExact<typeof id21, string | undefined>>(true); assert<IsExact<typeof id22, string | undefined>>(true); }); test('Loaded type and assignability with extending the ORM BaseEntity (#3865)', async () => { interface MemberNotification extends BaseEntity { id: string; notification?: Ref<Notification>; } interface Notification extends BaseEntity { id: string; } const test: MemberNotification = {} as Loaded<MemberNotification, 'notification'>; }); test('inference of entity type', async () => { interface MemberNotification { id: string; notification?: Ref<Notification>; } interface Notification { id: string; } const em = { findOne: jest.fn() as any } as EntityManager; const res: Loaded<MemberNotification> | null = await em.findOne('MemberNotification' as EntityName<MemberNotification>, {} as MemberNotification | string); }); test('Ref.load() returns Loaded type (#3755)', async () => { interface Parent { id: number; children: Collection<Child>; } interface Child { id: number; parent: Ref<Parent>; } const parent = { loadOrFail: jest.fn() as any } as Ref<Parent>; // @ts-expect-error Loaded<Parent, never> is not assignable const populated01: Loaded<Parent, 'children'> = {} as Loaded<Ref<Parent>>; // @ts-expect-error Loaded<Parent, never> is not assignable const populated02: Loaded<Parent, 'children'> = {} as Loaded<Parent>; function foo(e: Loaded<Parent, 'children'>) { // } const e = await parent.loadOrFail(); // @ts-expect-error Loaded<Parent, never> is not assignable foo(e); const populated1: Loaded<Parent, 'children'> = await parent.loadOrFail(); const populated22 = await parent.loadOrFail({ populate: [] }); // @ts-expect-error Loaded<Parent, never> is not assignable const populated2: Loaded<Parent, 'children'> = populated22; // only this should pass const populated3: Loaded<Parent, 'children'> = await parent.loadOrFail({ populate: ['children'] }); const populated4: Loaded<Parent, 'children'> = await parent.loadOrFail({ populate: ['children', 'id'] }); const populated5: Loaded<Parent, 'children'> = await parent.loadOrFail({ populate: ['children.parent'] }); }); test('assignability of Loaded<T> to Ref<T> via ref() helper', async () => { interface Node extends BaseEntity { id: string; } interface Author extends Node { name: string; } interface Publisher extends Node { foundingAuthor: Ref<Author>; publishedBooks: Collection<Book>; } interface Book extends Node { publishedBy: Ref<Publisher>; } const circularReferenceBook = {} as Loaded<Book, 'publishedBy.foundingAuthor'>; const circularReference: Ref<Book> = ref(circularReferenceBook); }); test('exclusion', async () => { interface Notification { id: string; readonly foo: number; getBar(): string; get bar(): string; [OptionalProps]?: 'bar'; } let q: FilterQuery<Notification>; q = { foo: 123 }; q = { bar: '' }; // getter is still a property, only functions and symbols are excluded // @ts-expect-error q = { getBar: () => '' }; // @ts-expect-error q = { [OptionalProps]: 'bar' }; }); test('tuple type after entity serializized', async () => { assert<IsExact<EntityDTO<Book>['point'], [number, number] | undefined>>(true); assert<IsExact<EntityDTO<Author>['age'], number | undefined>>(true); }); test('GH #3277', async () => { interface Owner { id: number; vehicles: Collection<Vehicle>; vehicle: Ref<Vehicle>; } interface Manufacturer { id: number; } interface Type { id: number; owner: Ref<Owner>; } interface Vehicle { id: number; owner: Ref<Owner>; manufacturer: Ref<Manufacturer>; type: Ref<Type>; } function preloaded1(owner: Loaded<Owner, 'vehicles'>) { // const a: number = owner.vehicles.$[0].type.$.owner.id; } function preloaded2(owner: Loaded<Owner, 'vehicles.type'>) { // const a: number = owner.vehicles.$[0].type.$.owner.id; } const owner1 = {} as Loaded<Owner, 'vehicles.manufacturer' | 'vehicles.type'>; const owner2 = {} as Loaded<Owner, 'vehicles.type.owner.vehicles'>; const owner3 = {} as Loaded<Owner, 'vehicle'>; const owner4 = {} as Loaded<Owner>; preloaded1(owner1); preloaded1(owner2); // @ts-expect-error preloaded1(owner3); // @ts-expect-error preloaded1(owner4); preloaded2(owner1); preloaded2(owner2); // @ts-expect-error preloaded2(owner3); // @ts-expect-error preloaded2(owner4); // const foo = await owner1.vehicles.$.loadItems({ populate: ['owner'] }); // const v1 = foo[0].owner.$.vehicles; }); test('GH #3277 (2)', async () => { interface Person { id: number; foobar: Collection<FooBar>; } interface Calendar { id: number; events: Collection<CalendarEvent>; owner: Ref<Person>; } interface FooBar { id: number; } interface CalendarEvent { id: number; order: Ref<Order>; calendar: Ref<Calendar>; } interface Order { id: number; customer: Ref<Customer>; foobar: Collection<FooBar>; } interface Customer { foobar: Collection<FooBar>; } function preloaded(event: Loaded<CalendarEvent, 'calendar.owner'>) { // no-op } const event1 = {} as Loaded<CalendarEvent, 'calendar.owner' | 'order.customer.foobar'>; const event2 = {} as Loaded<CalendarEvent, 'calendar.owner.foobar' | 'order.customer'>; const event3 = {} as Loaded<CalendarEvent, 'calendar.owner' | 'order.customer'>; const event4 = {} as Loaded<CalendarEvent, 'calendar.owner' | 'order'>; const event5 = {} as Loaded<CalendarEvent, 'calendar.owner' | 'order.foobar'>; preloaded(event1); preloaded(event2); preloaded(event3); preloaded(event4); preloaded(event5); }); test('GH #3277 (3)', async () => { interface ChildModel { id: string; grandChild: Ref<GrandChildModel>; } interface GrandChildModel { id: string; children: Collection<ChildModel>; } interface ParentModel { id: string; child: Ref<ChildModel>; } const childModel = {} as Loaded<ChildModel, 'grandChild'>; const parentModel = {} as ParentModel; parentModel.child = ref(childModel); const secondParentModel = {} as Loaded<ParentModel, 'child.grandChild'>; secondParentModel.child = ref(childModel); }); test('GH #4962', async () => { interface AbstractEntity extends BaseEntity { id: number; status?: string; } abstract class AbstractRepository< Entity extends AbstractEntity > extends EntityRepository<Entity> { async countWaiting() { return this.find({ status: 'waiting', } as FilterQuery<Entity>); } } }); test('GH #5006', async () => { interface User { id: number; name: string; } interface UserRepository extends EntityRepository<User> { test(): number; } const schema = new EntitySchema<User>({ name: 'User', repository: () => ({} as Constructor<UserRepository>), properties: { id: { type: 'number', primary: true }, name: { type: 'string' }, }, }); }); test('GH #5186', async () => { interface User { id: number; name: string | null; } type UserDTO = EntityDTO<User>; const dto1: UserDTO = { id: 1, name: null }; // @ts-expect-error const dto2: UserDTO = { id: 1, name: undefined }; }); });