Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
58 changes: 51 additions & 7 deletions api/docs/space-api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -811,13 +811,6 @@ paths:
schema:
type: string
example: test@user.com
- name: serviceName
in: query
description: The name of the service for filter. All returned contracts will beinclude this service in their subscriptions.
required: false
schema:
type: string
example: Zoom
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
Expand All @@ -835,6 +828,57 @@ paths:
- email
example: lastName
default: username
requestBody:
description: >-
Allow to define additional, more-complex filters on the requests regarding subscriptions composition.
content:
application/json:
schema:
type: object
properties:
services:
oneOf:
- type: array
description: >-
List of services that the subscription must include
items:
type: string
description: Name of the service
- type: object
description: >-
Map containing service names as keys and plans/add-ons
array that the subscription must include for such
service as values.
additionalProperties:
type: array
items:
type: string
description: Versions of the service
subscriptionPlans:
type: object
description: >-
Map containing service names as keys and plans array that the
subscription must include for such service as values.
additionalProperties:
type: array
items:
type: string
description: Name of the plan
subscriptionAddOns:
type: object
description: >-
Map containing service names as keys and add-ons array that
the subscription must include for such service as values.
additionalProperties:
type: array
items:
type: string
description: Name of the add-on
example:
subscriptionPlan: "PRO"
additionalAddOns:
largeMeetings: 1
zoomWhiteboard: 1
responses:
'200':
description: Successful operation
Expand Down
10 changes: 9 additions & 1 deletion api/src/main/controllers/ContractController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ class ContractController {
async index(req: any, res: any) {
try {
const queryParams = this._transformIndexQueryParams(req.query);
const filters = req.body?.filters;

if (filters) {
// merge filters into top-level params and delegate to index which will call repository.findByFilters
const merged = { ...queryParams, filters };
const contracts = await this.contractService.index(merged);
res.json(contracts);
return;
}

const contracts = await this.contractService.index(queryParams);
res.json(contracts);
Expand Down Expand Up @@ -171,7 +180,6 @@ class ContractController {
firstName: indexQueryParams.firstName as string,
lastName: indexQueryParams.lastName as string,
email: indexQueryParams.email as string,
serviceName: indexQueryParams.serviceName as string,
page: parseInt(indexQueryParams['page'] as string) || 1,
offset: parseInt(indexQueryParams['offset'] as string) || 0,
limit: parseInt(indexQueryParams['limit'] as string) || 20,
Expand Down
115 changes: 86 additions & 29 deletions api/src/main/repositories/mongoose/ContractRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@ import { ContractQueryFilters, ContractToCreate, LeanContract } from '../../type
import { toPlainObject } from '../../utils/mongoose';

class ContractRepository extends RepositoryBase {
async findAll(queryFilters?: ContractQueryFilters) {
/**
* Find contracts using advanced filters provided in `filters` key of queryFilters.
* filters may contain:
* - services: either array of service names OR object { serviceName: [versions] }
* - plans: { serviceName: [planNames] }
* - addOns: { serviceName: [addOnNames] }
*/
async findByFilters(queryFilters: any) {
const {
username,
firstName,
lastName,
email,
serviceName,
page = 1,
offset = 0,
limit = 20,
sort,
order = 'asc',
filters,
} = queryFilters || {};

const matchConditions = [];
const matchConditions: any[] = [];

if (username) {
matchConditions.push({ 'userContact.username': { $regex: username, $options: 'i' } });
Expand All @@ -32,35 +39,85 @@ class ContractRepository extends RepositoryBase {
if (email) {
matchConditions.push({ 'userContact.email': { $regex: email, $options: 'i' } });
}
if (serviceName) {
matchConditions.push({
contractedServicesArray: {
$elemMatch: {
k: { $regex: serviceName, $options: 'i' },
},
},
});

// We'll convert contractedServices object to array to ease matching
const pipeline: any[] = [
{ $addFields: { contractedServicesArray: { $objectToArray: '$contractedServices' } } },
];

if (filters) {
// services filter
if (filters.services) {
const services = filters.services;
if (Array.isArray(services)) {
// array of service names: contractedServicesArray.k in list
matchConditions.push({ 'contractedServicesArray.k': { $in: services.map((s: string) => s.toLowerCase()) } });
} else if (typeof services === 'object') {
// object mapping serviceName -> [versions]
const perServiceMatches: any[] = [];
for (const [serviceName, versions] of Object.entries(services)) {
if (!Array.isArray(versions) || versions.length === 0) {
// match any version for the service
perServiceMatches.push({ 'contractedServicesArray': { $elemMatch: { k: serviceName.toLowerCase() } } });
} else {
// match if contractedServices has key serviceName and its v (value) in provided versions
perServiceMatches.push({
$and: [
{ 'contractedServicesArray': { $elemMatch: { k: serviceName.toLowerCase(), v: { $in: versions.map((v: string) => v.replace(/\./g, '_')) } } } },
],
});
}
}
if (perServiceMatches.length > 0) {
matchConditions.push({ $or: perServiceMatches });
}
}
}

// plans filter: subscriptionPlans is an object serviceName -> planName
if (filters.plans && typeof filters.plans === 'object') {
const perServicePlanMatches: any[] = [];
for (const [serviceName, plans] of Object.entries(filters.plans)) {
if (Array.isArray(plans) && plans.length > 0) {
perServicePlanMatches.push({ [`subscriptionPlans.${serviceName.toLowerCase()}`]: { $in: plans.map((p: string) => new RegExp(`^${p}$`, 'i')) } });
}
}
if (perServicePlanMatches.length > 0) {
matchConditions.push({ $or: perServicePlanMatches });
}
}

// addOns filter: subscriptionAddOns is object serviceName -> { addOnName: count }
if (filters.addOns && typeof filters.addOns === 'object') {
const perServiceAddOnMatches: any[] = [];
for (const [serviceName, addOns] of Object.entries(filters.addOns)) {
if (Array.isArray(addOns) && addOns.length > 0) {
// We need to check keys of subscriptionAddOns[serviceName]
perServiceAddOnMatches.push({
$or: addOns.map((addOnName: string) => ({ [`subscriptionAddOns.${serviceName.toLowerCase()}.${addOnName.toLowerCase()}`]: { $exists: true } })),
});
}
}
if (perServiceAddOnMatches.length > 0) {
matchConditions.push({ $or: perServiceAddOnMatches });
}
}
}

const contracts = await ContractMongoose.aggregate([
{
$addFields: {
contractedServicesArray: { $objectToArray: '$contractedServices' },
},
},
...(matchConditions.length > 0 ? [{ $match: { $and: matchConditions } }] : []),
{
$sort: {
[`userContact.${sort ?? 'username'}`]: order === 'asc' ? 1 : -1,
},
},
{
$skip: offset === 0 ? (page - 1) * limit : offset,
},
{
$limit: Math.min(limit, 100),
if (matchConditions.length > 0) {
pipeline.push({ $match: { $and: matchConditions } });
}

pipeline.push({
$sort: {
[`userContact.${sort ?? 'username'}`]: order === 'asc' ? 1 : -1,
},
]);
});

pipeline.push({ $skip: offset === 0 ? (page - 1) * limit : offset });
pipeline.push({ $limit: Math.min(limit, 100) });

const contracts = await ContractMongoose.aggregate(pipeline);

return contracts.map(contract => toPlainObject<LeanContract>(contract));
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/main/services/ContractService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ContractService {
);
}

const contracts: LeanContract[] = await this.contractRepository.findAll(queryParams);
const contracts: LeanContract[] = await this.contractRepository.findByFilters(queryParams);

for (const contract of contracts) {
contract.contractedServices = resetEscapeContractedServiceVersions(contract.contractedServices);
Expand Down
6 changes: 3 additions & 3 deletions api/src/main/services/ServiceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,8 +790,8 @@ class ServiceService {
pricingVersion: string,
fallBackSubscription: FallBackSubscription
): Promise<void> {
const serviceContracts: LeanContract[] = await this.contractRepository.findAll({
serviceName: serviceName,
const serviceContracts: LeanContract[] = await this.contractRepository.findByFilters({
services: [serviceName],
});

if (Object.keys(fallBackSubscription).length === 0) {
Expand Down Expand Up @@ -911,7 +911,7 @@ class ServiceService {
}

async _removeServiceFromContracts(serviceName: string): Promise<boolean> {
const contracts: LeanContract[] = await this.contractRepository.findAll({});
const contracts: LeanContract[] = await this.contractRepository.findByFilters({});
const novatedContracts: LeanContract[] = [];
const contractsToDisable: LeanContract[] = [];

Expand Down
Loading
Loading