Merge pull request #23450 from mareklibra/FLPATH-1004.sorting.followup

fix: use orderField and createdAfter to unify for other plugins
This commit is contained in:
Marek Libra
2024-03-15 13:23:49 +01:00
committed by GitHub
9 changed files with 158 additions and 56 deletions
@@ -405,8 +405,7 @@ describe.each(databases.eachSupportedId())(
it('should sort created asc', async () => {
const notificationsCreatedAsc = await storage.getNotifications({
user,
sort: 'created',
sortOrder: 'asc',
orderField: [{ field: 'created', order: 'asc' }],
});
expect(notificationsCreatedAsc.map(idOnly)).toEqual([id1, id3, id2]);
});
@@ -414,8 +413,7 @@ describe.each(databases.eachSupportedId())(
it('should sort created desc', async () => {
const notificationsCreatedDesc = await storage.getNotifications({
user,
sort: 'created',
sortOrder: 'desc',
orderField: [{ field: 'created', order: 'desc' }],
});
expect(notificationsCreatedDesc.map(idOnly)).toEqual([id2, id3, id1]);
});
@@ -423,8 +421,7 @@ describe.each(databases.eachSupportedId())(
it('should sort topic asc', async () => {
const notificationsTopicAsc = await storage.getNotifications({
user,
sort: 'topic',
sortOrder: 'asc',
orderField: [{ field: 'topic', order: 'asc' }],
});
expect(notificationsTopicAsc.map(idOnly)).toEqual([id1, id3, id2]);
});
@@ -432,8 +429,7 @@ describe.each(databases.eachSupportedId())(
it('should sort topic desc', async () => {
const notificationsTopicDesc = await storage.getNotifications({
user,
sort: 'topic',
sortOrder: 'desc',
orderField: [{ field: 'topic', order: 'desc' }],
});
expect(notificationsTopicDesc.map(idOnly)).toEqual([id2, id3, id1]);
});
@@ -441,8 +437,7 @@ describe.each(databases.eachSupportedId())(
it('should sort origin asc', async () => {
const notificationsOrigin = await storage.getNotifications({
user,
sort: 'origin',
sortOrder: 'asc',
orderField: [{ field: 'origin', order: 'asc' }],
limit: 2,
offset: 0,
});
@@ -452,8 +447,7 @@ describe.each(databases.eachSupportedId())(
it('should sort origin desc', async () => {
const notificationsOriginNext = await storage.getNotifications({
user,
sort: 'origin',
sortOrder: 'desc',
orderField: [{ field: 'origin', order: 'desc' }],
limit: 2,
offset: 2,
});
@@ -160,7 +160,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
private getNotificationsBaseQuery = (
options: NotificationGetOptions | NotificationModifyOptions,
) => {
const { user } = options;
const { user, orderField } = options;
const subQuery = this.db('notification')
.select(NOTIFICATION_COLUMNS)
@@ -171,10 +171,12 @@ export class DatabaseNotificationsStore implements NotificationsStore {
q.where('user', user).orWhereNull('user');
});
if (options.sort !== undefined && options.sort !== null) {
query.orderBy(options.sort, options.sortOrder ?? 'desc');
} else if (options.sort !== null) {
query.orderBy('created', options.sortOrder ?? 'desc');
if (orderField && orderField.length > 0) {
orderField.forEach(orderBy => {
query.orderBy(orderBy.field, orderBy.order);
});
} else if (!orderField) {
query.orderBy('created', 'desc');
}
if (options.createdAfter) {
@@ -235,7 +237,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
const countOptions: NotificationGetOptions = { ...options };
countOptions.limit = undefined;
countOptions.offset = undefined;
countOptions.sort = null;
countOptions.orderField = [];
const notificationQuery = this.getNotificationsBaseQuery(countOptions);
const response = await notificationQuery.count('id as CNT');
return Number(response[0].CNT);
@@ -266,7 +268,7 @@ export class DatabaseNotificationsStore implements NotificationsStore {
async getStatus(options: NotificationGetOptions) {
const notificationQuery = this.getNotificationsBaseQuery({
...options,
sort: null,
orderField: [],
});
const readSubQuery = notificationQuery
.clone()
@@ -20,6 +20,12 @@ import {
NotificationStatus,
} from '@backstage/plugin-notifications-common';
/** @internal */
export type EntityOrder = {
field: string;
order: 'asc' | 'desc';
};
// TODO: reuse the common part of the type with front-end
/** @internal */
export type NotificationGetOptions = {
@@ -28,8 +34,7 @@ export type NotificationGetOptions = {
offset?: number;
limit?: number;
search?: string;
sort?: 'created' | 'topic' | 'origin' | null;
sortOrder?: 'asc' | 'desc';
orderField?: EntityOrder[];
read?: boolean;
saved?: boolean;
createdAfter?: Date;
@@ -0,0 +1,57 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams';
describe('parseEntityOrderFieldParams', () => {
it('supports no order fields', () => {
const result = parseEntityOrderFieldParams({});
expect(result).toEqual(undefined);
});
it('supports single orderField', () => {
const result = parseEntityOrderFieldParams({
orderField: ['metadata.name,desc'],
})!;
expect(result).toEqual([{ field: 'metadata.name', order: 'desc' }]);
});
it('supports single orderField without order', () => {
const result = parseEntityOrderFieldParams({
orderField: ['metadata.name'],
})!;
expect(result).toEqual([{ field: 'metadata.name' }]);
});
it('supports multiple order fields', () => {
const result = parseEntityOrderFieldParams({
orderField: ['metadata.name,desc', 'metadata.uid,asc'],
});
expect(result).toEqual([
{ field: 'metadata.name', order: 'desc' },
{ field: 'metadata.uid', order: 'asc' },
]);
});
it('throws if orderField order is not valid', () => {
expect(() =>
parseEntityOrderFieldParams({
orderField: ['metadata.name,desc', 'metadata.uid,invalid'],
}),
).toThrow(/Invalid order field order/);
});
});
@@ -0,0 +1,62 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is based on the plugins/catalog-backend
import { InputError } from '@backstage/errors';
import { EntityOrder } from '../database';
/**
* Takes a single unknown parameter and makes sure that it's a single string or
* an array of strings, and returns as an array.
*/
export function parseStringsParam(
param: unknown,
ctx: string,
): string[] | undefined {
if (param === undefined) {
return undefined;
}
const array = [param].flat();
if (array.some(p => typeof p !== 'string')) {
throw new InputError(`Invalid ${ctx}, not a string`);
}
return array as string[];
}
export function parseEntityOrderFieldParams(
params: Record<string, unknown>,
): EntityOrder[] | undefined {
const orderFieldStrings = parseStringsParam(params.orderField, 'orderField');
if (!orderFieldStrings) {
return undefined;
}
return orderFieldStrings.map(orderFieldString => {
const [field, order] = orderFieldString.split(',');
if (order !== undefined && !isOrder(order)) {
throw new InputError('Invalid order field order, must be asc or desc');
}
return { field, order };
});
}
export function isOrder(order: string): order is 'asc' | 'desc' {
return ['asc', 'desc'].includes(order);
}
@@ -45,6 +45,7 @@ import {
Notification,
NotificationReadSignal,
} from '@backstage/plugin-notifications-common';
import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams';
/** @internal */
export interface RouterOptions {
@@ -59,28 +60,6 @@ export interface RouterOptions {
processors?: NotificationProcessor[];
}
const getSort = (input: string): NotificationGetOptions['sort'] | undefined => {
const valid: NotificationGetOptions['sort'][] = [
'created',
'topic',
'origin',
];
if ((valid as string[]).includes(input)) {
return input as NotificationGetOptions['sort'];
}
return undefined;
};
const getSortOrder = (input: string): NotificationGetOptions['sortOrder'] => {
const valid: NotificationGetOptions['sortOrder'][] = ['asc', 'desc'];
if ((valid as string[]).includes(input)) {
return input as NotificationGetOptions['sortOrder'];
}
return 'desc';
};
/** @internal */
export async function createRouter(
options: RouterOptions,
@@ -209,11 +188,8 @@ export async function createRouter(
if (req.query.limit) {
opts.limit = Number.parseInt(req.query.limit.toString(), 10);
}
if (req.query.sort) {
opts.sort = getSort(req.query.sort.toString());
}
if (req.query.sort_order) {
opts.sortOrder = getSortOrder(req.query.sort_order.toString());
if (req.query.orderField) {
opts.orderField = parseEntityOrderFieldParams(req.query);
}
if (req.query.search) {
opts.search = req.query.search.toString();
@@ -230,8 +206,8 @@ export async function createRouter(
opts.saved = false;
// or keep undefined
}
if (req.query.created_after) {
const sinceEpoch = Date.parse(String(req.query.created_after));
if (req.query.createdAfter) {
const sinceEpoch = Date.parse(String(req.query.createdAfter));
if (isNaN(sinceEpoch)) {
throw new InputError('Unexpected date format');
}