Skip to content

Commit a0de2b9

Browse files
committed
Merge remote-tracking branch 'origin/develop'
2 parents 7a8a65c + 8ae7d53 commit a0de2b9

25 files changed

+652
-148
lines changed

src/common/exceptions/duplicate.exception.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { ArgumentsHost } from '@nestjs/common';
2+
import {
3+
GqlContextType as ContextKey,
4+
GqlExecutionContext,
5+
} from '@nestjs/graphql';
6+
import { lowerCase, upperFirst } from 'lodash';
7+
import type { ExclusivityViolationError } from '~/core/edgedb';
18
import { InputException } from './input.exception';
29

310
/**
@@ -11,4 +18,47 @@ export class DuplicateException extends InputException {
1118
previous,
1219
);
1320
}
21+
22+
static fromDB(exception: ExclusivityViolationError, context?: ArgumentsHost) {
23+
let property = exception.property;
24+
const message = `${upperFirst(
25+
lowerCase(property),
26+
)} already exists and needs to be unique`;
27+
28+
// Attempt to add path prefix automatically to the property name, based
29+
// on given GQL input.
30+
if (context && context.getType<ContextKey>() === 'graphql') {
31+
let gqlArgs = GqlExecutionContext.create(context as any).getArgs();
32+
33+
// unwind single `input` argument, based on our own conventions
34+
if (Object.keys(gqlArgs).length === 1 && 'input' in gqlArgs) {
35+
gqlArgs = gqlArgs.input;
36+
}
37+
38+
const flattened = flattenObject(gqlArgs);
39+
// Guess the correct path based on property name.
40+
// This kinda assumes the property name will be unique amongst all the input.
41+
const guessedPath = Object.keys(flattened).find(
42+
(path) => property === path || path.endsWith('.' + property),
43+
);
44+
property = guessedPath ?? property;
45+
}
46+
47+
const ex = new DuplicateException(property, message, exception);
48+
ex.stack = exception.stack;
49+
return ex;
50+
}
1451
}
52+
53+
const flattenObject = (obj: object, prefix = '') => {
54+
const result: Record<string, any> = {};
55+
for (const [key, value] of Object.entries(obj)) {
56+
if (value && typeof value === 'object' && !Array.isArray(value)) {
57+
const nestedObj = flattenObject(value, prefix + key + '.');
58+
Object.assign(result, nestedObj);
59+
} else {
60+
result[prefix + key] = value;
61+
}
62+
}
63+
return result;
64+
};

src/common/exceptions/exception.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export const hasPrevious = (e: Error): e is Error & { previous: Error } =>
2929
export function getPreviousList(ex: Error, includeSelf: boolean) {
3030
const previous: Error[] = includeSelf ? [ex] : [];
3131
let current = ex;
32-
while (hasPrevious(current)) {
33-
current = current.previous;
32+
while (current.cause instanceof Error || hasPrevious(current)) {
33+
current = hasPrevious(current)
34+
? current.previous
35+
: (current.cause as Error);
3436
previous.push(current);
3537
}
3638
return previous;

src/components/authentication/authentication.edgedb.repository.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ export class AuthenticationEdgeDBRepository
108108
),
109109
}));
110110
const result = await this.db.run(query);
111+
if (!result) {
112+
return undefined;
113+
}
111114
return {
112115
userId: result?.user?.id,
113116
roles: result?.user?.scopedRoles ?? [],

src/components/partner/dto/create-partner.dto.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { Field, ID as IDType, InputType, ObjectType } from '@nestjs/graphql';
22
import { Transform, Type } from 'class-transformer';
33
import { Matches, ValidateNested } from 'class-validator';
44
import { uniq } from 'lodash';
5-
import { ID, IdField, IdOf, IsId, NameField } from '../../../common';
5+
import {
6+
CalendarDate,
7+
DateField,
8+
ID,
9+
IdField,
10+
IdOf,
11+
IsId,
12+
NameField,
13+
} from '../../../common';
614
import { Location } from '../../../components/location';
715
import { FieldRegion } from '../../field-region';
816
import type { Language } from '../../language';
@@ -57,6 +65,9 @@ export abstract class CreatePartner {
5765
@Field(() => [IDType], { name: 'languagesOfConsulting', nullable: true })
5866
@Transform(({ value }) => uniq(value))
5967
readonly languagesOfConsulting?: ReadonlyArray<IdOf<Language>> = [];
68+
69+
@DateField({ nullable: true })
70+
readonly startDate?: CalendarDate;
6071
}
6172

6273
@InputType()

src/components/partner/dto/partner.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ResourceRelationsShape,
1414
Secured,
1515
SecuredBoolean,
16+
SecuredDateNullable,
1617
SecuredEnumList,
1718
SecuredProperty,
1819
SecuredProps,
@@ -88,6 +89,9 @@ export class Partner extends Interfaces {
8889
Secured<ReadonlyArray<IdOf<Language>>>
8990
>;
9091

92+
@Field()
93+
readonly startDate: SecuredDateNullable;
94+
9195
@DateTimeField()
9296
readonly modifiedAt: DateTime;
9397

src/components/partner/dto/update-partner.dto.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { Field, ID as IDType, InputType, ObjectType } from '@nestjs/graphql';
22
import { Transform, Type } from 'class-transformer';
33
import { Matches, ValidateNested } from 'class-validator';
44
import { uniq } from 'lodash';
5-
import { ID, IdField, IdOf, IsId, NameField } from '../../../common';
5+
import {
6+
CalendarDate,
7+
DateField,
8+
ID,
9+
IdField,
10+
IdOf,
11+
IsId,
12+
NameField,
13+
} from '../../../common';
614
import { Location } from '../../../components/location';
715
import { FieldRegion } from '../../field-region';
816
import type { Language } from '../../language';
@@ -57,6 +65,9 @@ export abstract class UpdatePartner {
5765
@Field(() => [IDType], { name: 'languagesOfConsulting', nullable: true })
5866
@Transform(({ value }) => (value ? uniq(value) : undefined))
5967
readonly languagesOfConsulting?: ReadonlyArray<IdOf<Language>>;
68+
69+
@DateField({ nullable: true })
70+
readonly startDate?: CalendarDate | null;
6071
}
6172

6273
@InputType()

src/components/partner/partner.repository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { node, Query, relation } from 'cypher-query-builder';
33
import { DateTime } from 'luxon';
44
import { ChangesOf } from '~/core/database/changes';
55
import {
6+
CalendarDate,
67
ID,
78
InputException,
89
ServerException,
@@ -54,6 +55,7 @@ export class PartnerRepository extends DtoRepository<
5455
pmcEntityCode: input.pmcEntityCode,
5556
globalInnovationsClient: input.globalInnovationsClient,
5657
active: input.active,
58+
startDate: input.startDate ?? CalendarDate.local(),
5759
address: input.address,
5860
modifiedAt: DateTime.local(),
5961
canDelete: true,

src/components/periodic-report/periodic-report.repository.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class PeriodicReportRepository extends DtoRepository<
182182
const filters = { type, parent, start, end };
183183
const result = await this.db
184184
.query()
185-
.matchNode('node', `${type || ''}Report`)
185+
.matchNode('node', 'PeriodicReport')
186186
.apply(
187187
filter.builder(filters, {
188188
parent: filter.pathExists((id) => [
@@ -192,8 +192,7 @@ export class PeriodicReportRepository extends DtoRepository<
192192
]),
193193
start: filter.dateTimeProp(),
194194
end: filter.dateTimeProp(),
195-
//TODO: determine how to fix this type filter
196-
type: ({ value }) => hasLabel(`${value}Report`),
195+
type: ({ value }) => ({ node: hasLabel(`${value}Report`) }),
197196
}),
198197
)
199198
.apply(sorting(resource, input))

src/components/project/project-member/project-member.repository.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ export class ProjectMemberRepository extends DtoRepository<
117117
node('user', 'User'),
118118
])
119119
.subQuery('user', (sub) =>
120-
sub.with('user as node').apply(this.users.hydrate(session.userId)),
120+
sub
121+
.with('user as node')
122+
.apply(this.users.hydrateAsNeo4j(session.userId)),
121123
)
122124
.return<{ dto: UnsecuredDto<ProjectMember> }>(
123125
merge('props', { user: 'dto' }).as('dto'),
Lines changed: 26 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,43 @@
11
import { Injectable } from '@nestjs/common';
2+
import { NotImplementedException, PublicOf } from '~/common';
3+
import { e, RepoFor, ScopeOf } from '~/core/edgedb';
24
import {
3-
DuplicateException,
4-
ID,
5-
NotFoundException,
6-
ServerException,
7-
Session,
8-
} from '~/common';
9-
import { e, EdgeDB, isExclusivityViolation } from '~/core/edgedb';
10-
import { CreatePerson, User, UserListInput } from './dto';
11-
import { UserRepository } from './user.repository';
12-
13-
const hydrate = e.shape(e.User, (user) => ({
14-
...user['*'],
15-
// Other links if needed
16-
}));
5+
AssignOrganizationToUser,
6+
RemoveOrganizationFromUser,
7+
User,
8+
UserListInput,
9+
} from './dto';
10+
import type { UserRepository } from './user.repository';
1711

1812
@Injectable()
19-
export class UserEdgedbRepository extends UserRepository {
20-
constructor(private readonly edgedb: EdgeDB) {
21-
super();
22-
}
23-
24-
async readOne(id: ID, _session: Session | ID) {
25-
const query = e.select(e.User, (user) => ({
26-
...hydrate(user),
27-
filter_single: { id },
28-
}));
29-
const user = await this.edgedb.run(query);
30-
if (!user) {
31-
throw new NotFoundException('Could not find user');
32-
}
33-
return user;
34-
}
35-
36-
async readMany(ids: readonly ID[], _session: Session | ID) {
37-
const query = e.params({ ids: e.array(e.uuid) }, ({ ids }) =>
38-
e.select(e.User, (user) => ({
39-
...hydrate(user),
40-
filter: e.op(user.id, 'in', e.array_unpack(ids)),
41-
})),
42-
);
43-
const users = await this.edgedb.run(query, { ids });
44-
return users;
45-
}
46-
13+
export class UserEdgeDBRepository
14+
extends RepoFor(User).withDefaults()
15+
implements PublicOf<UserRepository>
16+
{
4717
async doesEmailAddressExist(email: string) {
4818
const query = e.select(e.User, () => ({
4919
filter_single: { email },
5020
}));
51-
const result = await this.edgedb.run(query);
21+
const result = await this.db.run(query);
5222
return !!result;
5323
}
5424

55-
async list(input: UserListInput, _session: Session) {
56-
const sortKey = input.sort as keyof (typeof e.User)['*'];
57-
const all = e.select(e.User, (user) => ({
58-
filter: e.all(
59-
input.filter.pinned != null
60-
? e.op(user.pinned, '=', input.filter.pinned)
61-
: true,
62-
// More filters here when needed...
63-
),
64-
order_by: {
65-
expression: user[sortKey],
66-
direction: input.order,
67-
},
68-
}));
69-
const thisPage = e.select(all, () => ({
70-
offset: (input.page - 1) * input.count,
71-
limit: input.count + 1,
72-
}));
73-
const query = e.select({
74-
items: e.select(thisPage, (user) => ({
75-
...hydrate(user),
76-
limit: input.count,
77-
})),
78-
total: e.count(all),
79-
hasMore: e.op(e.count(thisPage), '>', input.count),
80-
});
81-
return await this.edgedb.run(query);
25+
protected listFilters(user: ScopeOf<typeof e.User>, input: UserListInput) {
26+
return [
27+
input.filter.pinned != null &&
28+
e.op(user.pinned, '=', input.filter.pinned),
29+
// More filters here when needed...
30+
];
8231
}
8332

84-
async create(input: CreatePerson) {
85-
const query = e.insert(e.User, { ...input });
86-
try {
87-
const result = await this.edgedb.run(query);
88-
return result.id;
89-
} catch (e) {
90-
if (isExclusivityViolation(e, 'email')) {
91-
throw new DuplicateException(
92-
'person.email',
93-
'Email address is already in use',
94-
e,
95-
);
96-
}
97-
throw new ServerException('Failed to create user', e);
98-
}
33+
assignOrganizationToUser(args: AssignOrganizationToUser): Promise<void> {
34+
throw new NotImplementedException().with(args);
35+
}
36+
removeOrganizationFromUser(args: RemoveOrganizationFromUser): Promise<void> {
37+
throw new NotImplementedException().with(args);
9938
}
10039

101-
async delete(id: ID, _session: Session, _object: User): Promise<void> {
102-
const query = e.delete(e.User, () => ({
103-
filter_single: { id },
104-
}));
105-
try {
106-
await this.edgedb.run(query);
107-
} catch (exception) {
108-
throw new ServerException('Failed to delete', exception);
109-
}
40+
hydrateAsNeo4j(_session: unknown): any {
41+
throw new NotImplementedException();
11042
}
11143
}

0 commit comments

Comments
 (0)