I've been having a tough time getting child entities to work automatically with a REST api.
I have a base class:
class Block {
@PrimaryGeneratedColumn('uuid')
public id: string;
@Column()
public type: string;
}
Then have extended this to other block types, for instance:
@Entity('sites_blocks_textblock')
class TextBlock extends Block {
@Column()
public text: string;
}
I made each block type their own entity so the columns would serialize to the database properly, and have validations on each property.
So... I have 10+ block types, and I am trying to avoid a separate Controller and endpoints to CRUD each block type. I would just like one BlockController, one /block endpoint, POSTing to create, and PUT on /block/:id for update, where it can infer the type of the block from the request's 'type' body parameter.
The problem is that in the request, the last @Body() parameter won't validate (request won't go through) unless I use type 'any'... because each custom block type is passing it's extra/custom properties. Otherwise I would have to use each specific Block child class as the parameter type, requiring custom methods for each type.
To achieve this I'm trying to use a custom validation Pipe and generics, where I can look at the incoming 'type' body parameter, and cast or instantiate the incoming data as a specific Block type.
Controller handler:
@Post()
@UseGuards(PrincipalGuard)
public create(@Principal() principal: User,
@Param('siteId', ParseUUIDPipe) siteId: string,
@Body(new BlockValidationPipe()) blockCreate: any): Promise<Block> {
return this.blockService.create(principal.organization, siteId, blockCreate);
}
BlockValidationPipe (this is supposed to cast the incoming data object as a specific block type, and then validate it, return the incoming data object as that type):
@Injectable()
export class BlockValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (value.type) {
if (value.type.id) {
metatype = getBlockTypeFromId(value.type.id);
}
}
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// MAGIC: ==========>
let object = objectToBlockByType(value, value.type.id, metatype);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException(errors, 'Validation failed');
}
return object ? object : value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
using this helper (but it might not be working exactly as intended, haven't gotten passing types down totally):
function castOrNull<C extends Block>(value: C, type): C | null {
return value as typeof type;
}
export function objectToBlockByType(object, typeId, metatype) {
switch(typeId) {
case 'text':
return castOrNull<TextBlock>(object, TextBlock);
case 'avatar':
return castOrNull<AvatarBlock>(object, AvatarBlock);
case 'button':
return castOrNull<ButtonBlock>(object, ButtonBlock);
// etc....
default:
return castOrNull<Block>(object, Block);
}
}
... That's all supposed to just give me a proper Block subclass instantiation for the controller to use, but I'm not sure how to pass this specific subclass type to the underlying service calls to update the specific block repositories for each entity type. Is this possible to do using generics?
For Instance, in BlockService, but I should pass the specific block type (TextBlock, ButtonBlock, etc) to the repository.save() method, so that it will serialize the sub-class types to their respective tables properly. I'm assuming this is possible to do, but someone please correct me if I'm wrong here...
Am trying to do this, where I pass the block data as its Block parent type, and try to then get its specific class type to pass to save, but it's not working...
public async create(organization: Organization, siteId: string, blockCreate: Block): Promise<Block> {
let blockType: Type<any> = getBlockTypeFromId(blockCreate.type.id);
console.log("create block", typeof blockCreate, blockCreate.constructor.name, blockCreate, typeof blockType, blockType);
///
let r = await this.blockRepository.save<typeof blockCreate>({
organization: organization,
site: await this.siteService.getByIdAndOrganization(siteId, organization),
type: await this.blockTypeService.getById(blockCreate.type.id),
...blockCreate
});
//r.data = JSON.parse(r.data);
return r;
}
Problem here is that the 'typeof blockCreate' always returns 'object', I have to call 'blockCreate.constructor.name' to get the proper subclass block type name, but can't pass this as a type T.
So I'm wondering... is there anyway to return the subclass Type T parameter all the way from the controller helper (where it is supposed to cast and validate the subtype) to the repository so I can pass this type T to the save(entity) call... and have that commit it properly? Or is there any other way to get this type T from the object instance itself, if 'typeof block' isn't returning the specific subclass type? I don't think it is possible to do the former during compile time... ?
I'm really just trying to get subclass serialization and validation working with just hopefully one set of controller endpoints, and service layer/repository calls... Should I be looking into Partial entities?
Anyone know of any direction I can look to accomplish this?