Compare commits

...

10 Commits

22 changed files with 263 additions and 18 deletions

View File

@@ -26,6 +26,7 @@ import {
import PartialEntity from 'Common/Types/Database/PartialEntity';
import { UserPermission } from 'Common/Types/Permission';
import CommonAPI from './CommonAPI';
import GroupBy from '../Types/Database/GroupBy';
export default class BaseAPI<
TBaseModel extends BaseModel,
@@ -237,6 +238,7 @@ export default class BaseAPI<
let query: Query<BaseModel> = {};
let select: Select<BaseModel> = {};
let sort: Sort<BaseModel> = {};
let groupBy: GroupBy<BaseModel> | undefined;
if (req.body) {
query = JSONFunctions.deserialize(
@@ -250,6 +252,10 @@ export default class BaseAPI<
sort = JSONFunctions.deserialize(
req.body['sort']
) as Sort<BaseModel>;
groupBy = JSONFunctions.deserialize(
req.body['groupBy']
) as GroupBy<BaseModel>;
}
const databaseProps: DatabaseCommonInteractionProps =
@@ -260,6 +266,7 @@ export default class BaseAPI<
select,
skip: skip,
limit: limit,
groupBy: groupBy,
sort: sort,
props: databaseProps,
});

View File

@@ -25,6 +25,7 @@ import { UserPermission } from 'Common/Types/Permission';
import AnalyticsDataModel from 'Common/AnalyticsModels/BaseModel';
import AnalyticsDatabaseService from '../Services/AnalyticsDatabaseService';
import CommonAPI from './CommonAPI';
import GroupBy from '../Types/AnalyticsDatabase/GroupBy';
export default class BaseAnalyticsAPI<
TAnalyticsDataModel extends AnalyticsDataModel,
@@ -235,6 +236,7 @@ export default class BaseAnalyticsAPI<
let query: Query<AnalyticsDataModel> = {};
let select: Select<AnalyticsDataModel> = {};
let sort: Sort<AnalyticsDataModel> = {};
let groupBy: GroupBy<AnalyticsDataModel> = {};
if (req.body) {
query = JSONFunctions.deserialize(
@@ -248,6 +250,10 @@ export default class BaseAnalyticsAPI<
sort = JSONFunctions.deserialize(
req.body['sort']
) as Sort<AnalyticsDataModel>;
groupBy = JSONFunctions.deserialize(
req.body['groupBy']
) as GroupBy<AnalyticsDataModel>;
}
const databaseProps: DatabaseCommonInteractionProps =
@@ -259,11 +265,13 @@ export default class BaseAnalyticsAPI<
skip: skip,
limit: limit,
sort: sort,
groupBy: groupBy,
props: databaseProps,
});
const count: PositiveNumber = await this.service.countBy({
query,
groupBy: groupBy,
props: databaseProps,
});

View File

