diff --git a/src/components/product/dto/product.dto.ts b/src/components/product/dto/product.dto.ts index cdf0b12be9..c0fc268b90 100644 --- a/src/components/product/dto/product.dto.ts +++ b/src/components/product/dto/product.dto.ts @@ -18,7 +18,7 @@ import { import { type SetDbType } from '~/core/database'; import { type SetChangeType } from '~/core/database/changes'; import { e } from '~/core/gel'; -import { RegisterResource } from '~/core/resources'; +import { type LinkTo, RegisterResource } from '~/core/resources'; import { type DbScriptureReferences } from '../../scripture'; import { type ScriptureRangeInput, @@ -55,8 +55,8 @@ export class Product extends Producible { static readonly Parent = () => import('../../engagement/dto').then((m) => m.LanguageEngagement); - readonly engagement: ID; - readonly project: ID; + readonly engagement: LinkTo<'LanguageEngagement'>; + readonly project: LinkTo<'Project'>; @Field() @DbLabel('ProductMedium') @@ -266,6 +266,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 53c3086952..743c3c8bbf 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 @@ -24,11 +24,11 @@ export class FixNaNTotalVerseEquivalentsMigration extends BaseMigration { .map('id') .run(); - const products = await this.productService.readManyUnsecured(ids); + const products = await this.productService.readMany(ids); for (const p of products) { const correctTotalVerseEquivalent = getTotalVerseEquivalents( - ...p.scriptureReferences, + ...p.scriptureReferences.value, ); if (p.__typename === 'DirectScriptureProduct') { diff --git a/src/components/product/product.gel.repository.ts b/src/components/product/product.gel.repository.ts new file mode 100644 index 0000000000..7f07c4f108 --- /dev/null +++ b/src/components/product/product.gel.repository.ts @@ -0,0 +1,228 @@ +import { Injectable, type Type } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { LazyGetter } from 'lazy-get-decorator'; +import { type ID, type PublicOf } from '../../common'; +import { grabInstances } from '../../common/instance-maps'; +import { e, RepoFor } from '../../core/gel'; +import { + ProductConcretes as ConcreteTypes, + type CreateDerivativeScriptureProduct, + type CreateDirectScriptureProduct, + type CreateOtherProduct, + Product, +} from './dto'; +import { type 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.select({ + identity: product.engagement.id, + labels: e.array_agg(e.set(product.engagement.__type__.name.slice(9, null))), + properties: e.select({ + 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.select([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 ProductGelRepository + 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; + }, + ) { + return await this.concretes.DerivativeScriptureProduct.create(input); + } + + async createDirect( + input: CreateDirectScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + ) { + return await this.concretes.DirectScriptureProduct.create(input); + } + + async createOther(input: CreateOtherProduct) { + 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 1581e087a8..84ef10f6ce 100644 --- a/src/components/product/product.repository.ts +++ b/src/components/product/product.repository.ts @@ -9,15 +9,18 @@ import { relation, } from 'cypher-query-builder'; import { DateTime } from 'luxon'; -import { type Except, type Merge } from 'type-fest'; +import { type Merge } from 'type-fest'; import { CreationFailed, EnhancedResource, type ID, + NotFoundException, type Range, + ServerException, + type UnsecuredDto, } from '~/common'; import { CommonRepository, type DbTypeOf, OnIndex } from '~/core/database'; -import { type DbChanges, getChanges } from '~/core/database/changes'; +import { getChanges } from '~/core/database/changes'; import { ACTIVE, collect, @@ -33,7 +36,12 @@ import { paginate, sorting, } from '~/core/database/query'; -import { ScriptureReferenceRepository } from '../scripture'; +import { ResourceResolver } from '../../core'; +import { type BaseNode } from '../../core/database/results'; +import { + ScriptureReferenceRepository, + ScriptureReferenceService, +} from '../scripture'; import { ScriptureRange as RawScriptureRange, type ScriptureRangeInput, @@ -41,6 +49,7 @@ import { type UnspecifiedScripturePortionInput, } from '../scripture/dto'; import { + type AnyProduct, ApproachToMethodologies, type CreateDerivativeScriptureProduct, type CreateDirectScriptureProduct, @@ -56,7 +65,9 @@ import { type ProductFilters, type ProductListInput, ProgressMeasurement, + type UpdateDerivativeScriptureProduct, type UpdateDirectScriptureProduct, + type UpdateOtherProduct, } from './dto'; const ScriptureRange = EnhancedResource.of(RawScriptureRange); @@ -77,10 +88,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) { + const query = this.db + .query() + .matchNode('node', 'Product', { id }) + .apply(this.hydrate()); + const result = await query.first(); + if (!result) { + throw new NotFoundException('Could not find Product'); + } + + return result.dto; + } + + async readOneUnsecured(id: ID) { + const result = await this.readOne(id); + return this.mapDbRowToDto(result); + } + async readMany(ids: readonly ID[]) { const query = this.db .query() @@ -91,6 +124,13 @@ export class ProductRepository extends CommonRepository { return await query.run(); } + async readManyUnsecured( + ids: readonly ID[], + ): Promise>> { + const rows = await this.readMany(ids); + return rows.map((row) => this.mapDbRowToDto(row)); + } + async listIdsAndScriptureRefs(engagementId: ID) { return await this.db .query() @@ -233,8 +273,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 }', @@ -245,33 +285,70 @@ export class ProductRepository extends CommonRepository { getActualDirectChanges = getChanges(DirectScriptureProduct); - async updateProperties( - object: DirectScriptureProduct, - changes: DbChanges, - ) { - return await this.db.updateProperties({ + async updateDirectProperties(changes: UpdateDirectScriptureProduct) { + 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)); } 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 }>('producible') .first(); + + if (!result) { + throw new NotFoundException( + 'Could not find producible node', + 'product.produces', + ); + } + + return result.producible; } - async create( + async createDerivative( + input: CreateDerivativeScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + ) { + return (await this.create( + input, + )) as UnsecuredDto; + } + + async createDirect( + input: CreateDirectScriptureProduct & { + totalVerses: number; + totalVerseEquivalents: number; + }, + ) { + return (await this.create(input)) as UnsecuredDto; + } + + private async create( input: (CreateDerivativeScriptureProduct | CreateDirectScriptureProduct) & { totalVerses: number; totalVerseEquivalents: number; @@ -362,7 +439,8 @@ export class ProductRepository extends CommonRepository { if (!result) { throw new CreationFailed(Product); } - return result.id; + + return this.mapDbRowToDto(await this.readOne(result.id)); } async createOther(input: CreateOtherProduct) { @@ -398,17 +476,17 @@ export class ProductRepository extends CommonRepository { if (!result) { throw new CreationFailed(OtherProduct); } - return result.id; + + return this.mapDbRowToDto( + await this.readOne(result.id), + ) 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'), ]) @@ -420,7 +498,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,23 +545,38 @@ export class ProductRepository extends CommonRepository { .first(); } - async updateDerivativeProperties( - object: DerivativeScriptureProduct, - changes: DbChanges, - ) { - return await this.db.updateProperties({ + async updateDerivativeProperties(changes: UpdateDerivativeScriptureProduct) { + 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)); } - async updateOther(object: OtherProduct, changes: DbChanges) { - return await this.db.updateProperties({ + async updateOther(changes: UpdateOtherProduct) { + const { id, ...simpleChanges } = changes; + + await this.db.updateProperties({ type: OtherProduct, - object, - changes, + object: { id }, + changes: simpleChanges, }); + + return this.mapDbRowToDto(await this.readOne(id)); } async list(input: ProductListInput) { @@ -515,7 +608,12 @@ export class ProductRepository extends CommonRepository { .apply(sorting(Product, input)) .apply(paginate(input, this.hydrate())) .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( @@ -540,6 +638,17 @@ export class ProductRepository extends CommonRepository { .run(); } + async delete(object: UnsecuredDto) { + try { + await this.deleteNode(object); + } catch (exception) { + throw new ServerException( + `Failed to delete product ${object.id}`, + exception, + ); + } + } + async suggestCompletionDescriptions({ query: queryInput, methodology, @@ -567,6 +676,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.resolver.ts b/src/components/product/product.resolver.ts index d5f96c6487..941512745c 100644 --- a/src/components/product/product.resolver.ts +++ b/src/components/product/product.resolver.ts @@ -75,8 +75,8 @@ export class ProductResolver { @Info(Fields, IsOnlyId) onlyId: boolean, ) { return onlyId - ? { id: product.project } - : await projects.load({ id: product.project, view: { active: true } }); + ? { id: product.project.id } + : await projects.load({ id: product.project.id, view: { active: true } }); } @ResolveField(() => ProductApproach, { nullable: true }) diff --git a/src/components/product/product.service.ts b/src/components/product/product.service.ts index cce464a1d7..a06d381e12 100644 --- a/src/components/product/product.service.ts +++ b/src/components/product/product.service.ts @@ -4,15 +4,13 @@ import { intersection, sumBy, uniq } from 'lodash'; import { type ID, InputException, - NotFoundException, type ObjectView, - ReadAfterCreationFailed, - ServerException, type 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, @@ -30,25 +28,19 @@ import { DerivativeScriptureProduct, DirectScriptureProduct, getAvailableSteps, - MethodologyToApproach, OtherProduct, ProducibleType, Product, - type ProductApproach, type ProductCompletionDescriptionSuggestionsInput, type ProductListInput, type ProductListOutput, - type ProductMethodology, resolveProductType, type UpdateDerivativeScriptureProduct, type UpdateDirectScriptureProduct, type UpdateOtherProduct, type UpdateBaseProduct as UpdateProduct, } from './dto'; -import { - type HydratedProductRow, - ProductRepository, -} from './product.repository'; +import { ProductRepository } from './product.repository'; @Injectable() export class ProductService { @@ -56,6 +48,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, ) {} @@ -66,19 +59,7 @@ export class ProductService { | CreateDerivativeScriptureProduct | CreateOtherProduct, ): 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); const otherInput: CreateOtherProduct | undefined = // Double-checking not undefined seems safer here since a union type @@ -96,19 +77,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; @@ -147,9 +119,17 @@ export class ProductService { Number: input.progressTarget ?? 1, }); - const id = otherInput + const created = otherInput ? await this.repo.createOther({ ...otherInput, progressTarget, steps }) - : await this.repo.create({ + : 'produces' in input + ? await this.repo.createDerivative({ + ...input, + progressTarget, + steps, + totalVerses, + totalVerseEquivalents, + }) + : await this.repo.createDirect({ ...input, progressTarget, steps, @@ -157,18 +137,13 @@ export class ProductService { totalVerseEquivalents, }); - this.logger.debug(`product created`, { id }); - const created = await this.readOne(id).catch((e) => { - throw e instanceof NotFoundException - ? new ReadAfterCreationFailed(Product) - : e; - }); + const securedCreated = this.secure(created); this.privileges - .for(resolveProductType(created), created) + .for(resolveProductType(securedCreated), securedCreated) .verifyCan('create'); - return created; + return securedCreated; } @HandleIdLookup([ @@ -177,94 +152,15 @@ export class ProductService { OtherProduct, ]) async readOne(id: ID, _view?: ObjectView): Promise { - const dto = await this.readOneUnsecured(id); + const dto = await this.repo.readOneUnsecured(id); return this.secure(dto); } - async readOneUnsecured(id: ID): Promise> { - const rows = await this.readManyUnsecured([id]); - const result = rows[0]; - if (!result) { - throw new NotFoundException('Could not find product'); - } - return result; - } - async readMany(ids: readonly ID[]): Promise { - const rows = await this.readManyUnsecured(ids); + const rows = await this.repo.readManyUnsecured(ids); return rows.map((row) => this.secure(row)); } - async readManyUnsecured( - ids: readonly ID[], - ): Promise>> { - const rows = await this.repo.readMany(ids); - 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): AnyProduct { return this.privileges.for(resolveProductType(dto)).secure(dto); } @@ -274,36 +170,24 @@ export class ProductService { currentProduct?: UnsecuredDto, ): Promise { currentProduct ??= asProductType(DirectScriptureProduct)( - await this.readOneUnsecured(input.id), + await this.repo.readOneUnsecured(input.id), ); + const changes = this.getDirectProductChanges(input, currentProduct); this.privileges .for(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, - ); - } + const updated = await this.repo.updateDirectProperties({ + id: currentProduct.id, + ...changes, + }); + //TODO - perhaps move this into the repo? await this.mergeCompletionDescription(changes, currentProduct); - const productUpdatedScriptureReferences = asProductType( - DirectScriptureProduct, - )(await this.readOne(input.id)); - - return await this.repo.updateProperties( - productUpdatedScriptureReferences, - simpleChanges, - ); + return asProductType(DirectScriptureProduct)(this.secure(updated)); } private getDirectProductChanges( @@ -315,6 +199,7 @@ export class ProductService { // We'll compare below scriptureReferences: undefined, }); + let changes = { ...partialChanges, steps: this.restrictStepsChange(current, partialChanges), @@ -331,6 +216,7 @@ export class ProductService { current.scriptureReferences, ), }; + if ( changes.unspecifiedScripture !== undefined || changes.scriptureReferences !== undefined @@ -345,6 +231,7 @@ export class ProductService { : getTotalVerseEquivalents(...(changes.scriptureReferences ?? [])), }; } + return changes; } @@ -353,7 +240,7 @@ export class ProductService { currentProduct?: UnsecuredDto, ): Promise { currentProduct ??= asProductType(DerivativeScriptureProduct)( - await this.readOneUnsecured(input.id), + await this.repo.readOneUnsecured(input.id), ); const changes = this.getDerivativeProductChanges(input, currentProduct); @@ -361,38 +248,15 @@ export class ProductService { .for(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); - } - - await this.mergeCompletionDescription(changes, currentProduct); - - // update the scripture references (override) - await this.scriptureRefs.update(input.id, scriptureReferencesOverride, { - isOverriding: true, + const updated = await this.repo.updateDerivativeProperties({ + id: currentProduct.id, + ...changes, }); - const productUpdatedScriptureReferences = asProductType( - DerivativeScriptureProduct, - )(await this.readOne(input.id)); + //TODO - perhaps move this into the repo? + await this.mergeCompletionDescription(changes, currentProduct); - return await this.repo.updateDerivativeProperties( - productUpdatedScriptureReferences, - simpleChanges, - ); + return asProductType(DerivativeScriptureProduct)(this.secure(updated)); } private getDerivativeProductChanges( @@ -405,6 +269,7 @@ export class ProductService { current as unknown as DerivativeScriptureProduct, input, ); + let changes = { ...partialChanges, steps: this.restrictStepsChange(current, partialChanges), @@ -428,6 +293,7 @@ export class ProductService { ? input.scriptureReferencesOverride : undefined, }; + if (changes.scriptureReferencesOverride !== undefined) { const scripture = changes.scriptureReferencesOverride ?? @@ -438,11 +304,13 @@ export class ProductService { totalVerseEquivalents: getTotalVerseEquivalents(...scripture), }; } + return changes; } - async updateOther(input: UpdateOtherProduct) { - const currentProduct = await this.readOneUnsecured(input.id); + async updateOther(input: UpdateOtherProduct): Promise { + const currentProduct = await this.repo.readOneUnsecured(input.id); + if (!currentProduct.title) { throw new InputException('Product given is not an OtherProduct'); } @@ -463,10 +331,12 @@ export class ProductService { await this.mergeCompletionDescription(changes, currentProduct); - const currentSecured = asProductType(OtherProduct)( - this.secure(currentProduct), - ); - return await this.repo.updateOther(currentSecured, changes); + const updated = await this.repo.updateOther({ + id: currentProduct.id, + ...changes, + }); + + return asProductType(OtherProduct)(this.secure(updated)); } /** @@ -550,24 +420,19 @@ export class ProductService { } async delete(id: ID): Promise { - const object = await this.readOne(id); + const object = await this.repo.readOneUnsecured(id); this.privileges.for(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(input: ProductListInput): Promise { // all roles can list, so no need to check canList for now - const results = await this.repo.list(input); + const { items, ...results } = await this.repo.list(input); return { ...results, - items: results.items.map((row) => this.secure(this.mapDbRowToDto(row))), + items: items.map((row) => this.secure(row)), }; } @@ -633,14 +498,6 @@ export class ProductService { return mapEntries(refs, ({ id, name }) => [name, id]).asMap; } - protected getMethodologiesByApproach( - approach: ProductApproach, - ): ProductMethodology[] { - return Object.keys(MethodologyToApproach).filter( - (key) => MethodologyToApproach[key as ProductMethodology] === approach, - ) as ProductMethodology[]; - } - async suggestCompletionDescriptions( input: ProductCompletionDescriptionSuggestionsInput, ) {