Skip to content

Commit

Permalink
fix: fetching customers of businesses via businesses API
Browse files Browse the repository at this point in the history
  • Loading branch information
kasir-barati committed Jan 1, 2025
1 parent ed8b6b9 commit bb89303
Show file tree
Hide file tree
Showing 19 changed files with 306 additions and 56 deletions.
4 changes: 3 additions & 1 deletion apps/depth/src/business/business.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
} from 'typeorm';
import { CustomerEntity } from '../customer/customer.entity';

@Entity()
export const BUSINESS_TABLE_NAME = 'businesses';

@Entity(BUSINESS_TABLE_NAME)
export class BusinessEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
Expand Down
25 changes: 23 additions & 2 deletions apps/depth/src/business/business.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import {
CursorPager,
FilterQueryBuilder,
PagingDto,
QueryService,
} from '@shared';
import { Args, Query, Resolver } from 'type-graphql';
import {
Args,
Ctx,
FieldResolver,
Query,
Resolver,
Root,
} from 'type-graphql';
import { CustomerDto } from '../customer/dto/customer.dto';
import { AppDataSource } from '../shared/data-source';
import { GraphQLResolveContext } from '../shared/dataloader';
import { BusinessEntity } from './business.entity';
import { BusinessService } from './business.service';
import {
BusinessDto,
BusinessDtoConnection,
} from './dto/business.dto';
import { PagingDto } from './dto/paging.dto';

@Resolver(() => BusinessDto)
export class BusinessResolver {
Expand Down Expand Up @@ -39,4 +48,16 @@ export class BusinessResolver {
businesses(@Args(() => PagingDto) paging: PagingDto) {
return BusinessResolver.businessService.findAll(paging);
}

@FieldResolver(() => [CustomerDto])
async customers(
@Root() business: BusinessDto,
@Ctx() context: GraphQLResolveContext,
) {
const customer = await context.loaders.customerLoader.load(
business.id,
);

return customer;
}
}
2 changes: 1 addition & 1 deletion apps/depth/src/business/business.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {
createEdgeType,
CursorPager,
PageInfoTypeImplementation,
PagingDto,
QueryService,
} from '@shared';
import { validatePagination } from '../shared/validate-pagination.util';
import { BusinessEntity } from './business.entity';
import { BusinessDto } from './dto/business.dto';
import { PagingDto } from './dto/paging.dto';

export class BusinessService {
constructor(
Expand Down
2 changes: 1 addition & 1 deletion apps/depth/src/business/dto/business.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ID,
ObjectType,
} from 'type-graphql';
import { CustomerDto } from '../../customer/customer.dto';
import { CustomerDto } from '../../customer/dto/customer.dto';

@ObjectType('Business')
export class BusinessDto {
Expand Down
28 changes: 0 additions & 28 deletions apps/depth/src/business/dto/paging.dto.ts

This file was deleted.

11 changes: 8 additions & 3 deletions apps/depth/src/customer/customer.entity.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
RelationId,
} from 'typeorm';
import { BusinessEntity } from '../business/business.entity';

@Entity()
export const CUSTOMER_TABLE_NAME = 'customers';

@Entity(CUSTOMER_TABLE_NAME)
export class CustomerEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('varchar', { length: 200 })
name: string;

@RelationId((customer: CustomerEntity) => customer.shopAt)
// https://stackoverflow.com/a/61433772/8784518
// @RelationId is buggy!
@Column('uuid')
shopAtId: string;

@ManyToOne(() => BusinessEntity, (business) => business.customers)
@JoinColumn({ name: 'shopAtId' })
shopAt: BusinessEntity;
}
54 changes: 54 additions & 0 deletions apps/depth/src/customer/customer.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Repository } from 'typeorm';
import { CustomerEntity } from './customer.entity';
import {
RawFindAllByBusinessesIdsCustomer,
SerializedFindAllByBusinessesIds,
} from './customer.type';

export class CustomerRepository {
constructor(
public readonly repository: Repository<CustomerEntity>,
) {}

async findAllByBusinessesIds(businessesIds: readonly string[]) {
/**
* @description
* - Result needs to be serialized.
* - We have to use `this.repository.manager.connection.createQueryBuilder`
* since if we use `this.repository.createQueryBuilder` it'll add extra
* `FROM` clause, generating a different query!
*
* @link https://github.com/typeorm/typeorm/issues/4015
* @link https://stackoverflow.com/a/73916935/8784518
*/
const result = await this.repository.manager.connection
.createQueryBuilder()
.select('*')
.from((queryBuilder) => {
return queryBuilder
.select('*')
.addSelect(
'ROW_NUMBER() OVER(PARTITION BY "shopAtId" ORDER BY id)',
)
.from(CustomerEntity, 'customers')
.where('"shopAtId" IN (:...ids)', { ids: businessesIds });
}, 'customers_of_shops')
.where('row_number <= :length', {
length: businessesIds.length,
})
.getRawMany();

return this.serializeFindAllByBusinessesIds(result);
}

private serializeFindAllByBusinessesIds(
result: RawFindAllByBusinessesIdsCustomer[],
): SerializedFindAllByBusinessesIds[] {
return result.map((unsanitizedCustomer) => {
const { row_number, ...rest } = unsanitizedCustomer;
return {
...rest,
};
});
}
}
40 changes: 40 additions & 0 deletions apps/depth/src/customer/customer.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
CursorPager,
FilterQueryBuilder,
PagingDto,
QueryService,
} from '@shared';
import { Args, Query, Resolver } from 'type-graphql';
import { AppDataSource } from '../shared/data-source';
import { CustomerEntity } from './customer.entity';
import { CustomerRepository } from './customer.repository';
import { CustomerService } from './customer.service';
import {
CustomerDto,
CustomerDtoConnection,
} from './dto/customer.dto';