@@ -1018,9 +1018,8 @@ export default class StatusPageAPI extends BaseAPI<
},
select: scheduledEventsSelect,
sort: {
createdAt: SortOrder.Ascending,
startsAt: SortOrder.Ascending,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
props: {
@@ -1039,9 +1038,8 @@ export default class StatusPageAPI extends BaseAPI<
},
select: scheduledEventsSelect,
sort: {
createdAt: SortOrder.Ascending,
startsAt: SortOrder.Ascending,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
props: {

View File

@@ -320,6 +320,7 @@ export default class AnalyticsDatabaseService<
const databaseName: string =
this.database.getDatasourceOptions().database!;
const whereStatement: Statement =
this.statementGenerator.toWhereStatement(countBy.query);
@@ -331,6 +332,15 @@ export default class AnalyticsDatabaseService<
WHERE TRUE `.append(whereStatement);
/* eslint-enable prettier/prettier */
if (countBy.groupBy && Object.keys(countBy.groupBy).length > 0) {
statement.append(
SQL`
GROUP BY `.append(
this.statementGenerator.toGroupByStatement(countBy.groupBy)
)
);
}
if (countBy.limit) {
statement.append(SQL`
LIMIT ${{
@@ -364,29 +374,72 @@ export default class AnalyticsDatabaseService<
const databaseName: string =
this.database.getDatasourceOptions().database!;
let groupByStatement: Statement | null = null;
if (findBy.groupBy && Object.keys(findBy.groupBy).length > 0) {
// overwrite select object
findBy.select = {
...findBy.groupBy,
};
groupByStatement = this.statementGenerator.toGroupByStatement(
findBy.groupBy
);
}
const select: { statement: Statement; columns: Array<string> } =
this.statementGenerator.toSelectStatement(findBy.select!);
const whereStatement: Statement =
this.statementGenerator.toWhereStatement(findBy.query);
const sortStatement: Statement =
this.statementGenerator.toSortStatement(findBy.sort!);
/* eslint-disable prettier/prettier */
const statement: Statement = SQL`
SELECT `.append(select.statement).append(SQL`
FROM ${databaseName}.${this.model.tableName}
WHERE TRUE `).append(whereStatement).append(SQL`
ORDER BY `).append(sortStatement).append(SQL`
const statement: Statement = SQL``;
statement.append(
SQL`
SELECT `.append(select.statement)
);
statement.append(SQL`
FROM ${databaseName}.${this.model.tableName}`);
statement
.append(
SQL`
WHERE TRUE `
)
.append(whereStatement);
if (groupByStatement) {
statement
.append(
SQL`
GROUP BY `
)
.append(groupByStatement);
}
statement
.append(
SQL`
ORDER BY `
)
.append(sortStatement);
statement.append(SQL`
LIMIT ${{
value: Number(findBy.limit),
type: TableColumnType.Number,
}}
}}`);
statement.append(SQL`
OFFSET ${{
value: Number(findBy.skip),
type: TableColumnType.Number,
}}
`);
/* eslint-enable prettier/prettier */
logger.info(`${this.model.tableName} Find Statement`);

View File

@@ -809,9 +809,16 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
skip,
limit,
props,
groupBy,
distinctOn,
}: CountBy<TBaseModel>): Promise<PositiveNumber> {
try {
if (groupBy && Object.keys(groupBy).length > 0) {
throw new BadDataException(
'Group By is not supported for countBy'
);
}
if (!skip) {
skip = new PositiveNumber(0);
}
@@ -1130,6 +1137,15 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
onBeforeFind.limit = new PositiveNumber(onBeforeFind.limit);
}
if (
onBeforeFind.groupBy &&
Object.keys(onBeforeFind.groupBy).length > 0
) {
throw new BadDataException(
'GroupBy is currently not supported'
);
}
const items: Array<TBaseModel> = await this.getRepository().find({
skip: onBeforeFind.skip.toNumber(),
take: onBeforeFind.limit.toNumber(),

View File

@@ -196,13 +196,14 @@ describe('StatementGenerator', () => {
test('should SELECT multiple columns', () => {
const { statement, columns } = generator.toSelectStatement({
_id: true,
createdAt: false,
createdAt: true,
updatedAt: true,
});
expect(statement.query).toBe('{p0:Identifier}, {p1:Identifier}');
expect(statement.query_params).toStrictEqual({
p0: '_id',
p1: 'updatedAt',
p1: 'createdAt',
p2: 'updatedAt',
});
expect(columns).toStrictEqual(['_id', 'updatedAt']);
});

View File

@@ -2,10 +2,12 @@ import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
import PositiveNumber from 'Common/Types/PositiveNumber';
import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCommonInteractionProps';
import Query from './Query';
import GroupBy from './GroupBy';
export default interface CountBy<TBaseModel extends AnalyticsBaseModel> {
query: Query<TBaseModel>;
skip?: PositiveNumber | number;
limit?: PositiveNumber | number;
groupBy?: GroupBy<TBaseModel> | undefined;
props: DatabaseCommonInteractionProps;
}

View File

@@ -1,3 +1,4 @@
import GroupBy from './GroupBy';
import Query from './Query';
import Select from './Select';
import Sort from './Sort';
@@ -8,5 +9,6 @@ export default interface FindOneBy<TBaseModel extends BaseModel> {
query: Query<TBaseModel>;
select?: Select<TBaseModel> | undefined;
sort?: Sort<TBaseModel> | undefined;
groupBy?: GroupBy<TBaseModel> | undefined;
props: DatabaseCommonInteractionProps;
}

View File

@@ -0,0 +1,8 @@
/**
* GroupBy find options.
*/
declare type GroupBy<Entity> = {
[P in keyof Entity]?: true;
};
export default GroupBy;

View File

@@ -1,7 +1,7 @@
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
import Dictionary from 'Common/Types/Dictionary';
export type SelectPropertyOptions = boolean | Dictionary<boolean>;
export type SelectPropertyOptions = true | Dictionary<true>;
/**
* Select find options.

View File

@@ -2,10 +2,12 @@ import BaseModel from 'Common/Models/BaseModel';
import PositiveNumber from 'Common/Types/PositiveNumber';
import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCommonInteractionProps';
import Query from './Query';
import GroupBy from './GroupBy';
export default interface CountBy<TBaseModel extends BaseModel> {
query: Query<TBaseModel>;
skip?: PositiveNumber | number;
groupBy?: GroupBy<TBaseModel> | undefined;
limit?: PositiveNumber | number;
props: DatabaseCommonInteractionProps;
distinctOn?: string | undefined;

View File

@@ -1,3 +1,4 @@
import GroupBy from './GroupBy';
import Query from './Query';
import Select from './Select';
import Sort from './Sort';
@@ -8,5 +9,6 @@ export default interface FindOneBy<TBaseModel extends BaseModel> {
query: Query<TBaseModel>;
select?: Select<TBaseModel> | undefined;
sort?: Sort<TBaseModel> | undefined;
groupBy?: GroupBy<TBaseModel> | undefined;
props: DatabaseCommonInteractionProps;
}

View File

@@ -0,0 +1,8 @@
/**
* GroupBy find options.
*/
declare type GroupBy<Entity> = {
[P in keyof Entity]?: true;
};
export default GroupBy;

View File

@@ -24,6 +24,7 @@ import GreaterThanOrEqual from 'Common/Types/BaseDatabase/GreaterThanOrEqual';
import InBetween from 'Common/Types/BaseDatabase/InBetween';
import IsNull from 'Common/Types/BaseDatabase/IsNull';
import Includes from 'Common/Types/BaseDatabase/Includes';
import GroupBy from '../../Types/AnalyticsDatabase/GroupBy';
// import JSONFunctions from 'Common/Types/JSONFunctions';
// import { JSONObject } from 'Common/Types/JSON';
@@ -471,6 +472,22 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
return whereStatement;
}
public toGroupByStatement(groupBy: GroupBy<TBaseModel>): Statement {
const groupByStatement: Statement = new Statement();
let first: boolean = true;
for (const key in groupBy) {
if (first) {
first = false;
} else {
groupByStatement.append(SQL`, `);
}
groupByStatement.append(SQL`${key}`);
}
return groupByStatement;
}
public toSortStatement(sort: Sort<TBaseModel>): Statement {
const sortStatement: Statement = new Statement();

View File

@@ -12,6 +12,7 @@ import Sort from '../../Utils/BaseDatabase/Sort';
import RequestOptions from '../../Utils/BaseDatabase/RequestOptions';
import NotImplementedException from 'Common/Types/Exception/NotImplementedException';
import { BaseModelType } from 'Common/Models/BaseModel';
import GroupBy from '../../Utils/BaseDatabase/GroupBy';
export interface ComponentProps<TBaseModel extends AnalyticsBaseModel>
extends BaseTableProps<TBaseModel> {
@@ -71,6 +72,7 @@ const AnalyticsModelTable: <TBaseModel extends AnalyticsBaseModel>(
getList: async (data: {
modelType: BaseModelType | AnalyticsBaseModelType;
query: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
limit: number;
skip: number;
sort: Sort<TBaseModel>;
@@ -80,6 +82,7 @@ const AnalyticsModelTable: <TBaseModel extends AnalyticsBaseModel>(
return await modelAPI.getList<TBaseModel>({
modelType: data.modelType as { new (): TBaseModel },
query: data.query,
groupBy: data.groupBy,
limit: data.limit,
skip: data.skip,
sort: data.sort,

View File

@@ -77,6 +77,7 @@ import SelectEntityField from '../../Types/SelectEntityField';
import { FilterData } from '../Filters/Filter';
import ClassicFilterType from '../Filters/Types/Filter';
import { getRefreshButton } from '../Card/CardButtons/Refresh';
import GroupBy from '../../Utils/BaseDatabase/GroupBy';
export enum ShowAs {
Table,
@@ -94,6 +95,7 @@ export interface BaseTableCallbacks<
getList: (data: {
modelType: BaseModelType | AnalyticsBaseModelType;
query: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
limit: number;
skip: number;
sort: Sort<TBaseModel>;
@@ -148,6 +150,7 @@ export interface BaseTableProps<
viewPageRoute?: undefined | Route;
onViewPage?: (item: TBaseModel) => Promise<Route>;
query?: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
onBeforeFetch?: (() => Promise<TBaseModel>) | undefined;
createInitialValues?: FormValues<TBaseModel> | undefined;
onBeforeCreate?:
@@ -611,6 +614,9 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
...query,
...props.query,
},
groupBy: {
...props.groupBy,
},
limit: itemsOnPage,
skip: (currentPageNumber - 1) * itemsOnPage,
select: {

View File

@@ -11,6 +11,7 @@ import Sort from '../../Utils/BaseDatabase/Sort';
import ModelFormModal from '../ModelFormModal/ModelFormModal';
import { FormType } from '../Forms/ModelForm';
import { AnalyticsBaseModelType } from 'Common/AnalyticsModels/BaseModel';
import GroupBy from '../../Utils/BaseDatabase/GroupBy';
export interface ComponentProps<TBaseModel extends BaseModel>
extends BaseTableProps<TBaseModel> {
@@ -53,6 +54,7 @@ const ModelTable: <TBaseModel extends BaseModel>(
getList: async (data: {
modelType: BaseModelType | AnalyticsBaseModelType;
query: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
limit: number;
skip: number;
sort: Sort<TBaseModel>;
@@ -63,6 +65,7 @@ const ModelTable: <TBaseModel extends BaseModel>(
modelType: data.modelType as { new (): TBaseModel },
query: data.query,
limit: data.limit,
groupBy: data.groupBy,
skip: data.skip,
sort: data.sort,
select: data.select,

View File

@@ -20,6 +20,7 @@ import Project from 'Model/Models/Project';
import Navigation from '../Navigation';
import BaseListResult from '../BaseDatabase/ListResult';
import RequestOptions from '../BaseDatabase/RequestOptions';
import GroupBy from '../BaseDatabase/GroupBy';
export interface ListResult<TAnalyticsBaseModel extends AnalyticsBaseModel>
extends BaseListResult<TAnalyticsBaseModel> {}
@@ -200,14 +201,23 @@ export default class ModelAPI {
>(data: {
modelType: { new (): TAnalyticsBaseModel };
query: Query<TAnalyticsBaseModel>;
groupBy?: GroupBy<TAnalyticsBaseModel> | undefined;
limit: number;
skip: number;
select: Select<TAnalyticsBaseModel>;
sort: Sort<TAnalyticsBaseModel>;
requestOptions?: RequestOptions | undefined;
}): Promise<ListResult<TAnalyticsBaseModel>> {
const { modelType, query, limit, skip, select, sort, requestOptions } =
data;
const {
modelType,
query,
limit,
skip,
select,
sort,
requestOptions,
groupBy,
} = data;
const model: TAnalyticsBaseModel = new modelType();
const apiPath: Route | null = model.crudApiPath;
@@ -242,6 +252,7 @@ export default class ModelAPI {
query: JSONFunctions.serialize(query as JSONObject),
select: JSONFunctions.serialize(select as JSONObject),
sort: JSONFunctions.serialize(sort as JSONObject),
groupBy: JSONFunctions.serialize(groupBy as JSONObject),
},
headers,
{

View File

@@ -0,0 +1,9 @@
import AnalyticsDataModel from 'Common/AnalyticsModels/BaseModel';
import BaseModel from 'Common/Models/BaseModel';
import { JSONObject } from 'Common/Types/JSON';
type GroupBy<TBaseModel extends AnalyticsDataModel | BaseModel | JSONObject> = {
[P in keyof TBaseModel]?: true;
};
export default GroupBy;

View File

@@ -20,6 +20,7 @@ import Project from 'Model/Models/Project';
import Navigation from '../Navigation';
import BaseListResult from '../BaseDatabase/ListResult';
import BaseRequestOptions from '../BaseDatabase/RequestOptions';
import GroupBy from '../BaseDatabase/GroupBy';
export class ModelAPIHttpResponse<
TBaseModel extends BaseModel
@@ -184,6 +185,7 @@ export default class ModelAPI {
public static async getList<TBaseModel extends BaseModel>(data: {
modelType: { new (): TBaseModel };
query: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
limit: number;
skip: number;
select: Select<TBaseModel>;
@@ -227,6 +229,9 @@ export default class ModelAPI {
query: JSONFunctions.serialize(data.query as JSONObject),
select: JSONFunctions.serialize(data.select as JSONObject),
sort: JSONFunctions.serialize(data.sort as JSONObject),
groupBy: JSONFunctions.serialize(
data.groupBy as JSONObject
),
},
headers,
{

View File

@@ -1,13 +1,71 @@
import React, { Fragment, FunctionComponent, ReactElement } from 'react';
import PageComponentProps from '../../../../PageComponentProps';
import ComingSoon from 'CommonUI/src/Components/ComingSoon/ComingSoon';
import AnalyticsModelTable from 'CommonUI/src/Components/ModelTable/AnalyticsModelTable';
import Metric from 'Model/AnalyticsModels/Metric';
import DashboardNavigation from '../../../../../Utils/Navigation';
import Navigation from 'CommonUI/src/Utils/Navigation';
import ObjectID from 'Common/Types/ObjectID';
import FieldType from 'CommonUI/src/Components/Types/FieldType';
import Route from 'Common/Types/API/Route';
import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
const ServiceDelete: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps
): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
return (
<Fragment>
<ComingSoon />
<AnalyticsModelTable<Metric>
modelType={Metric}
id="metrics-table"
isDeleteable={false}
isEditable={false}
isCreateable={false}
singularName="Metric"
pluralName="Metrics"
name="Metrics"
isViewable={true}
sortBy="name"
sortOrder={SortOrder.Ascending}
cardProps={{
title: 'Metrics',
description:
'Metrics are the individual data points that make up a service. They are the building blocks of a service and represent the work done by a single service.',
}}
groupBy={{
name: true,
}}
onViewPage={async (_item: Metric) => {
return Promise.resolve(new Route(''));
}}
query={{
projectId: DashboardNavigation.getProjectId(),
serviceId: modelId,
}}
showViewIdButton={false}
noItemsMessage={'No metrics found for this service.'}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[
{
field: {
name: true,
},
title: 'Name',
type: FieldType.Text,
},
]}
columns={[
{
field: {
name: true,
},
title: 'Name',
type: FieldType.Text,
},
]}
/>
</Fragment>
);
};

View File

@@ -15,6 +15,32 @@ export default class Metric extends AnalyticsBaseModel {
singularName: 'Metric',
pluralName: 'Metrics',
crudApiPath: new Route('/metrics'),
accessControl: {
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanReadTelemetryServiceTraces,
],
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanCreateTelemetryServiceTraces,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanEditTelemetryServiceTraces,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanDeleteTelemetryServiceTraces,
],
},
tableColumns: [
new AnalyticsTableColumn({
key: 'projectId',