Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/interfaces/create-many-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CreateOneOptions } from './create-one-options.interface'

export type CreateManyOptions<DTO> = CreateOneOptions<DTO>
9 changes: 9 additions & 0 deletions packages/core/src/interfaces/create-one-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Filter } from './filter.interface'

export interface CreateOneOptions<DTO> {
/**
* Additional filter applied to the input dto before creation. This could be used to apply an additional filter to ensure
* that the entity being created belongs to a particular user.
*/
filter?: Filter<DTO>
}
2 changes: 2 additions & 0 deletions packages/core/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export * from './aggregate-options.interface'
export * from './aggregate-query.interface'
export * from './aggregate-response.interface'
export * from './count-options.interface'
export * from './create-many-options.interface'
export * from './create-one-options.interface'
export * from './delete-many-options.interface'
export * from './delete-many-response.interface'
export * from './delete-one-options.interface'
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/services/assembler-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
AggregateQuery,
AggregateResponse,
CountOptions,
CreateManyOptions,
CreateOneOptions,
DeleteManyResponse,
DeleteOneOptions,
Filter,
Expand Down Expand Up @@ -39,15 +41,15 @@ export class AssemblerQueryService<DTO, Entity, C = DeepPartial<DTO>, CE = DeepP
)
}

public async createMany(items: C[]): Promise<DTO[]> {
public async createMany(items: C[], opts?: CreateManyOptions<DTO>): Promise<DTO[]> {
const { assembler } = this
const converted = await assembler.convertToCreateEntities(items)
return this.assembler.convertToDTOs(await this.queryService.createMany(converted))
return this.assembler.convertToDTOs(await this.queryService.createMany(converted, this.convertFilterable(opts)))
}

public async createOne(item: C): Promise<DTO> {
public async createOne(item: C, opts?: CreateOneOptions<DTO>): Promise<DTO> {
const c = await this.assembler.convertToCreateEntity(item)
return this.assembler.convertToDTO(await this.queryService.createOne(c))
return this.assembler.convertToDTO(await this.queryService.createOne(c, this.convertFilterable(opts)))
}

public async deleteMany(filter: Filter<DTO>): Promise<DeleteManyResponse> {
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/services/noop-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
AggregateQuery,
AggregateResponse,
CountOptions,
CreateManyOptions,
CreateOneOptions,
DeleteManyOptions,
DeleteManyResponse,
DeleteOneOptions,
Expand Down Expand Up @@ -43,11 +45,11 @@ export class NoOpQueryService<DTO, C = DeepPartial<DTO>, U = DeepPartial<DTO>> i
return Promise.reject(new NotImplementedException('addRelations is not implemented'))
}

public createMany(items: C[]): Promise<DTO[]> {
public createMany(items: C[], opts?: CreateManyOptions<DTO>): Promise<DTO[]> {
return Promise.reject(new NotImplementedException('createMany is not implemented'))
}

public createOne(item: C): Promise<DTO> {
public createOne(item: C, opts?: CreateOneOptions<DTO>): Promise<DTO> {
return Promise.reject(new NotImplementedException('createOne is not implemented'))
}

Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/services/proxy-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
AggregateQuery,
AggregateResponse,
CountOptions,
CreateManyOptions,
CreateOneOptions,
DeleteManyResponse,
DeleteOneOptions,
Filter,
Expand Down Expand Up @@ -172,12 +174,12 @@ export class ProxyQueryService<DTO, C = DeepPartial<DTO>, U = DeepPartial<DTO>>
return this.proxied.findRelation(RelationClass, relationName, dto, opts)
}

public createMany(items: C[]): Promise<DTO[]> {
return this.proxied.createMany(items)
public createMany(items: C[], opts?: CreateManyOptions<DTO>): Promise<DTO[]> {
return this.proxied.createMany(items, opts)
}

public createOne(item: C): Promise<DTO> {
return this.proxied.createOne(item)
public createOne(item: C, opts?: CreateOneOptions<DTO>): Promise<DTO> {
return this.proxied.createOne(item, opts)
}

public async deleteMany(filter: Filter<DTO>): Promise<DeleteManyResponse> {
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/services/query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
AggregateQuery,
AggregateResponse,
CountOptions,
CreateManyOptions,
CreateOneOptions,
DeleteManyOptions,
DeleteManyResponse,
DeleteOneOptions,
Expand Down Expand Up @@ -243,17 +245,19 @@ export interface QueryService<DTO, C = DeepPartial<DTO>, U = DeepPartial<DTO>> {
* Create a single record.
*
* @param item - the record to create.
* @param opts - Additional opts to apply when creating one entity.
* @returns the created record.
*/
createOne(item: C): Promise<DTO>
createOne(item: C, opts?: CreateOneOptions<DTO>): Promise<DTO>

/**
* Creates a multiple record.
*
* @param items - the records to create.
* @param opts - Additional opts to apply when creating many entities.
* @returns a created records.
*/
createMany(items: C[]): Promise<DTO[]>
createMany(items: C[], opts?: CreateManyOptions<DTO>): Promise<DTO[]>

/**
* Update one record.
Expand Down
10 changes: 4 additions & 6 deletions packages/query-graphql/src/resolvers/create.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,10 @@ export const Creatable =
@AuthorizerFilter({
operationGroup: OperationGroup.CREATE,
many: false
}) // eslint-disable-next-line @typescript-eslint/no-unused-vars
})
authorizeFilter?: Filter<DTO>
): Promise<DTO> {
// Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException
const created = await this.service.createOne(input.input.input)
const created = await this.service.createOne(input.input.input, { filter: authorizeFilter ?? {} })
if (enableOneSubscriptions) {
await this.publishCreatedEvent(created, authorizeFilter)
}
Expand All @@ -159,11 +158,10 @@ export const Creatable =
@AuthorizerFilter({
operationGroup: OperationGroup.CREATE,
many: true
}) // eslint-disable-next-line @typescript-eslint/no-unused-vars
})
authorizeFilter?: Filter<DTO>
): Promise<DTO[]> {
// Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException
const created = await this.service.createMany(input.input.input)
const created = await this.service.createMany(input.input.input, { filter: authorizeFilter ?? {} })
if (enableManySubscriptions) {
await Promise.all(created.map((c) => this.publishCreatedEvent(c, authorizeFilter)))
}
Expand Down
13 changes: 10 additions & 3 deletions packages/query-mongoose/src/services/mongoose-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { NotFoundException } from '@nestjs/common'
import {
AggregateQuery,
AggregateResponse,
applyFilter,
CreateOneOptions,
DeepPartial,
DeleteManyResponse,
DeleteOneOptions,
Expand Down Expand Up @@ -132,9 +134,13 @@ export class MongooseQueryService<Entity extends Document>
* const todoItem = await this.service.createOne({title: 'Todo Item', completed: false });
* ```
* @param record - The entity to create.
* @param opts - Additional options.
*/
async createOne(record: DeepPartial<Entity>): Promise<Entity> {
async createOne(record: DeepPartial<Entity>, opts?: CreateOneOptions<Entity>): Promise<Entity> {
this.ensureIdIsNotPresent(record)
if (opts?.filter && !applyFilter(record as Entity, opts.filter)) {
throw new Error('Entity does not meet creation constraints')
}
Comment on lines +141 to +143
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just doing some type casting here to make this happy but I think raises a good question. What should be the behaviour for a filter like createdAt > "last_year" when the createdAt is populated by the db?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something we could do, but that would make it all a lot more complex is by doing inside a TypeORM query runner, doing the insert, getting the record, apply filter (can even then be done with the normal builder), if failed revert and throw error.

Otherwise I think this would also need to be documented as a limitation. (Preferred)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's my preference too.

return this.Model.create(record)
}

Expand All @@ -149,10 +155,11 @@ export class MongooseQueryService<Entity extends Document>
* ]);
* ```
* @param records - The entities to create.
* @param opts - Additional options.
*/
public async createMany(records: DeepPartial<Entity>[]): Promise<Entity[]> {
public async createMany(records: DeepPartial<Entity>[], opts?: CreateOneOptions<Entity>): Promise<Entity[]> {
records.forEach((r) => this.ensureIdIsNotPresent(r))
return this.Model.create(records)
return this.Model.create(opts?.filter ? applyFilter(records as Entity[], opts.filter) : records)
}

/**
Expand Down
16 changes: 13 additions & 3 deletions packages/query-sequelize/src/services/sequelize-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NotFoundException } from '@nestjs/common'
import {
AggregateQuery,
AggregateResponse,
applyFilter,
CreateOneOptions,
DeepPartial,
DeleteManyResponse,
DeleteOneOptions,
Expand Down Expand Up @@ -129,9 +131,13 @@ export class SequelizeQueryService<Entity extends Model<Entity, Partial<Entity>>
* const todoItem = await this.service.createOne({title: 'Todo Item', completed: false });
* ```
* @param record - The entity to create.
* @param opts - Additional options.
*/
public async createOne(record: DeepPartial<Entity>): Promise<Entity> {
public async createOne(record: DeepPartial<Entity>, opts?: CreateOneOptions<Entity>): Promise<Entity> {
await this.ensureEntityDoesNotExist(record)
if (opts?.filter && !applyFilter(record as Entity, opts.filter)) {
throw new Error('Entity does not meet creation constraints')
}
const changedValues = this.getChangedValues(record)
return this.model.create<Entity>(changedValues as MakeNullishOptional<Entity>)
}
Expand All @@ -147,11 +153,15 @@ export class SequelizeQueryService<Entity extends Model<Entity, Partial<Entity>>
* ]);
* ```
* @param records - The entities to create.
* @param opts - Additional options.
*/
public async createMany(records: DeepPartial<Entity>[]): Promise<Entity[]> {
public async createMany(records: DeepPartial<Entity>[], opts?: CreateOneOptions<Entity>): Promise<Entity[]> {
await Promise.all(records.map((r) => this.ensureEntityDoesNotExist(r)))
const filteredRecords = opts?.filter ? applyFilter(records as Entity[], opts.filter) : records

return this.model.bulkCreate<Entity>(records.map((r) => this.getChangedValues(r) as MakeNullishOptional<Entity>))
return this.model.bulkCreate<Entity>(
filteredRecords.map((r) => this.getChangedValues(r as DeepPartial<Entity>) as MakeNullishOptional<Entity>)
)
}

/**
Expand Down
13 changes: 10 additions & 3 deletions packages/query-typegoose/src/services/typegoose-query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { NotFoundException } from '@nestjs/common'
import {
AggregateQuery,
AggregateResponse,
applyFilter,
CreateManyOptions,
CreateOneOptions,
DeepPartial,
DeleteManyResponse,
DeleteOneOptions,
Expand Down Expand Up @@ -116,9 +119,13 @@ export class TypegooseQueryService<Entity extends Base> extends ReferenceQuerySe
* const todoItem = await this.service.createOne({title: 'Todo Item', completed: false });
* ```
* @param record - The entity to create.
* @param opts - Additional options.
*/
async createOne(record: DeepPartial<Entity>): Promise<DocumentType<Entity>> {
async createOne(record: DeepPartial<Entity>, opts?: CreateOneOptions<Entity>): Promise<DocumentType<Entity>> {
this.ensureIdIsNotPresent(record)
if (opts?.filter && !applyFilter(record as Entity, opts.filter)) {
throw new Error('Entity does not meet creation constraints')
}
const doc = await this.Model.create(record)
return doc
}
Expand All @@ -135,9 +142,9 @@ export class TypegooseQueryService<Entity extends Base> extends ReferenceQuerySe
* ```
* @param records - The entities to create.
*/
async createMany(records: DeepPartial<Entity>[]): Promise<DocumentType<Entity>[]> {
async createMany(records: DeepPartial<Entity>[], opts?: CreateManyOptions<Entity>): Promise<DocumentType<Entity>[]> {
records.forEach((r) => this.ensureIdIsNotPresent(r))
const entities = await this.Model.create(records)
const entities = await this.Model.create(opts?.filter ? applyFilter(records as Entity[], opts.filter) : records)
return entities
}

Expand Down
18 changes: 11 additions & 7 deletions packages/query-typeorm/src/services/typeorm-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
AggregateOptions,
AggregateQuery,
AggregateResponse,
applyFilter,
Class,
CountOptions,
CreateOneOptions,
DeepPartial,
DeleteManyOptions,
DeleteManyResponse,
Expand Down Expand Up @@ -190,12 +192,15 @@ export class TypeOrmQueryService<Entity>
* const todoItem = await this.service.createOne({title: 'Todo Item', completed: false });
* ```
* @param record - The entity to create.
* @param opts - Additional options.
*/
public async createOne(record: DeepPartial<Entity>): Promise<Entity> {
public async createOne(record: DeepPartial<Entity>, opts?: CreateOneOptions<Entity>): Promise<Entity> {
const entity = await this.ensureIsEntityAndDoesNotExist(record)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (opts?.filter && !applyFilter(entity, opts.filter)) {
throw new Error('Entity does not meet creation constraints')
}
Comment on lines +200 to +202
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I'm thinking we should remove this check and only pass it down to the service, so that, when users want to they can use it there, main reason being: I would not want the service even beign called when the user does not have the right access (that's why on our API's we are using the before create).

This would also make it a breaking change so also not really in favor of that.

Would it work for you if we do pass it down (as that is a good idea to do!) but do not check the filter/throw a error? Especially as applyFilter would not work with possible relations.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be ignored as it will become opt-in, would be good to document on the flag that it could/would not work with relation filters though.


return this.repo.save(entity)
}

Expand All @@ -210,12 +215,11 @@ export class TypeOrmQueryService<Entity>
* ]);
* ```
* @param records - The entities to create.
* @param opts - Additional options.
*/
public async createMany(records: DeepPartial<Entity>[]): Promise<Entity[]> {
public async createMany(records: DeepPartial<Entity>[], opts?: CreateOneOptions<Entity>): Promise<Entity[]> {
const entities = await Promise.all(records.map((r) => this.ensureIsEntityAndDoesNotExist(r)))
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.repo.save(entities)
return this.repo.save(opts?.filter ? applyFilter(entities, opts.filter) : entities)
}

/**
Expand Down