Skip to content

Commit

Permalink
[EdgeDB] EthnoArt queries (#3205)
Browse files Browse the repository at this point in the history
  • Loading branch information
willdch committed May 9, 2024
1 parent 70685db commit ab7ad29
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 88 deletions.
56 changes: 56 additions & 0 deletions src/components/ethno-art/ethno-art.edgedb.repository.ts
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { PublicOf, UnsecuredDto } from '~/common';
import { e, RepoFor } from '~/core/edgedb';
import * as scripture from '../scripture/edgedb.utils';
import { CreateEthnoArt, EthnoArt, UpdateEthnoArt } from './dto';
import { EthnoArtRepository } from './ethno-art.repository';

@Injectable()
export class EthnoArtEdgeDBRepository
extends RepoFor(EthnoArt, {
hydrate: (ethnoArt) => ({
...ethnoArt['*'],
scriptureReferences: scripture.hydrate(ethnoArt.scripture),
}),
omit: ['create', 'update'],
})
implements PublicOf<EthnoArtRepository>
{
async create(input: CreateEthnoArt): Promise<UnsecuredDto<EthnoArt>> {
const query = e.params(
{ name: e.str, scripture: e.optional(scripture.type) },
($) => {
const created = e.insert(this.resource.db, {
name: $.name,
scripture: scripture.insert($.scripture),
});
return e.select(created, this.hydrate);
},
);
return await this.db.run(query, {
name: input.name,
scripture: scripture.valueOptional(input.scriptureReferences),
});
}

async update({
id,
...changes
}: UpdateEthnoArt): Promise<UnsecuredDto<EthnoArt>> {
const query = e.params({ scripture: e.optional(scripture.type) }, ($) => {
const ethnoArt = e.cast(e.EthnoArt, e.uuid(id));
const updated = e.update(ethnoArt, () => ({
set: {
...(changes.name ? { name: changes.name } : {}),
...(changes.scriptureReferences !== undefined
? { scripture: scripture.insert($.scripture) }
: {}),
},
}));
return e.select(updated, this.hydrate);
});
return await this.db.run(query, {
scripture: scripture.valueOptional(changes.scriptureReferences),
});
}
}
4 changes: 3 additions & 1 deletion src/components/ethno-art/ethno-art.module.ts
@@ -1,6 +1,8 @@
import { forwardRef, Module } from '@nestjs/common';
import { splitDb } from '~/core';
import { AuthorizationModule } from '../authorization/authorization.module';
import { ScriptureModule } from '../scripture/scripture.module';
import { EthnoArtEdgeDBRepository } from './ethno-art.edgedb.repository';
import { EthnoArtLoader } from './ethno-art.loader';
import { EthnoArtRepository } from './ethno-art.repository';
import { EthnoArtResolver } from './ethno-art.resolver';
Expand All @@ -11,7 +13,7 @@ import { EthnoArtService } from './ethno-art.service';
providers: [
EthnoArtLoader,
EthnoArtResolver,
EthnoArtRepository,
splitDb(EthnoArtRepository, EthnoArtEdgeDBRepository),
EthnoArtService,
],
exports: [EthnoArtService],
Expand Down
96 changes: 77 additions & 19 deletions src/components/ethno-art/ethno-art.repository.ts
@@ -1,16 +1,25 @@
import { Injectable } from '@nestjs/common';
import { Query } from 'cypher-query-builder';
import { ChangesOf } from '~/core/database/changes';
import { ID } from '../../common';
import { DbTypeOf, DtoRepository } from '../../core';
import {
DuplicateException,
ID,
PaginatedListType,
ServerException,
Session,
UnsecuredDto,
} from '~/common';
import { DbTypeOf, DtoRepository } from '~/core';
import {
createNode,
matchProps,
merge,
paginate,
sorting,
} from '../../core/database/query';
import { ScriptureReferenceRepository } from '../scripture';
} from '~/core/database/query';
import {
ScriptureReferenceRepository,
ScriptureReferenceService,
} from '../scripture';
import {
CreateEthnoArt,
EthnoArt,
Expand All @@ -20,46 +29,95 @@ import {

@Injectable()
export class EthnoArtRepository extends DtoRepository(EthnoArt) {
constructor(private readonly scriptureRefs: ScriptureReferenceRepository) {
constructor(
private readonly scriptureRefsRepository: ScriptureReferenceRepository,
private readonly scriptureRefsService: ScriptureReferenceService,
) {
super();
}
async create(input: CreateEthnoArt) {

async create(input: CreateEthnoArt, session: Session) {
if (!(await this.isUnique(input.name))) {
throw new DuplicateException(
'ethnoArt.name',
'Ethno art with this name already exists',
);
}

const initialProps = {
name: input.name,
canDelete: true,
};
return await this.db
const result = await this.db
.query()
.apply(await createNode(EthnoArt, { initialProps }))
.return<{ id: ID }>('node.id as id')
.first();

if (!result) {
throw new ServerException('Failed to create ethno art');
}

await this.scriptureRefsService.create(
result.id,
input.scriptureReferences,
session,
);

return await this.readOne(result.id);
}

async update(
existing: EthnoArt,
simpleChanges: Omit<
ChangesOf<EthnoArt, UpdateEthnoArt>,
'scriptureReferences'
>,
) {
await this.updateProperties(existing, simpleChanges);
async update(input: UpdateEthnoArt) {
const { id, name, scriptureReferences } = input;
await this.updateProperties({ id }, { name });
if (scriptureReferences !== undefined) {
await this.scriptureRefsService.update(id, scriptureReferences);
}
return await this.readOne(input.id);
}

async readOne(id: ID) {
return (await super.readOne(id)) as UnsecuredDto<EthnoArt>;
}

async list(input: EthnoArtListInput) {
async readMany(
ids: readonly ID[],
): Promise<ReadonlyArray<UnsecuredDto<EthnoArt>>> {
const items = await super.readMany(ids);
return items.map((r) => ({
...r,
scriptureReferences: this.scriptureRefsService.parseList(
r.scriptureReferences,
),
}));
}

async list({
filter,
...input
}: EthnoArtListInput): Promise<PaginatedListType<UnsecuredDto<EthnoArt>>> {
const result = await this.db
.query()
.matchNode('node', 'EthnoArt')
.apply(sorting(EthnoArt, input))
.apply(paginate(input, this.hydrate()))
.first();
return result!; // result from paginate() will always have 1 row.
return {
...result!,
items: result!.items.map((r) => ({
...r,
scriptureReferences: this.scriptureRefsService.parseList(
r.scriptureReferences,
),
})),
};
}

protected hydrate() {
return (query: Query) =>
query
.apply(matchProps())
.subQuery('node', this.scriptureRefs.list())
.subQuery('node', this.scriptureRefsRepository.list())
.return<{ dto: DbTypeOf<EthnoArt> }>(
merge('props', {
scriptureReferences: 'scriptureReferences',
Expand Down
94 changes: 26 additions & 68 deletions src/components/ethno-art/ethno-art.service.ts
@@ -1,63 +1,35 @@
import { Injectable } from '@nestjs/common';
import {
DuplicateException,
ID,
ObjectView,
ServerException,
Session,
} from '../../common';
import { DbTypeOf, HandleIdLookup, ILogger, Logger } from '../../core';
import { ifDiff } from '../../core/database/changes';
import { mapListResults } from '../../core/database/results';
UnsecuredDto,
} from '~/common';
import { HandleIdLookup } from '~/core';
import { ifDiff } from '~/core/database/changes';
import { Privileges } from '../authorization';
import { isScriptureEqual, ScriptureReferenceService } from '../scripture';
import { isScriptureEqual } from '../scripture';
import {
CreateEthnoArt,
EthnoArt,
EthnoArtListInput,
EthnoArtListOutput,
UpdateEthnoArt,
} from './dto';
import { EthnoArtRepository } from './ethno-art.repository';

@Injectable()
export class EthnoArtService {
constructor(
@Logger('ethno-art:service') private readonly logger: ILogger,
private readonly scriptureRefs: ScriptureReferenceService,
private readonly privileges: Privileges,
private readonly repo: EthnoArtRepository,
) {}

async create(input: CreateEthnoArt, session: Session): Promise<EthnoArt> {
this.privileges.for(session, EthnoArt).verifyCan('create');
if (!(await this.repo.isUnique(input.name))) {
throw new DuplicateException(
'ethnoArt.name',
'Ethno art with this name already exists',
);
}

try {
const result = await this.repo.create(input);

if (!result) {
throw new ServerException('Failed to create ethno art');
}

await this.scriptureRefs.create(
result.id,
input.scriptureReferences,
session,
);

this.logger.debug(`ethno art created`, { id: result.id });
return await this.readOne(result.id, session);
} catch (exception) {
this.logger.error('Could not create ethno art', {
exception,
});
throw new ServerException('Could not create ethno art', exception);
}
const dto = await this.repo.create(input, session);
this.privileges.for(session, EthnoArt, dto).verifyCan('create');
return this.secure(dto, session);
}

@HandleIdLookup(EthnoArt)
Expand All @@ -67,67 +39,53 @@ export class EthnoArtService {
_view?: ObjectView,
): Promise<EthnoArt> {
const result = await this.repo.readOne(id);
return await this.secure(result, session);
return this.secure(result, session);
}

async readMany(ids: readonly ID[], session: Session) {
const ethnoArt = await this.repo.readMany(ids);
return await Promise.all(ethnoArt.map((dto) => this.secure(dto, session)));
return ethnoArt.map((dto) => this.secure(dto, session));
}

private async secure(
dto: DbTypeOf<EthnoArt>,
session: Session,
): Promise<EthnoArt> {
return this.privileges.for(session, EthnoArt).secure({
...dto,
scriptureReferences: this.scriptureRefs.parseList(
dto.scriptureReferences,
),
});
private secure(dto: UnsecuredDto<EthnoArt>, session: Session): EthnoArt {
return this.privileges.for(session, EthnoArt).secure(dto);
}

async update(input: UpdateEthnoArt, session: Session): Promise<EthnoArt> {
const ethnoArt = await this.readOne(input.id, session);

const ethnoArt = await this.repo.readOne(input.id);
const changes = {
...this.repo.getActualChanges(ethnoArt, input),
scriptureReferences: ifDiff(isScriptureEqual)(
input.scriptureReferences,
ethnoArt.scriptureReferences.value,
ethnoArt.scriptureReferences,
),
};

this.privileges.for(session, EthnoArt, ethnoArt).verifyChanges(changes);

const { scriptureReferences, ...simpleChanges } = changes;

await this.scriptureRefs.update(input.id, scriptureReferences);

await this.repo.update(ethnoArt, simpleChanges);

return await this.readOne(input.id, session);
const updated = await this.repo.update({ id: input.id, ...changes });
return this.secure(updated, session);
}

async delete(id: ID, session: Session): Promise<void> {
const ethnoArt = await this.readOne(id, session);
const ethnoArt = await this.repo.readOne(id);

this.privileges.for(session, EthnoArt, ethnoArt).verifyCan('delete');

try {
await this.repo.deleteNode(ethnoArt);
} catch (exception) {
this.logger.error('Failed to delete', { id, exception });
throw new ServerException('Failed to delete', exception);
}

this.logger.debug(`deleted ethnoArt with id`, { id });
}

async list(input: EthnoArtListInput, session: Session) {
// -- don't need a check for canList. all roles are allowed to see at least one prop,
// and this isn't a sensitive component.
async list(
input: EthnoArtListInput,
session: Session,
): Promise<EthnoArtListOutput> {
const results = await this.repo.list(input);
return await mapListResults(results, (dto) => this.secure(dto, session));
return {
...results,
items: results.items.map((dto) => this.secure(dto, session)),
};
}
}

0 comments on commit ab7ad29

Please sign in to comment.