Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Neo4j] Nested Sorting + Engagement expanded usage #3226

Merged
merged 13 commits into from
Jun 3, 2024
2 changes: 1 addition & 1 deletion src/common/db-sort.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const DbSortSymbol = Symbol('DbSortSymbol');
/**
* A function given a cypher variable will output cypher to transform it for sorting.
*/
type SortTransformer = (value: string) => string;
export type SortTransformer = (value: string) => string;

/**
* Customize the way this field is sorted upon.
Expand Down
2 changes: 1 addition & 1 deletion src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export * from './data-object';
export * from './date-filter.input';
export { DbLabel } from './db-label.decorator';
export * from './db-label.helpers';
export * from './db-sort.decorator';
export { DbSort } from './db-sort.decorator';
export * from './db-unique.decorator';
export * from './disabled.decorator';
export * from './mutation-placeholder.output';
Expand Down
2 changes: 1 addition & 1 deletion src/common/pagination.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const SortablePaginationInput = <SortKey extends string = string>({
description: 'The field in which to sort on',
defaultValue: defaultSort,
})
@Matches(/^[A-Za-z0-9_]+$/)
@Matches(/^[A-Za-z0-9_.]+$/)
readonly sort: SortKey = defaultSort;

@Field(() => Order, {
Expand Down
109 changes: 106 additions & 3 deletions src/components/engagement/engagement.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { mapValues, simpleSwitch } from '@seedcompany/common';
import { cleanJoin, mapValues, simpleSwitch } from '@seedcompany/common';
import { inArray, node, Node, Query, relation } from 'cypher-query-builder';
import { difference, pickBy } from 'lodash';
import { DateTime } from 'luxon';
Expand All @@ -23,20 +23,29 @@ import {
coalesce,
createNode,
createRelationships,
defineSorters,
filter,
INACTIVE,
matchChangesetAndChangedProps,
matchProjectSens,
matchPropsAndProjectSensAndScopedRoles,
merge,
oncePerProject,
paginate,
requestingUser,
sorting,
SortCol,
sortWith,
whereNotDeletedInChangeset,
} from '~/core/database/query';
import { Privileges } from '../authorization';
import { FileId } from '../file/dto';
import { languageSorters } from '../language/language.repository';
import {
matchCurrentDue,
progressReportSorters,
} from '../periodic-report/periodic-report.repository';
import { ProjectType } from '../project/dto';
import { projectSorters } from '../project/project.repository';
import {
CreateInternshipEngagement,
CreateLanguageEngagement,
Expand Down Expand Up @@ -379,7 +388,7 @@ export class EngagementRepository extends CommonRepository {
wrapContext: oncePerProject,
}),
)
.apply(sorting(IEngagement, input))
.apply(sortWith(engagementSorters, input))
.apply(paginate(input, this.hydrate(session, viewOfChangeset(changeset))))
.first();
return result!; // result from paginate() will always have 1 row.
Expand Down Expand Up @@ -514,3 +523,97 @@ export class EngagementRepository extends CommonRepository {
return this.getConstraintsFor(IEngagement);
}
}

export const engagementSorters = defineSorters(IEngagement, {
nameProjectFirst: (query) =>
query
.apply(matchNames)
.return<SortCol>(
multiPropsAsSortString(['projectName', 'languageName', 'dfn', 'dln']),
),
nameProjectLast: (query) =>
query
.apply(matchNames)
.return<SortCol>(
multiPropsAsSortString(['languageName', 'dfn', 'dln', 'projectName']),
),
sensitivity: (query) =>
query
.match([node('project'), relation('out', '', 'engagement'), node('node')])
.apply(matchProjectSens())
.return<{ sortValue: unknown }>('sensitivity as sortValue'),
// eslint-disable-next-line @typescript-eslint/naming-convention
'language.*': (query, input) =>
query
.with('node as eng')
.match([node('eng'), relation('out', '', 'language'), node('node')])
.apply(sortWith(languageSorters, input))
// Use null for all internship engagements
.union()
.with('node')
.with('node as eng')
.raw('where eng:InternshipEngagement')
.return<SortCol>('null as sortValue'),
// eslint-disable-next-line @typescript-eslint/naming-convention
'project.*': (query, input) =>
query
.with('node as eng')
.match([node('eng'), relation('in', '', 'engagement'), node('node')])
.apply(sortWith(projectSorters, input)),
// eslint-disable-next-line @typescript-eslint/naming-convention
'currentProgressReportDue.*': (query, input) =>
query
.subQuery('node', (sub) =>
sub
.with('node as parent')
.apply(matchCurrentDue(undefined, 'Progress'))
.return('collect(node) as reports'),
)
.subQuery('reports', (sub) =>
sub
.with('reports')
.raw('where size(reports) = 0')
.return('null as sortValue')
.union()
.with('reports')
.with('reports')
.raw('where size(reports) <> 0')
.raw('unwind reports as node')
.apply(sortWith(progressReportSorters, input)),
)
.return('sortValue'),
});

const matchNames = (query: Query) =>
query
.match([
node('project'),
relation('out', '', 'name', ACTIVE),
node('projectName', 'Property'),
])
.optionalMatch([
node('node'),
relation('out', '', 'language'),
node('', 'Language'),
relation('out', '', 'name', ACTIVE),
node('languageName', 'Property'),
])
.optionalMatch([
[node('node'), relation('out', '', 'intern'), node('intern', 'User')],
[
node('intern'),
relation('out', '', 'displayFirstName', ACTIVE),
node('dfn', 'Property'),
],
[
node('intern'),
relation('out', '', 'displayLastName', ACTIVE),
node('dln', 'Property'),
],
]);

const multiPropsAsSortString = (props: string[]) =>
cleanJoin(
' + ',
props.map((prop) => `coalesce(${prop}.value, "")`),
) + ' as sortValue';
21 changes: 19 additions & 2 deletions src/components/language/language.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
collect,
createNode,
createRelationships,
defineSorters,
exp,
filter,
matchChangesetAndChangedProps,
Expand All @@ -35,12 +36,13 @@ import {
paginate,
rankSens,
requestingUser,
sorting,
sortWith,
variable,
} from '~/core/database/query';
import { ProjectStatus } from '../project/dto';
import {
CreateLanguage,
EthnologueLanguage,
Language,
LanguageListInput,
UpdateLanguage,
Expand Down Expand Up @@ -245,7 +247,7 @@ export class LanguageRepository extends DtoRepository<
wrapContext: oncePerProject,
}),
)
.apply(sorting(Language, input))
.apply(sortWith(languageSorters, input))
.apply(paginate(input, this.hydrate(session)))
.first();
return result!; // result from paginate() will always have 1 row.
Expand Down Expand Up @@ -322,3 +324,18 @@ export class LanguageRepository extends DtoRepository<
);
}
}

export const languageSorters = defineSorters(Language, {
// eslint-disable-next-line @typescript-eslint/naming-convention
'ethnologue.*': (query, input) =>
query
.with('node as lang')
.match([
node('lang'),
relation('out', '', 'ethnologue'),
node('node', 'EthnologueLanguage'),
])
.apply(sortWith(ethnologueSorters, input)),
});

const ethnologueSorters = defineSorters(EthnologueLanguage, {});
9 changes: 6 additions & 3 deletions src/components/location/location.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import {
ACTIVE,
createNode,
createRelationships,
defineSorters,
matchProps,
merge,
paginate,
sorting,
sortWith,
} from '~/core/database/query';
import { FileService } from '../file';
import { FileId } from '../file/dto';
Expand Down Expand Up @@ -160,7 +161,7 @@ export class LocationRepository extends DtoRepository(Location) {
const result = await this.db
.query()
.matchNode('node', 'Location')
.apply(sorting(Location, input))
.apply(sortWith(locationSorters, input))
.apply(paginate(input, this.hydrate()))
.first();
return result!; // result from paginate() will always have 1 row.
Expand Down Expand Up @@ -216,7 +217,7 @@ export class LocationRepository extends DtoRepository(Location) {
relation('in', '', rel, ACTIVE),
node(`${label.toLowerCase()}`, label, { id }),
])
.apply(sorting(Location, input))
.apply(sortWith(locationSorters, input))
.apply(paginate(input, this.hydrate()))
.first();
return result!; // result from paginate() will always have 1 row.
Expand All @@ -231,3 +232,5 @@ export class LocationRepository extends DtoRepository(Location) {
return !!result;
}
}

export const locationSorters = defineSorters(Location, {});
64 changes: 41 additions & 23 deletions src/components/periodic-report/periodic-report.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ACTIVE,
createNode,
createRelationships,
defineSorters,
deleteBaseNode,
filter,
matchPropsAndProjectSensAndScopedRoles,
Expand All @@ -33,8 +34,14 @@ import {
Variable,
} from '~/core/database/query';
import { File } from '../file/dto';
import { ProgressReportStatus as ProgressStatus } from '../progress-report/dto';
import { ProgressReportExtraForPeriodicInterfaceRepository } from '../progress-report/progress-report-extra-for-periodic-interface.repository';
import {
ProgressReport,
ProgressReportStatus as ProgressStatus,
} from '../progress-report/dto';
import {
ProgressReportExtraForPeriodicInterfaceRepository,
progressReportExtrasSorters,
} from '../progress-report/progress-report-extra-for-periodic-interface.repository';
import {
IPeriodicReport,
MergePeriodicReports,
Expand Down Expand Up @@ -202,26 +209,7 @@ export class PeriodicReportRepository extends DtoRepository<
}

matchCurrentDue(parentId: ID | Variable, reportType: ReportType) {
return (query: Query) =>
query.comment`matchCurrentDue()`
.match([
[
node('baseNode', 'BaseNode', { id: parentId }),
relation('out', '', 'report', ACTIVE),
node('node', `${reportType}Report`),
relation('out', '', 'end', ACTIVE),
node('end', 'Property'),
],
[
node('node'),
relation('out', '', 'start', ACTIVE),
node('start', 'Property'),
],
])
.raw(`WHERE end.value < date()`)
.with('node, start')
.orderBy('start.value', 'desc')
.limit(1);
return matchCurrentDue(parentId, reportType);
}

async getByDate(
Expand Down Expand Up @@ -273,7 +261,7 @@ export class PeriodicReportRepository extends DtoRepository<
const res = await this.db
.query()
.match([
node('baseNode', 'BaseNode', { id: parentId }),
node('parent', 'BaseNode', { id: parentId }),
relation('out', '', 'report', ACTIVE),
node('node', `${reportType}Report`),
relation('out', '', 'end', ACTIVE),
Expand Down Expand Up @@ -445,3 +433,33 @@ export class PeriodicReportRepository extends DtoRepository<
);
}
}

export const matchCurrentDue =
(parentId: ID | Variable | undefined, reportType: ReportType) =>
(query: Query) =>
query.comment`matchCurrentDue()`
.match([
[
node('parent', 'BaseNode', parentId ? { id: parentId } : undefined),
relation('out', '', 'report', ACTIVE),
node('node', `${reportType}Report`),
relation('out', '', 'end', ACTIVE),
node('end', 'Property'),
],
[
node('node'),
relation('out', '', 'start', ACTIVE),
node('start', 'Property'),
],
])
.raw(`WHERE end.value < date()`)
.with('node, start')
.orderBy('start.value', 'desc')
.limit(1);

export const periodicReportSorters = defineSorters(IPeriodicReport, {});

export const progressReportSorters = defineSorters(ProgressReport, {
...periodicReportSorters.matchers,
...progressReportExtrasSorters.matchers,
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { keys as keysOf } from 'ts-transformer-keys';
import {
Calculated,
DbSort,
parentIdMiddleware,
ResourceRelationsShape,
SecuredProperty,
SecuredProps,
} from '~/common';
import { sortingForEnumIndex } from '~/core/database/query';
import { BaseNode } from '~/core/database/results';
import { e } from '~/core/edgedb';
import { RegisterResource } from '~/core/resources';
Expand All @@ -15,7 +17,10 @@ import { DefinedFile } from '../../file/dto';
import { IPeriodicReport } from '../../periodic-report/dto/periodic-report.dto';
import { ProgressReportCommunityStory } from './community-stories.dto';
import { ProgressReportHighlight } from './highlights.dto';
import { SecuredProgressReportStatus as SecuredStatus } from './progress-report-status.enum';
import {
SecuredProgressReportStatus as SecuredStatus,
ProgressReportStatus as Status,
} from './progress-report-status.enum';
import { ProgressReportTeamNews } from './team-news.dto';

@RegisterResource({ db: e.ProgressReport })
Expand Down Expand Up @@ -46,6 +51,7 @@ export class ProgressReport extends IPeriodicReport {
middleware: [parentIdMiddleware],
})
@Calculated()
@DbSort(sortingForEnumIndex(Status))
readonly status: SecuredStatus;
}

Expand Down
Loading
Loading