From b406bfd637513b844a50ddfa1e80efe482757c7b Mon Sep 17 00:00:00 2001 From: William Harris Date: Tue, 7 May 2024 17:41:12 -0700 Subject: [PATCH] [EdgeDB] EthnoArt queries --- .../ethno-art/ethno-art.edgedb.repository.ts | 56 +++++++++++ src/components/ethno-art/ethno-art.module.ts | 4 +- .../ethno-art/ethno-art.repository.ts | 96 +++++++++++++++---- src/components/ethno-art/ethno-art.service.ts | 94 +++++------------- 4 files changed, 162 insertions(+), 88 deletions(-) create mode 100644 src/components/ethno-art/ethno-art.edgedb.repository.ts diff --git a/src/components/ethno-art/ethno-art.edgedb.repository.ts b/src/components/ethno-art/ethno-art.edgedb.repository.ts new file mode 100644 index 0000000000..605aceaf2f --- /dev/null +++ b/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 +{ + async create(input: CreateEthnoArt): Promise> { + 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> { + 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), + }); + } +} diff --git a/src/components/ethno-art/ethno-art.module.ts b/src/components/ethno-art/ethno-art.module.ts index 695a93a4db..b66fbe063d 100644 --- a/src/components/ethno-art/ethno-art.module.ts +++ b/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'; @@ -11,7 +13,7 @@ import { EthnoArtService } from './ethno-art.service'; providers: [ EthnoArtLoader, EthnoArtResolver, - EthnoArtRepository, + splitDb(EthnoArtRepository, EthnoArtEdgeDBRepository), EthnoArtService, ], exports: [EthnoArtService], diff --git a/src/components/ethno-art/ethno-art.repository.ts b/src/components/ethno-art/ethno-art.repository.ts index 7989e13931..4da67b0850 100644 --- a/src/components/ethno-art/ethno-art.repository.ts +++ b/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, @@ -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, - '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; } - async list(input: EthnoArtListInput) { + async readMany( + ids: readonly ID[], + ): Promise>> { + const items = await super.readMany(ids); + return items.map((r) => ({ + ...r, + scriptureReferences: this.scriptureRefsService.parseList( + r.scriptureReferences, + ), + })); + } + + async list({ + filter, + ...input + }: EthnoArtListInput): Promise>> { 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 }>( merge('props', { scriptureReferences: 'scriptureReferences', diff --git a/src/components/ethno-art/ethno-art.service.ts b/src/components/ethno-art/ethno-art.service.ts index 257249b170..a9c3c19dcc 100644 --- a/src/components/ethno-art/ethno-art.service.ts +++ b/src/components/ethno-art/ethno-art.service.ts @@ -1,20 +1,20 @@ 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'; @@ -22,42 +22,14 @@ 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 { - 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) @@ -67,67 +39,53 @@ export class EthnoArtService { _view?: ObjectView, ): Promise { 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, - session: Session, - ): Promise { - return this.privileges.for(session, EthnoArt).secure({ - ...dto, - scriptureReferences: this.scriptureRefs.parseList( - dto.scriptureReferences, - ), - }); + private secure(dto: UnsecuredDto, session: Session): EthnoArt { + return this.privileges.for(session, EthnoArt).secure(dto); } async update(input: UpdateEthnoArt, session: Session): Promise { - 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 { - 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 { 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)), + }; } }