Skip to content

[EdgeDB] EthnoArt queries #3205

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

Merged
merged 1 commit into from
May 9, 2024
Merged
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
56 changes: 56 additions & 0 deletions src/components/ethno-art/ethno-art.edgedb.repository.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)),
};
}
}
Loading