Skip to content
Draft
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
15 changes: 15 additions & 0 deletions dbschema/migrations/00023-m1zewki.edgeql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions dbschema/organization.gel
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ module default {
address: str;
multi types: Organization::Type;
multi reach: Organization::Reach;
multi joinedAlliances: Organization::AllianceMembership;
multi allianceMembers: Organization::AllianceMembership;

multi locations: Location;
parent: Organization;

overloaded link projectContext: Project::Context {
default := (insert Project::Context);
Expand All @@ -34,4 +37,11 @@ module Organization {
National,
`Global`
>;

type AllianceMembership {
required link member: default::Organization;
required link alliance: default::Organization;
required joinedAt: cal::local_date;
constraint exclusive on ((.member, .alliance));
}
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import process from 'node:process';
import { AdminModule } from './components/admin/admin.module';
import { AllianceMembershipModule } from './components/alliance-membership/alliance-membership.module';
import { AuthorizationModule } from './components/authorization/authorization.module';
import { BudgetModule } from './components/budget/budget.module';
import { CeremonyModule } from './components/ceremony/ceremony.module';
Expand Down Expand Up @@ -88,6 +89,7 @@ if (process.env.NODE_ENV !== 'production') {
NotificationModule,
SystemNotificationModule,
FinanceDepartmentModule,
AllianceMembershipModule,
],
})
export class AppModule {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { CalendarDate, type PublicOf, type UnsecuredDto } from '~/common';
import { e, RepoFor } from '~/core/gel';
import { type AllianceMembershipRepository } from './alliance-membership.repository';
import { AllianceMembership, type CreateAllianceMembership } from './dto';

@Injectable()
export class AllianceMembershipGelRepository
extends RepoFor(AllianceMembership, {
hydrate: (allianceMembership) => ({
__typename: e.str('AllianceMembership'),
...allianceMembership['*'],
member: true,
alliance: true,
}),
omit: ['create'],
})
implements PublicOf<AllianceMembershipRepository>
{
async create(
input: CreateAllianceMembership,
): Promise<UnsecuredDto<AllianceMembership>> {
const joinedAt = input.joinedAt ?? CalendarDate.local();

const query = e.params(
{
memberId: e.uuid,
allianceId: e.uuid,
joinedAt: e.cal.local_date,
},
($) => {
const member = e.cast(e.Organization, $.memberId);
const alliance = e.cast(e.Organization, $.allianceId);

const created = e.insert(this.resource.db, {
member,
alliance,
joinedAt: $.joinedAt,
});

return e.select(created, this.hydrate);
},
);

return await this.db.run(query, {
memberId: input.memberId,
allianceId: input.allianceId,
joinedAt,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type ID } from '~/common';
import { type DataLoaderStrategy, LoaderFactory } from '~/core/data-loader';
import { AllianceMembershipService } from './alliance-membership.service';
import { AllianceMembership } from './dto';

@LoaderFactory(() => AllianceMembership)
export class AllianceMembershipLoader
implements DataLoaderStrategy<AllianceMembership, ID<AllianceMembership>>
{
constructor(
private readonly allianceMemberships: AllianceMembershipService,
) {}

async loadMany(ids: ReadonlyArray<ID<AllianceMembership>>) {
return await this.allianceMemberships.readMany(ids);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { AuthorizationModule } from '../authorization/authorization.module';
import { AllianceMembershipLoader } from './alliance-membership.loader';
import { AllianceMembershipRepository } from './alliance-membership.repository';
import { AllianceMembershipResolver } from './alliance-membership.resolver';
import { AllianceMembershipService } from './alliance-membership.service';

@Module({
imports: [AuthorizationModule],
providers: [
AllianceMembershipResolver,
AllianceMembershipService,
AllianceMembershipRepository,
AllianceMembershipLoader,
],
exports: [AllianceMembershipService],
})
export class AllianceMembershipModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { node, type Query, relation } from 'cypher-query-builder';
import {
CreationFailed,
type ID,
NotFoundException,
ReadAfterCreationFailed,
type UnsecuredDto,
} from '~/common';
import { DtoRepository } from '~/core/database';
import {
ACTIVE,
createNode,
createRelationships,
matchProps,
merge,
} from '~/core/database/query';
import { AllianceMembership, type CreateAllianceMembership } from './dto';

@Injectable()
export class AllianceMembershipRepository extends DtoRepository(
AllianceMembership,
) {
async create(input: CreateAllianceMembership) {
const initialProps = {
joinedAt: input.joinedAt,
canDelete: true,
};

const result = await this.db
.query()
.apply(
await createNode(AllianceMembership, {
initialProps,
}),
)
.apply(
createRelationships(AllianceMembership, 'out', {
member: ['Organization', input.memberId],
alliance: ['Organization', input.allianceId],
}),
)
.return<{ id: ID }>('node.id as id')
.first();

if (!result) {
throw new CreationFailed(AllianceMembership);
}

return await this.readOne(result.id).catch((e) => {
throw e instanceof NotFoundException
? new ReadAfterCreationFailed(AllianceMembership)
: e;
});
}

protected hydrate() {
return (query: Query) =>
query
.apply(matchProps())
.optionalMatch([
node('node'),
relation('out', '', 'member', ACTIVE),
node('member', 'Organization'),
])
.optionalMatch([
node('node'),
relation('out', '', 'alliance', ACTIVE),
node('alliance', 'Organization'),
])
.return<{ dto: UnsecuredDto<AllianceMembership> }>(
merge('props', {
member: 'member { .id}',
alliance: 'alliance { .id}',
}).as('dto'),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
Args,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { ID, IdArg, mapSecuredValue } from '~/common';

Check failure on line 9 in src/components/alliance-membership/alliance-membership.resolver.ts

View workflow job for this annotation

GitHub Actions / lint

'ID' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
import { Loader, LoaderOf } from '~/core';

Check failure on line 10 in src/components/alliance-membership/alliance-membership.resolver.ts

View workflow job for this annotation

GitHub Actions / lint

'LoaderOf' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
import { OrganizationLoader } from '../organization';
import { SecuredOrganization } from '../organization/dto';
import { AllianceMembershipLoader } from './alliance-membership.loader';
import { AllianceMembershipService } from './alliance-membership.service';
import { AllianceMembership } from './dto/alliance-membership.dto';
import {
CreateAllianceMembershipInput,
CreateAllianceMembershipOutput,
} from './dto/create-alliance-membership.dto';
import { DeleteAllianceMembershipOutput } from './dto/delete-alliance-membership.dto';

@Resolver(AllianceMembership)
export class AllianceMembershipResolver {
constructor(private readonly service: AllianceMembershipService) {}

@Query(() => AllianceMembership, {
description: 'Read one field zone by id',
})
async allianceMembership(
@Loader(AllianceMembershipLoader)
allianceMemberships: LoaderOf<AllianceMembershipLoader>,
@IdArg() id: ID,
): Promise<AllianceMembership> {
return await allianceMemberships.load(id);
}

@Mutation(() => CreateAllianceMembershipOutput, {
description: 'Create an alliance membership',
})
async createAllianceMembership(
@Args('input') { allianceMembership: input }: CreateAllianceMembershipInput,
): Promise<CreateAllianceMembershipOutput> {
const allianceMembership = await this.service.create(input);
return { allianceMembership };
}

@Mutation(() => DeleteAllianceMembershipOutput, {
description: 'Delete an alliance membership',
})
async deleteAllianceMembership(
@IdArg() id: ID,
): Promise<DeleteAllianceMembershipOutput> {
await this.service.delete(id);
return { success: true };
}

@ResolveField(() => SecuredOrganization)
async member(
@Parent() allianceMembership: AllianceMembership,
@Loader(OrganizationLoader) organizations: LoaderOf<OrganizationLoader>,
): Promise<SecuredOrganization> {
return await mapSecuredValue(allianceMembership.member, ({ id }) =>
organizations.load(id),
);
}

@ResolveField(() => SecuredOrganization)
async alliance(
@Parent() allianceMembership: AllianceMembership,
@Loader(OrganizationLoader) organizations: LoaderOf<OrganizationLoader>,
): Promise<SecuredOrganization> {
return await mapSecuredValue(allianceMembership.alliance, ({ id }) =>
organizations.load(id),
);
}
}
Loading
Loading