@Resolver(() => CustomerDto)
export class CustomerResolver {
private static customerService: CustomerService;

static init() {
const cursorPager = new CursorPager(CustomerDto, ['id']);
const customerRepository =
AppDataSource.getRepository(CustomerEntity);
const filterQueryBuilder = new FilterQueryBuilder(
customerRepository,
);
const queryService = new QueryService(filterQueryBuilder);
CustomerResolver.customerService = new CustomerService(
cursorPager,
queryService,
new CustomerRepository(customerRepository),
);
}

@Query(() => CustomerDtoConnection)
customers(@Args(() => PagingDto) paging: PagingDto) {
CustomerResolver.customerService.findAll(paging);
}
}
61 changes: 61 additions & 0 deletions apps/depth/src/customer/customer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { CursorPager, PagingDto, QueryService } from '@shared';
import { CustomerEntity } from './customer.entity';
import { CustomerRepository } from './customer.repository';
import { SerializedFindAllByBusinessesIds } from './customer.type';
import { CustomerDto } from './dto/customer.dto';

export class CustomerService {
constructor(
private readonly cursorPager: CursorPager<CustomerDto>,
private readonly queryService: QueryService<CustomerEntity>,
private readonly customerRepository: CustomerRepository,
) {}

findAll(paging: PagingDto) {
return this.cursorPager.page(
(query) => this.queryService.query(query),
{ paging },
);
}

async getCustomersByBatch(
businessesIds: readonly string[],
): Promise<Array<SerializedFindAllByBusinessesIds[] | Error>> {
const customers =
await this.customerRepository.findAllByBusinessesIds(
businessesIds,
);
const mappedResults = this.mapCustomersToBusinessesIds(
customers,
businessesIds,
);

return mappedResults;
}

/**
* @description
* When using Dataloader we have to fulfill 2 requirements:
* 1. `?? null` part: The length of the returned array must be the same with the length of the supplied keys.
* We need to return `null` if a customer is not found for a given business ID.
* 2. `customers.filter` part: The returned values must be ordered in the same order as the supplied keys.
* E.g. if the keys are `[1, 3, 4]`, the value must be something like `[customerOfBusiness1, customerOfBusiness3, customerOfBusiness4]`.
* The data source might not return them in the same order, so we have to reorder them.
*/
private mapCustomersToBusinessesIds(
customers: Readonly<SerializedFindAllByBusinessesIds[]>,
businessesIds: Readonly<string[]>,
): SerializedFindAllByBusinessesIds[][] {
const mappedCustomers = businessesIds.map((businessId) => {
const customer = customers.filter(
(customer) => customer.shopAtId === businessId,
);

return customer ?? null;
});

// console.log(mappedCustomers);

return mappedCustomers;
}
}
19 changes: 19 additions & 0 deletions apps/depth/src/customer/customer.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CustomerEntity } from './customer.entity';

export interface RawFindAllByBusinessesIdsCustomer {
/**@example '000d2a08-21d4-4482-a4a6-f18f67c62787' */
id: string;
/**@example 'customer01' */
name: string;
/**@example '0088cbc0-c82a-473c-bcae-6954610ddd72' */
shopAtId: string;
/**
* @description This field in the DB is of type `bigint` but it seems that TypeORM is converting it to `string`.
* @example '1'
*/
row_number: string;
}
export type SerializedFindAllByBusinessesIds = Omit<
CustomerEntity,
'shopAt'
>;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createConnectionType } from '@shared';
import { Field, ID, ObjectType } from 'type-graphql';
import { BusinessDto } from '../business/dto/business.dto';
import { BusinessDto } from '../../business/dto/business.dto';

@ObjectType()
export class CustomerDto {
Expand All @@ -15,3 +16,5 @@ export class CustomerDto {
@Field(() => BusinessDto)
shopAt: BusinessDto;
}
export const CustomerDtoConnection =
createConnectionType(CustomerDto);
34 changes: 33 additions & 1 deletion apps/depth/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import {
CursorPager,
FilterQueryBuilder,
QueryService,
} from '@shared';
import 'reflect-metadata';
import { buildSchema } from 'type-graphql';
import { BusinessResolver } from './business/business.resolver';
import { CustomerEntity } from './customer/customer.entity';
import { CustomerRepository } from './customer/customer.repository';
import { CustomerResolver } from './customer/customer.resolver';
import { CustomerService } from './customer/customer.service';
import { CustomerDto } from './customer/dto/customer.dto';
import { AppDataSource } from './shared/data-source';
import { DataloaderService } from './shared/dataloader';

(async () => {
await AppDataSource.initialize();

console.log('Connected to database.');

const schema = await buildSchema({
resolvers: [BusinessResolver],
resolvers: [BusinessResolver, CustomerResolver],
});
const server = new ApolloServer({ schema });
const { url } = await startStandaloneServer(server, {
listen: { port: 4009 },
context: async () => {
// I know this is ugly and not practical. But I just wanted to get over it.
// Maybe later I'll think about refactoring it, MAYBE!
const customerRepository =
AppDataSource.getRepository(CustomerEntity);
const filterQueryBuilder = new FilterQueryBuilder(
customerRepository,
);
const queryService = new QueryService(filterQueryBuilder);
const cursorPager = new CursorPager(CustomerDto, ['id']);
const customerService = new CustomerService(
cursorPager,
queryService,
new CustomerRepository(customerRepository),
);
const loaders = new DataloaderService(
customerService,
).getLoaders();

return { loaders };
},
});

BusinessResolver.init();
Expand Down
Loading

0 comments on commit bb89303

Please sign in to comment.