Skip to content

Commit

Permalink
feat(core): add HiddenProps symbol as type-level companion for `hid…
Browse files Browse the repository at this point in the history
…den: true`

Related: #4093
  • Loading branch information
B4nan committed Sep 20, 2023
1 parent 8e80260 commit c0e94b9
Show file tree
Hide file tree
Showing 13 changed files with 55 additions and 16 deletions.
7 changes: 7 additions & 0 deletions docs/docs/defining-entities.md
Expand Up @@ -1053,6 +1053,8 @@ values={[
@Entity()
export class User {

[HiddenProps]?: 'firstName' | 'lastName';

@Property({ hidden: true })
firstName!: string;

Expand All @@ -1079,6 +1081,8 @@ export class User {
@Entity()
export class User {

[HiddenProps]?: 'firstName' | 'lastName';

@Property({ hidden: true })
firstName!: string;

Expand All @@ -1103,6 +1107,9 @@ export class User {

```ts title="./entities/User.ts"
export class User {

[HiddenProps]?: 'firstName' | 'lastName';

firstName!: string;
lastName!: string;

Expand Down
10 changes: 9 additions & 1 deletion docs/docs/serializing.md
Expand Up @@ -37,19 +37,27 @@ export class Book {
## Hidden Properties

If you want to omit some properties from serialized result, you can mark them with `hidden` flag on `@Property()` decorator:
If you want to omit some properties from serialized result, you can mark them with `hidden` flag on `@Property()` decorator. To have this information available on the type level, you also need to use the `HiddenProps` symbol:

```ts
@Entity()
export class Book {

// we use the `HiddenProps` symbol to define hidden properties on type level
[HiddenProps]?: 'hiddenField' | 'otherHiddenField';

@Property({ hidden: true })
hiddenField = Date.now();

@Property({ hidden: true, nullable: true })
otherHiddenField?: string;

}

const book = new Book(...);
console.log(wrap(book).toObject().hiddenField); // undefined

// @ts-expect-error accessing `hiddenField` will fail to compile thanks to the `HiddenProps` symbol
console.log(wrap(book).toJSON().hiddenField); // undefined
```

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/entity/WrappedEntity.ts
Expand Up @@ -74,7 +74,7 @@ export class WrappedEntity<Entity extends object> {
}

toPOJO(): EntityDTO<Entity> {
return EntityTransformer.toObject(this.entity, [], true);
return EntityTransformer.toObject(this.entity, [], true) as EntityDTO<Entity>;
}

toJSON(...args: any[]): EntityDictionary<Entity> {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Expand Up @@ -7,7 +7,7 @@ export {
Constructor, ConnectionType, Dictionary, PrimaryKeyProp, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter,
AnyEntity, EntityClass, EntityProperty, EntityMetadata, QBFilterQuery, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator,
GetRepository, EntityRepositoryType, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, MigrationDiff, GenerateOptions, FilterObject,
IEntityGenerator, ISeedManager, EntityClassGroup, OptionalProps, EagerProps, RequiredEntityData, CheckCallback, SimpleColumnMeta, Rel, Ref, ISchemaGenerator,
IEntityGenerator, ISeedManager, EntityClassGroup, OptionalProps, EagerProps, HiddenProps, RequiredEntityData, CheckCallback, SimpleColumnMeta, Rel, Ref, ISchemaGenerator,
UmzugMigration, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, FilterKey,
} from './typings';
export * from './enums';
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/serialization/EntitySerializer.ts
@@ -1,6 +1,7 @@
import type { Collection } from '../entity/Collection';
import type {
AutoPath,
Dictionary,
EntityDTO,
EntityDTOProp,
EntityKey,
Expand Down Expand Up @@ -62,7 +63,7 @@ export class EntitySerializer {
}

const root = wrapped.__serializationContext.root!;
const ret = {} as EntityDTO<T>;
const ret = {} as Dictionary;
const keys = new Set<EntityKey<T>>(meta.primaryKeys);
Utils.keys(entity as object).forEach(prop => keys.add(prop));
const visited = root.visited.has(entity);
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/serialization/EntityTransformer.ts
@@ -1,5 +1,5 @@
import type { Collection } from '../entity/Collection';
import type { AnyEntity, EntityDTO, EntityKey, EntityMetadata, EntityValue, IPrimaryKey } from '../typings';
import type { AnyEntity, Dictionary, EntityDTO, EntityKey, EntityMetadata, EntityValue, IPrimaryKey } from '../typings';
import { helper, wrap } from '../entity/wrap';
import type { Platform } from '../platforms';
import { Utils } from '../utils/Utils';
Expand Down Expand Up @@ -33,7 +33,7 @@ export class EntityTransformer {

const root = wrapped.__serializationContext.root!;
const meta = wrapped.__meta;
const ret = {} as EntityDTO<Entity>;
const ret = {} as Dictionary;
const keys = new Set<EntityKey<Entity>>();

if (meta.serializedPrimaryKey && !meta.compositePK) {
Expand Down Expand Up @@ -78,20 +78,20 @@ export class EntityTransformer {
return [prop, val] as const;
})
.filter(([, value]) => typeof value !== 'undefined')
.forEach(([prop, value]) => ret[this.propertyName(meta, prop!, wrapped.__platform)] = value as any);
.forEach(([prop, value]) => ret[this.propertyName(meta, prop!, wrapped.__platform) as any] = value as any);

if (!visited) {
root.visited.delete(entity);
}

if (!wrapped.isInitialized() && wrapped.hasPrimaryKey()) {
return ret;
return ret as EntityDTO<Entity>;
}

// decorated getters
meta.props
.filter(prop => prop.getter && !prop.hidden && typeof entity[prop.name] !== 'undefined')
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = entity[prop.name] as any);
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform) as any] = entity[prop.name] as any);

// decorated get methods
meta.props
Expand All @@ -102,7 +102,7 @@ export class EntityTransformer {
root.close();
}

return ret;
return ret as EntityDTO<Entity>;
}

private static propertyName<Entity>(meta: EntityMetadata<Entity>, prop: EntityKey<Entity>, platform?: Platform): EntityKey<Entity> {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/typings.ts
Expand Up @@ -45,6 +45,7 @@ export const EntityRepositoryType = Symbol('EntityRepositoryType');
export const PrimaryKeyProp = Symbol('PrimaryKeyProp');
export const OptionalProps = Symbol('OptionalProps');
export const EagerProps = Symbol('EagerProps');
export const HiddenProps = Symbol('HiddenProps');

export type UnwrapPrimary<T> = T extends Scalar
? T
Expand Down Expand Up @@ -246,7 +247,9 @@ export type EntityDTOProp<T> = T extends Scalar
: T extends Relation<T>
? EntityDTONested<T>
: T;
export type EntityDTO<T> = { [K in EntityKey<T>]: EntityDTOProp<T[K]> };
type ExtractHiddenProps<T> = T extends { [HiddenProps]?: infer Prop } ? Prop : never;
type ExcludeHidden<T, K extends keyof T> = K extends ExtractHiddenProps<T> ? never : K;
export type EntityDTO<T> = { [K in EntityKey<T> as ExcludeHidden<T, K>]: EntityDTOProp<T[K]> };

export type CheckCallback<T> = (columns: Record<keyof T, string>) => string;

Expand Down
1 change: 1 addition & 0 deletions tests/EntityHelper.mongo.test.ts
Expand Up @@ -27,6 +27,7 @@ describe('EntityHelperMongo', () => {
test('#toObject() should ignore properties marked with hidden flag', async () => {
const test = Test.create('Bible');
expect(test.hiddenField).toBeDefined();
// @ts-expect-error
expect(wrap(test).toJSON().hiddenField).not.toBeDefined();
});

Expand Down
6 changes: 5 additions & 1 deletion tests/EntityManager.mysql.test.ts
Expand Up @@ -1785,7 +1785,11 @@ describe('EntityManagerMySql', () => {
const b1 = (await orm.em.findOne(FooBar2, { id: bar.id }))!;
expect(b1).toBe(b1.fooBar);
expect(b1.id).not.toBeNull();
expect(wrap(b1).toJSON()).toMatchObject({ fooBar: b1.id });
expect(wrap(b1).toJSON().fooBar).toBe(b1.id);
// @ts-expect-error
expect(wrap(b1).toJSON().foo).toBeUndefined();
// @ts-expect-error
expect(wrap(b1).toJSON().bar).toBeUndefined();
});

test('persisting entities in parallel inside forked EM with copied IM', async () => {
Expand Down
4 changes: 3 additions & 1 deletion tests/entities-schema/Author4.ts
@@ -1,5 +1,5 @@
import type { Collection, EventArgs } from '@mikro-orm/core';
import { EntitySchema, DateType, TimeType, BooleanType, t, ReferenceKind, wrap } from '@mikro-orm/core';
import { EntitySchema, DateType, TimeType, BooleanType, t, ReferenceKind, HiddenProps } from '@mikro-orm/core';
import type { IBaseEntity5 } from './BaseEntity5';
import type { IBook4 } from './Book4';

Expand All @@ -23,6 +23,8 @@ function randomHook(args: EventArgs<IAuthor4>) {

export class Identity {

[HiddenProps]?: 'foo' | 'bar';

constructor(public foo: string, public bar: number) {}

get fooBar() {
Expand Down
4 changes: 3 additions & 1 deletion tests/entities/test.model.ts
@@ -1,8 +1,10 @@
import { Entity, PrimaryKey, Property, SerializedPrimaryKey } from '@mikro-orm/core';
import { Entity, HiddenProps, PrimaryKey, Property, SerializedPrimaryKey } from '@mikro-orm/core';

@Entity()
export class Test {

[HiddenProps]?: 'hiddenField';

@PrimaryKey({ type: 'ObjectId' })
_id: any;

Expand Down
6 changes: 5 additions & 1 deletion tests/features/result-cache/GH3294.test.ts
@@ -1,10 +1,12 @@
import { Entity, MikroORM, PrimaryKey, Property, wrap } from '@mikro-orm/core';
import { Entity, MikroORM, PrimaryKey, Property, wrap, HiddenProps } from '@mikro-orm/core';
import { mockLogger } from '../../helpers';
import { BetterSqliteDriver } from '@mikro-orm/better-sqlite';

@Entity()
export class EntityWithHiddenProp {

[HiddenProps]?: 'hiddenProp';

@PrimaryKey()
id!: number;

Expand Down Expand Up @@ -55,7 +57,9 @@ describe('hidden properties are still included when cached (GH 3294)', () => {
expect(res1.hiddenProp).toStrictEqual(res2.hiddenProp);

// Expect hidden prop to still be hidden when using `toJSON`
// @ts-expect-error
expect(wrap(res1).toJSON().hiddenProp).toBeUndefined();
// @ts-expect-error
expect(wrap(res2).toJSON().hiddenProp).toBeUndefined();
});

Expand Down
9 changes: 8 additions & 1 deletion tests/issues/GH3429.test.ts
@@ -1,8 +1,10 @@
import { MikroORM, Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/sqlite';
import { MikroORM, Embeddable, Embedded, Entity, PrimaryKey, Property, HiddenProps, wrap } from '@mikro-orm/sqlite';

@Embeddable()
class Address {

[HiddenProps]?: 'addressLine1' | 'addressLine2';

@Property({ hidden: true })
addressLine1!: string;

Expand Down Expand Up @@ -60,4 +62,9 @@ test('embeddable serialization flags', async () => {

expect(JSON.stringify(org)).toBe(`{"id":1,"address":{"city":"city 1","country":"country 1","address":"l1 l2"}}`);
expect(JSON.stringify([org])).toBe(`[{"id":1,"address":{"city":"city 1","country":"country 1","address":"l1 l2"}}]`);

// @ts-expect-error
expect(wrap(org).toObject().address.addressLine1).toBeUndefined();
// @ts-expect-error
expect(wrap(org).toObject().address.addressLine2).toBeUndefined();
});

0 comments on commit c0e94b9

Please sign in to comment.