diff --git a/src/components/product/dto/product.dto.ts b/src/components/product/dto/product.dto.ts index dac2d367de..635072af2b 100644 --- a/src/components/product/dto/product.dto.ts +++ b/src/components/product/dto/product.dto.ts @@ -20,7 +20,7 @@ import { import { SetDbType } from '~/core/database'; import { SetChangeType } from '~/core/database/changes'; import { e } from '~/core/edgedb'; -import { RegisterResource } from '~/core/resources'; +import { LinkTo, RegisterResource } from '~/core/resources'; import { DbScriptureReferences } from '../../scripture'; import { ScriptureRangeInput, @@ -54,8 +54,8 @@ export class Product extends Producible { static readonly Parent = () => import('../../engagement/dto').then((m) => m.LanguageEngagement); - readonly engagement: ID; - readonly project: ID; + readonly engagement: Secured>; + readonly project: Secured>; @Field() @DbLabel('ProductMedium') @@ -269,6 +269,12 @@ declare module '../dto/producible.dto' { } } +export const ProductConcretes = { + DirectScriptureProduct, + DerivativeScriptureProduct, + OtherProduct, +}; + declare module '~/core/resources/map' { interface ResourceMap { Product: typeof Product; diff --git a/src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts b/src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts index 681527e7e5..d8653af5c4 100644 --- a/src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts +++ b/src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts @@ -25,14 +25,14 @@ export class FixNaNTotalVerseEquivalentsMigration extends BaseMigration { .map('id') .run(); - const products = await this.productService.readManyUnsecured( + const products = await this.productService.readMany( ids, this.fakeAdminSession, ); for (const p of products) { const correctTotalVerseEquivalent = getTotalVerseEquivalents( - ...p.scriptureReferences, + ...p.scriptureReferences.value, ); if (p.__typename === 'DirectScriptureProduct') { diff --git a/src/components/product/product.edgedb.repository.ts b/src/components/product/product.edgedb.repository.ts new file mode 100644 index 0000000000..11eaec26df --- /dev/null +++ b/src/components/product/product.edgedb.repository.ts @@ -0,0 +1,230 @@ +import { Injectable, Type } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { LazyGetter } from 'lazy-get-decorator'; +import { ID, PublicOf, Session } from '../../common'; +import { grabInstances } from '../../common/instance-maps'; +import { e, RepoFor } from '../../core/edgedb'; +import { + ProductConcretes as ConcreteTypes, + CreateDerivativeScriptureProduct, + CreateDirectScriptureProduct, + CreateOtherProduct, + Product, +} from './dto'; +import { ProductRepository } from './product.repository'; + +// scriptureReferencesOverride, scriptureReferences + +const baseHydrate = e.shape(e.Product, (product) => ({ + ...product['*'], + __typename: product.__type__.name, + project: { + id: true, + status: true, + type: true, + }, + engagement: { + id: true, + status: true, + }, + parent: e.tuple({ + identity: product.engagement.id, + labels: e.array_agg(e.set(product.engagement.__type__.name.slice(9, null))), + properties: e.tuple({ + id: product.engagement.id, + createdAt: product.engagement.createdAt, + }), + }), + pnpIndex: true, + scriptureReferences: product.scripture, +})); + +const directScriptureExtraHydrate = { + totalVerses: true, + totalVerseEquivalents: true, +} as const; + +const derivativeScriptureExtraHydrate = { + scripture: true, + composite: true, + totalVerses: true, + totalVerseEquivalents: true, +} as const; + +const otherExtraHydrate = { + title: true, + description: true, +} as const; + +const directScriptureProductHydrate = e.shape( + e.DirectScriptureProduct, + (dsp) => ({ + ...baseHydrate(dsp), + __typename: dsp.__type__.name, + unspecifiedScripture: { + book: true, + totalVerses: true, + }, + //TODO - remove after migration + unspecifiedScripturePortion: { + book: true, + totalVerses: true, + }, + ...directScriptureExtraHydrate, + }), +); + +const derivativeScriptureProductHydrate = e.shape( + e.DerivativeScriptureProduct, + (dsp) => ({ + ...baseHydrate(dsp), + __typename: dsp.__type__.name, + scriptureReferencesOverride: dsp.scriptureOverride, + produces: { + scriptureReferences: e.tuple([dsp.produces.scripture]), + createdAt: dsp.produces.createdAt, + id: dsp.produces.id, + }, + ...derivativeScriptureExtraHydrate, + }), +); + +const otherProductHydrate = e.shape(e.OtherProduct, (op) => ({ + ...baseHydrate(op), + __typename: op.__type__.name, + scriptureReferencesOverride: false, //TODO - remove after migration + ...otherExtraHydrate, +})); + +const hydrate = e.shape(e.Product, (product) => ({ + ...baseHydrate(product), + ...e.is(e.DirectScriptureProduct, directScriptureExtraHydrate), + ...e.is(e.DerivativeScriptureProduct, derivativeScriptureExtraHydrate), + ...e.is(e.OtherProduct, otherExtraHydrate), +})); + +export const ConcreteRepos = { + DirectScriptureProduct: class DirectScriptureProductRepository extends RepoFor( + ConcreteTypes.DirectScriptureProduct, + { + hydrate: directScriptureProductHydrate, + omit: ['create'], + }, + ) { + async create(input: CreateDirectScriptureProduct) { + const engagement = e.cast( + e.LanguageEngagement, + e.uuid(input.engagementId), + ); + return await this.defaults.create({ + ...input, + projectContext: engagement.projectContext, + }); + } + }, + + DerivativeScriptureProduct: class DerivativeScriptureProductRepository extends RepoFor( + ConcreteTypes.DerivativeScriptureProduct, + { + hydrate: derivativeScriptureProductHydrate, + omit: ['create'], + }, + ) { + async create(input: CreateDerivativeScriptureProduct) { + const engagement = e.cast( + e.LanguageEngagement, + e.uuid(input.engagementId), + ); + return await this.defaults.create({ + ...input, + projectContext: engagement.projectContext, + }); + } + }, + + OtherProduct: class OtherProductRepository extends RepoFor( + ConcreteTypes.OtherProduct, + { + hydrate: otherProductHydrate, + omit: ['create'], + }, + ) { + async create(input: CreateOtherProduct) { + const engagement = e.cast( + e.LanguageEngagement, + e.uuid(input.engagementId), + ); + return await this.defaults.create({ + ...input, + projectContext: engagement.projectContext, + }); + } + }, +} satisfies Record; + +@Injectable() +export class ProductEdgedbRepository + extends RepoFor(Product, { + hydrate, + omit: ['create'], + }) + implements PublicOf +{ + constructor(private readonly moduleRef: ModuleRef) { + super(); + } + + @LazyGetter() protected get concretes() { + return grabInstances(this.moduleRef, ConcreteRepos); + } + + async createDerivative( + input: CreateDerivativeScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + _session: Session, + ) { + return await this.concretes.DerivativeScriptureProduct.create(input); + } + + async createDirect( + input: CreateDirectScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + _session: Session, + ) { + return await this.concretes.DirectScriptureProduct.create(input); + } + + async createOther(input: CreateOtherProduct, _session: Session) { + return await this.concretes.OtherProduct.create(input); + } + + async listIdsAndScriptureRefs(engagementId: ID) { + const engagement = e.cast(e.LanguageEngagement, e.uuid(engagementId)); + const query = e.select(e.DirectScriptureProduct, (dsp) => ({ + id: true, + pnpIndex: true, + scriptureRanges: dsp.scripture, + unspecifiedScripture: dsp.unspecifiedScripture, + filter: e.op(dsp.engagement, '=', engagement), + })); + + return await this.db.run(query); + } + + async listIdsWithPnpIndexes(engagementId: ID, _type?: string) { + const engagement = e.cast(e.LanguageEngagement, e.uuid(engagementId)); + + const query = e.select(e.Product, (p) => ({ + id: true, + pnpIndex: p.pnpIndex, + ...e.is(e.DirectScriptureProduct, {}), + filter: e.op(p.engagement, '=', engagement), + })); + + return await this.db.run(query); + } +} diff --git a/src/components/product/product.repository.ts b/src/components/product/product.repository.ts index 27aa837c91..efc9a75a11 100644 --- a/src/components/product/product.repository.ts +++ b/src/components/product/product.repository.ts @@ -9,16 +9,18 @@ import { relation, } from 'cypher-query-builder'; import { DateTime } from 'luxon'; -import { Except, Merge } from 'type-fest'; +import { Merge } from 'type-fest'; import { getDbClassLabels, ID, + NotFoundException, Range, ServerException, Session, + UnsecuredDto, } from '~/common'; import { CommonRepository, DbTypeOf, OnIndex } from '~/core/database'; -import { DbChanges, getChanges } from '~/core/database/changes'; +import { getChanges } from '~/core/database/changes'; import { ACTIVE, collect, @@ -34,7 +36,12 @@ import { paginate, sorting, } from '~/core/database/query'; -import { ScriptureReferenceRepository } from '../scripture'; +import { ResourceResolver } from '../../core'; +import { BaseNode } from '../../core/database/results'; +import { + ScriptureReferenceRepository, + ScriptureReferenceService, +} from '../scripture'; import { ScriptureRange, ScriptureRangeInput, @@ -42,6 +49,7 @@ import { UnspecifiedScripturePortionInput, } from '../scripture/dto'; import { + AnyProduct, ApproachToMethodologies, CreateDerivativeScriptureProduct, CreateDirectScriptureProduct, @@ -57,7 +65,9 @@ import { ProductFilters, ProductListInput, ProgressMeasurement, + UpdateDerivativeScriptureProduct, UpdateDirectScriptureProduct, + UpdateOtherProduct, } from './dto'; export type HydratedProductRow = Merge< @@ -76,10 +86,32 @@ export type HydratedProductRow = Merge< @Injectable() export class ProductRepository extends CommonRepository { - constructor(private readonly scriptureRefs: ScriptureReferenceRepository) { + constructor( + private readonly scriptureRefs: ScriptureReferenceRepository, + private readonly scriptureRefService: ScriptureReferenceService, + private readonly resourceResolver: ResourceResolver, + ) { super(); } + async readOne(id: ID, session: Session) { + const query = this.db + .query() + .matchNode('node', 'Product', { id }) + .apply(this.hydrate(session)); + const result = await query.first(); + if (!result) { + throw new NotFoundException('Could not find Product'); + } + + return result.dto; + } + + async readOneUnsecured(id: ID, session: Session) { + const result = await this.readOne(id, session); + return this.mapDbRowToDto(result); + } + async readMany(ids: readonly ID[], session: Session) { const query = this.db .query() @@ -90,6 +122,14 @@ export class ProductRepository extends CommonRepository { return await query.run(); } + async readManyUnsecured( + ids: readonly ID[], + session: Session, + ): Promise>> { + const rows = await this.readMany(ids, session); + return rows.map((row) => this.mapDbRowToDto(row)); + } + async listIdsAndScriptureRefs(engagementId: ID) { return await this.db .query() @@ -232,8 +272,8 @@ export class ProductRepository extends CommonRepository { ) .return<{ dto: HydratedProductRow }>( merge('props', { - engagement: 'engagement.id', - project: 'project.id', + engagement: 'engagement { .id }', + project: 'project { .id }', produces: 'produces', unspecifiedScripture: 'unspecifiedScripture { .book, .totalVerses }', @@ -244,37 +284,84 @@ export class ProductRepository extends CommonRepository { getActualDirectChanges = getChanges(DirectScriptureProduct); - async updateProperties( - object: DirectScriptureProduct, - changes: DbChanges, + async updateDirectProperties( + changes: UpdateDirectScriptureProduct, + session: Session, ) { - return await this.db.updateProperties({ + const { id, scriptureReferences, unspecifiedScripture, ...simpleChanges } = + changes; + + await this.scriptureRefService.update(id, scriptureReferences); + + if (unspecifiedScripture !== undefined) { + await this.updateUnspecifiedScripture(id, unspecifiedScripture); + } + + await this.db.updateProperties({ type: DirectScriptureProduct, - object, - changes, + object: { id }, + changes: simpleChanges, }); + + return this.mapDbRowToDto(await this.readOne(id, session)); } getActualDerivativeChanges = getChanges(DerivativeScriptureProduct); getActualOtherChanges = getChanges(OtherProduct); async findProducible(produces: ID | undefined) { - return await this.db + const result = await this.db .query() .match([ node('producible', 'Producible', { id: produces, }), ]) - .return('producible') + .return<{ producible: BaseNode }>('node') .first(); + + if (!result) { + throw new NotFoundException( + 'Could not find producible node', + 'product.produces', + ); + } + + return result; } - async create( + async createDerivative( + input: CreateDerivativeScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + session: Session, + ) { + return (await this.create( + input, + session, + )) as UnsecuredDto; + } + + async createDirect( + input: CreateDirectScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + session: Session, + ) { + return (await this.create( + input, + session, + )) as UnsecuredDto; + } + + private async create( input: (CreateDerivativeScriptureProduct | CreateDirectScriptureProduct) & { totalVerses: number; totalVerseEquivalents: number; }, + session: Session, ) { const isDerivative = 'produces' in input; const Product = isDerivative @@ -361,10 +448,11 @@ export class ProductRepository extends CommonRepository { if (!result) { throw new ServerException('Failed to create product'); } - return result.id; + + return this.mapDbRowToDto(await this.readOne(result.id, session)); } - async createOther(input: CreateOtherProduct) { + async createOther(input: CreateOtherProduct, session: Session) { const initialProps = { mediums: input.mediums ?? [], purposes: input.purposes ?? [], @@ -397,17 +485,17 @@ export class ProductRepository extends CommonRepository { if (!result) { throw new ServerException('Failed to create product'); } - return result.id; + + return this.mapDbRowToDto( + await this.readOne(result.id, session), + ) as UnsecuredDto; } - async updateProducible( - input: Except, - produces: ID, - ) { + async updateProducible(id: ID, produces: ID) { await this.db .query() .match([ - node('product', 'Product', { id: input.id }), + node('product', 'Product', id), relation('out', 'rel', 'produces', ACTIVE), node('', 'Producible'), ]) @@ -419,7 +507,7 @@ export class ProductRepository extends CommonRepository { await this.db .query() - .match([node('product', 'Product', { id: input.id })]) + .match([node('product', 'Product', id)]) .match([ node('producible', 'Producible', { id: produces, @@ -467,22 +555,40 @@ export class ProductRepository extends CommonRepository { } async updateDerivativeProperties( - object: DerivativeScriptureProduct, - changes: DbChanges, + changes: UpdateDerivativeScriptureProduct, + session: Session, ) { - return await this.db.updateProperties({ + const { id, produces, scriptureReferencesOverride, ...simpleChanges } = + changes; + + if (produces) { + await this.findProducible(produces); + await this.updateProducible(id, produces); + } + + await this.scriptureRefService.update(id, scriptureReferencesOverride, { + isOverriding: true, + }); + + await this.db.updateProperties({ type: DerivativeScriptureProduct, - object, - changes, + object: { id }, + changes: simpleChanges, }); + + return this.mapDbRowToDto(await this.readOne(id, session)); } - async updateOther(object: OtherProduct, changes: DbChanges) { - return await this.db.updateProperties({ + async updateOther(changes: UpdateOtherProduct, session: Session) { + const { id, ...simpleChanges } = changes; + + await this.db.updateProperties({ type: OtherProduct, - object, - changes, + object: { id }, + changes: simpleChanges, }); + + return this.mapDbRowToDto(await this.readOne(id, session)); } async list(input: ProductListInput, session: Session) { @@ -514,7 +620,12 @@ export class ProductRepository extends CommonRepository { .apply(sorting(Product, input)) .apply(paginate(input, this.hydrate(session))) .first(); - return result!; // result from paginate() will always have 1 row. + + return { + // result from paginate() will always have 1 row + ...result!, + items: result!.items.map((row) => this.mapDbRowToDto(row)), + }; } async mergeCompletionDescription( @@ -539,6 +650,17 @@ export class ProductRepository extends CommonRepository { .run(); } + async delete(object: AnyProduct) { + try { + await this.deleteNode(object); + } catch (exception) { + throw new ServerException( + `Failed to delete product ${object.id}`, + exception, + ); + } + } + async suggestCompletionDescriptions({ query: queryInput, methodology, @@ -566,6 +688,69 @@ export class ProductRepository extends CommonRepository { return result!; } + private mapDbRowToDto(row: HydratedProductRow): UnsecuredDto { + const { + isOverriding, + produces: rawProducible, + title, + description, + ...rawProps + } = row; + const props = { + ...rawProps, + mediums: rawProps.mediums ?? [], + purposes: rawProps.purposes ?? [], + steps: rawProps.steps ?? [], + scriptureReferences: this.scriptureRefService.parseList( + rawProps.scriptureReferences, + ), + }; + + if (title) { + const dto: UnsecuredDto = { + ...props, + title, + description, + __typename: 'OtherProduct', + }; + return dto; + } + + if (!rawProducible) { + const dto: UnsecuredDto = { + ...props, + totalVerses: props.totalVerses ?? 0, + totalVerseEquivalents: props.totalVerseEquivalents ?? 0, + __typename: 'DirectScriptureProduct', + }; + return dto; + } + + const producible = { + ...rawProducible, + scriptureReferences: this.scriptureRefService.parseList( + rawProducible.scriptureReferences, + ), + }; + + const producibleType = this.resourceResolver.resolveType( + producible.__typename, + ) as ProducibleType; + + const dto: UnsecuredDto = { + ...props, + produces: { ...producible, __typename: producibleType }, + scriptureReferences: !isOverriding + ? producible.scriptureReferences + : props.scriptureReferences, + scriptureReferencesOverride: !isOverriding + ? null + : props.scriptureReferences, + __typename: 'DerivativeScriptureProduct', + }; + return dto; + } + @OnIndex('schema') private async createCompletionDescriptionIndex() { await this.db diff --git a/src/components/product/product.service.ts b/src/components/product/product.service.ts index b9070decb9..a3992f8f8d 100644 --- a/src/components/product/product.service.ts +++ b/src/components/product/product.service.ts @@ -4,15 +4,14 @@ import { intersection, sumBy, uniq } from 'lodash'; import { ID, InputException, - NotFoundException, ObjectView, - ServerException, Session, UnsecuredDto, } from '~/common'; import { HandleIdLookup, ILogger, Logger, ResourceResolver } from '~/core'; import { compareNullable, ifDiff, isSame } from '~/core/database/changes'; import { Privileges } from '../authorization'; +import { EngagementService } from '../engagement'; import { getTotalVerseEquivalents, getTotalVerses, @@ -45,7 +44,7 @@ import { UpdateOtherProduct, UpdateBaseProduct as UpdateProduct, } from './dto'; -import { HydratedProductRow, ProductRepository } from './product.repository'; +import { ProductRepository } from './product.repository'; @Injectable() export class ProductService { @@ -53,6 +52,7 @@ export class ProductService { private readonly scriptureRefs: ScriptureReferenceService, private readonly privileges: Privileges, private readonly repo: ProductRepository, + private readonly engagementService: EngagementService, private readonly resources: ResourceResolver, @Logger('product:service') private readonly logger: ILogger, ) {} @@ -64,19 +64,7 @@ export class ProductService { | CreateOtherProduct, session: Session, ): Promise { - const engagement = await this.repo.getBaseNode( - input.engagementId, - 'Engagement', - ); - if (!engagement) { - this.logger.warning(`Could not find engagement`, { - id: input.engagementId, - }); - throw new NotFoundException( - 'Could not find engagement', - 'product.engagementId', - ); - } + await this.engagementService.readOne(input.engagementId, session); const otherInput: CreateOtherProduct | undefined = 'title' in input && input.title !== undefined ? input : undefined; @@ -90,19 +78,10 @@ export class ProductService { let producibleType: ProducibleType | undefined = undefined; if (derivativeInput) { - const producible = await this.repo.getBaseNode( + const { producible } = await this.repo.findProducible( derivativeInput.produces, - 'Producible', ); - if (!producible) { - this.logger.warning(`Could not find producible node`, { - id: derivativeInput.produces, - }); - throw new NotFoundException( - 'Could not find producible node', - 'product.produces', - ); - } + producibleType = this.resources.resolveTypeByBaseNode( producible, ) as ProducibleType; @@ -142,25 +121,41 @@ export class ProductService { Number: input.progressTarget ?? 1, }); - const id = + const created = 'title' in input && input.title !== undefined - ? await this.repo.createOther({ ...input, progressTarget, steps }) - : await this.repo.create({ - ...input, - progressTarget, - steps, - totalVerses, - totalVerseEquivalents, - }); - - this.logger.debug(`product created`, { id }); - const created = await this.readOne(id, session); + ? await this.repo.createOther( + { ...input, progressTarget, steps }, + session, + ) + : 'produces' in input + ? await this.repo.createDerivative( + { + ...input, + progressTarget, + steps, + totalVerses, + totalVerseEquivalents, + }, + session, + ) + : await this.repo.createDirect( + { + ...input, + progressTarget, + steps, + totalVerses, + totalVerseEquivalents, + }, + session, + ); + + const securedCreated = this.secure(created, session); this.privileges - .for(session, resolveProductType(created), created) + .for(session, resolveProductType(securedCreated), securedCreated) .verifyCan('create'); - return created; + return securedCreated; } @HandleIdLookup([ @@ -173,101 +168,18 @@ export class ProductService { session: Session, _view?: ObjectView, ): Promise { - const dto = await this.readOneUnsecured(id, session); + const dto = await this.repo.readOneUnsecured(id, session); return this.secure(dto, session); } - async readOneUnsecured( - id: ID, - session: Session, - ): Promise> { - const rows = await this.readManyUnsecured([id], session); - const result = rows[0]; - if (!result) { - throw new NotFoundException('Could not find product'); - } - return result; - } - async readMany( ids: readonly ID[], session: Session, ): Promise { - const rows = await this.readManyUnsecured(ids, session); + const rows = await this.repo.readManyUnsecured(ids, session); return rows.map((row) => this.secure(row, session)); } - async readManyUnsecured( - ids: readonly ID[], - session: Session, - ): Promise>> { - const rows = await this.repo.readMany(ids, session); - return rows.map((row) => this.mapDbRowToDto(row)); - } - - private mapDbRowToDto(row: HydratedProductRow): UnsecuredDto { - const { - isOverriding, - produces: rawProducible, - title, - description, - ...rawProps - } = row; - const props = { - ...rawProps, - mediums: rawProps.mediums ?? [], - purposes: rawProps.purposes ?? [], - steps: rawProps.steps ?? [], - scriptureReferences: this.scriptureRefs.parseList( - rawProps.scriptureReferences, - ), - }; - - if (title) { - const dto: UnsecuredDto = { - ...props, - title, - description, - __typename: 'OtherProduct', - }; - return dto; - } - - if (!rawProducible) { - const dto: UnsecuredDto = { - ...props, - totalVerses: props.totalVerses ?? 0, - totalVerseEquivalents: props.totalVerseEquivalents ?? 0, - __typename: 'DirectScriptureProduct', - }; - return dto; - } - - const producible = { - ...rawProducible, - scriptureReferences: this.scriptureRefs.parseList( - rawProducible.scriptureReferences, - ), - }; - - const producibleType = this.resources.resolveType( - producible.__typename, - ) as ProducibleType; - - const dto: UnsecuredDto = { - ...props, - produces: { ...producible, __typename: producibleType }, - scriptureReferences: !isOverriding - ? producible.scriptureReferences - : props.scriptureReferences, - scriptureReferencesOverride: !isOverriding - ? null - : props.scriptureReferences, - __typename: 'DerivativeScriptureProduct', - }; - return dto; - } - secure(dto: UnsecuredDto, session: Session): AnyProduct { return this.privileges.for(session, resolveProductType(dto)).secure(dto); } @@ -278,36 +190,26 @@ export class ProductService { currentProduct?: UnsecuredDto, ): Promise { currentProduct ??= asProductType(DirectScriptureProduct)( - await this.readOneUnsecured(input.id, session), + await this.repo.readOneUnsecured(input.id, session), ); const changes = this.getDirectProductChanges(input, currentProduct); this.privileges .for(session, DirectScriptureProduct, currentProduct) .verifyChanges(changes, { pathPrefix: 'product' }); - const { scriptureReferences, unspecifiedScripture, ...simpleChanges } = - changes; - - await this.scriptureRefs.update(input.id, scriptureReferences); - - // update unspecifiedScripture if it's defined - if (unspecifiedScripture !== undefined) { - await this.repo.updateUnspecifiedScripture( - input.id, - unspecifiedScripture, - ); - } + //TODO - perhaps move this into the repo? await this.mergeCompletionDescription(changes, currentProduct); - const productUpdatedScriptureReferences = asProductType( - DirectScriptureProduct, - )(await this.readOne(input.id, session)); - - return await this.repo.updateProperties( - productUpdatedScriptureReferences, - simpleChanges, + const updated = await this.repo.updateDirectProperties( + { + id: currentProduct.id, + ...changes, + }, + session, ); + + return asProductType(DirectScriptureProduct)(this.secure(updated, session)); } private getDirectProductChanges( @@ -358,7 +260,7 @@ export class ProductService { currentProduct?: UnsecuredDto, ): Promise { currentProduct ??= asProductType(DerivativeScriptureProduct)( - await this.readOneUnsecured(input.id, session), + await this.repo.readOneUnsecured(input.id, session), ); const changes = this.getDerivativeProductChanges(input, currentProduct); @@ -366,37 +268,19 @@ export class ProductService { .for(session, DerivativeScriptureProduct, currentProduct) .verifyChanges(changes, { pathPrefix: 'product' }); - const { produces, scriptureReferencesOverride, ...simpleChanges } = changes; - - if (produces) { - const producible = await this.repo.findProducible(produces); - - if (!producible) { - this.logger.warning(`Could not find producible node`, { - id: produces, - }); - throw new NotFoundException( - 'Could not find producible node', - 'product.produces', - ); - } - await this.repo.updateProducible(input, produces); - } - + //TODO - perhaps move this into the repo? await this.mergeCompletionDescription(changes, currentProduct); - // update the scripture references (override) - await this.scriptureRefs.update(input.id, scriptureReferencesOverride, { - isOverriding: true, - }); - - const productUpdatedScriptureReferences = asProductType( - DerivativeScriptureProduct, - )(await this.readOne(input.id, session)); + const updated = await this.repo.updateDerivativeProperties( + { + id: currentProduct.id, + ...changes, + }, + session, + ); - return await this.repo.updateDerivativeProperties( - productUpdatedScriptureReferences, - simpleChanges, + return asProductType(DerivativeScriptureProduct)( + this.secure(updated, session), ); } @@ -446,8 +330,11 @@ export class ProductService { return changes; } - async updateOther(input: UpdateOtherProduct, session: Session) { - const currentProduct = await this.readOneUnsecured(input.id, session); + async updateOther( + input: UpdateOtherProduct, + session: Session, + ): Promise { + const currentProduct = await this.repo.readOneUnsecured(input.id, session); if (!currentProduct.title) { throw new InputException('Product given is not an OtherProduct'); } @@ -468,10 +355,12 @@ export class ProductService { await this.mergeCompletionDescription(changes, currentProduct); - const currentSecured = asProductType(OtherProduct)( - this.secure(currentProduct, session), + const updated = await this.repo.updateOther( + { id: currentProduct.id, ...changes }, + session, ); - return await this.repo.updateOther(currentSecured, changes); + + return asProductType(OtherProduct)(this.secure(updated, session)); } /** @@ -559,12 +448,7 @@ export class ProductService { this.privileges.for(session, Product, object).verifyCan('delete'); - try { - await this.repo.deleteNode(object); - } catch (exception) { - this.logger.error('Failed to delete', { id, exception }); - throw new ServerException('Failed to delete', exception); - } + await this.repo.delete(object); } async list( @@ -572,12 +456,10 @@ export class ProductService { session: Session, ): Promise { // all roles can list, so no need to check canList for now - const results = await this.repo.list(input, session); + const { items, ...results } = await this.repo.list(input, session); return { ...results, - items: results.items.map((row) => - this.secure(this.mapDbRowToDto(row), session), - ), + items: items.map((row) => this.secure(row, session)), }; }