From 3d13b6abc943cc1a09af9db09d90f59d65a7415a Mon Sep 17 00:00:00 2001 From: Travis Date: Sun, 2 Aug 2020 00:25:04 +0200 Subject: [PATCH 1/6] update story service for the scripture references --- src/components/budget/budget.service.ts | 5 +- src/components/ceremony/ceremony.service.ts | 5 +- src/components/film/film.service.ts | 5 +- src/components/product/product.service.ts | 90 +++++------ src/components/project/project.service.ts | 7 +- src/components/story/story.service.ts | 158 ++++++++++++++++---- 6 files changed, 179 insertions(+), 91 deletions(-) diff --git a/src/components/budget/budget.service.ts b/src/components/budget/budget.service.ts index 5a1ade030b..b2d8e59636 100644 --- a/src/components/budget/budget.service.ts +++ b/src/components/budget/budget.service.ts @@ -11,7 +11,6 @@ import { ISession, Order } from '../../common'; import { addAllSecureProperties, addBaseNodeMetaPropsWithClause, - addUserToSG, ConfigService, createBaseNode, DatabaseService, @@ -194,7 +193,7 @@ export class BudgetService { .query() .call(matchRequestingUser, session) .match([ - node('rootUser', 'User', { + node('root', 'User', { active: true, id: this.config.rootAdmin.id, }), @@ -202,8 +201,6 @@ export class BudgetService { .call(createBaseNode, 'Budget', secureProps, { owningOrgId: session.owningOrgId, }) - .call(addUserToSG, 'rootUser', 'adminSG') - .call(addUserToSG, 'rootUser', 'readerSG') .return('node.id as id'); const result = await createBudget.first(); diff --git a/src/components/ceremony/ceremony.service.ts b/src/components/ceremony/ceremony.service.ts index a8686458d4..b280f2bac0 100644 --- a/src/components/ceremony/ceremony.service.ts +++ b/src/components/ceremony/ceremony.service.ts @@ -11,7 +11,6 @@ import { ISession } from '../../common'; import { addAllSecureProperties, addBaseNodeMetaPropsWithClause, - addUserToSG, ConfigService, createBaseNode, DatabaseService, @@ -172,7 +171,7 @@ export class CeremonyService { .query() .call(matchRequestingUser, session) .match([ - node('rootUser', 'User', { + node('root', 'User', { active: true, id: this.config.rootAdmin.id, }), @@ -180,8 +179,6 @@ export class CeremonyService { .call(createBaseNode, 'Ceremony', secureProps, { owningOrgId: session.owningOrgId, }) - .call(addUserToSG, 'rootUser', 'adminSG') - .call(addUserToSG, 'rootUser', 'readerSG') .return('node.id as id'); const result = await query.first(); diff --git a/src/components/film/film.service.ts b/src/components/film/film.service.ts index 87012bef41..1d420ecdf3 100644 --- a/src/components/film/film.service.ts +++ b/src/components/film/film.service.ts @@ -10,7 +10,6 @@ import { DuplicateException, ISession } from '../../common'; import { addAllSecureProperties, addBaseNodeMetaPropsWithClause, - addUserToSG, ConfigService, createBaseNode, DatabaseService, @@ -163,7 +162,7 @@ export class FilmService { .query() .call(matchRequestingUser, session) .match([ - node('rootuser', 'User', { + node('root', 'User', { active: true, id: this.config.rootAdmin.id, }), @@ -172,8 +171,6 @@ export class FilmService { owningOrgId: session.owningOrgId, }) .create([...this.permission('range', 'node')]) - .call(addUserToSG, 'rootuser', 'adminSG') - .call(addUserToSG, 'rootuser', 'readerSG') .return('node.id as id'); const result = await query.first(); diff --git a/src/components/product/product.service.ts b/src/components/product/product.service.ts index a0086627b8..9c36734745 100644 --- a/src/components/product/product.service.ts +++ b/src/components/product/product.service.ts @@ -12,10 +12,8 @@ import { addBaseNodeMetaPropsWithClause, addPropertyCoalesceWithClause, addShapeForBaseNodeMetaProperty, - addUserToSG, ConfigService, createBaseNode, - createSG, DatabaseService, filterByString, ILogger, @@ -102,7 +100,7 @@ export class ProductService { addToReaderSg: true, isPublic: true, isOrgPublic: true, - label: 'Mediums', + label: 'ProductMedium', }, { key: 'purposes', @@ -112,7 +110,7 @@ export class ProductService { addToReaderSg: true, isPublic: true, isOrgPublic: true, - label: 'Purposes', + label: 'ProductPurpose', }, { key: 'methodology', @@ -122,10 +120,9 @@ export class ProductService { addToReaderSg: true, isPublic: true, isOrgPublic: true, - label: 'Methodology', + label: 'ProductMethodology', }, ]; - // const baseMetaProps = []; const query = this.db .query() @@ -140,44 +137,47 @@ export class ProductService { } query - .match([ - node('publicSG', 'PublicSecurityGroup', { - active: true, - id: this.config.publicSecurityGroup.id, - }), - ]) .call(matchRequestingUser, session) - .call(createSG, 'orgSG', 'OrgPublicSecurityGroup') - .call(createBaseNode, 'Product', secureProps) - .call(addUserToSG, 'requestingUser', 'adminSG'); // must come after base node creation + .call(createBaseNode, 'Product', secureProps, { + owningOrgId: session.owningOrgId, + }); if (engagementId) { query.create([ [ node('engagement'), - relation('in', '', 'engagement', { active: true, createdAt }), + relation('in', '', 'product', { active: true, createdAt }), node('node'), ], ...this.permission('product', 'engagement', true), ]); } - query.return('node.id as id'); + if (input.produces) { + query + .match([ + node('node'), + node('pr', 'Producible', { id: input.produces, active: true }), + ]) + .create([ + node('node'), + relation('out', '', 'produces', { + active: true, + createdAt: DateTime.local().toString(), + }), + node('pr'), + ]); + } - const result = await query.first(); + const result = await query.return('node.id as id').first(); if (!result) { throw new ServerException('failed to create default product'); } - const id = result.id; + this.logger.debug(`product created`, { id: result.id }); - // add root admin to new product as an admin - await this.db.addRootAdminToBaseNodeAsAdmin(id, 'Product'); - - this.logger.debug(`product created`, { id }); - - return this.readOne(id, session); + return this.readOne(result.id, session); // try { // await this.db.createNode({ // session, @@ -221,7 +221,12 @@ export class ProductService { } async readOne(id: string, session: ISession): Promise { - const props = ['mediums', 'purposes', 'methodology']; + const props = [ + 'mediums', + 'purposes', + 'methodologies', + 'scriptureReferences', + ]; const baseNodeMetaProps = ['id', 'createdAt']; const query = this.db @@ -245,8 +250,8 @@ export class ProductService { ...result, scriptureReferences: { // TODO - canRead: true, - canEdit: true, + canRead: result.scriptureReferences.canRead, + canEdit: result.scriptureReferences.canEdit, value: [], }, }; @@ -294,7 +299,6 @@ export class ProductService { ): Promise { const label = 'Product'; const baseNodeMetaProps = ['id', 'createdAt']; - // const unsecureProps = ['']; const secureProps = ['mediums', 'purposes', 'methodology']; const query = this.db @@ -335,22 +339,22 @@ export class ProductService { ` ); - const result = await runListQuery( - query, - input, - secureProps.includes(input.sort) + const result: { + items: Array<{ + identity: string; + labels: string[]; + properties: Product; + }>; + hasMore: boolean; + total: number; + } = await runListQuery(query, input, secureProps.includes(input.sort)); + + const items = await Promise.all( + result.items.map((item) => { + return this.readOne(item.properties.id, session); + }) ); - const items = result.items.map((item) => ({ - ...(item as Product), - scriptureReferences: { - // TODO - canRead: true, - canEdit: true, - value: [], - }, - })); - // // TODO this is bad, we should at least fetch the the producible IDs in the // // list query above. Then we may have to call each service to fully hydrate // // the object (film, story, song, etc.). diff --git a/src/components/project/project.service.ts b/src/components/project/project.service.ts index cada53a929..f122610cf0 100644 --- a/src/components/project/project.service.ts +++ b/src/components/project/project.service.ts @@ -17,7 +17,6 @@ import { import { addAllSecureProperties, addBaseNodeMetaPropsWithClause, - addUserToSG, ConfigService, createBaseNode, DatabaseService, @@ -303,7 +302,7 @@ export class ProjectService { .query() .call(matchRequestingUser, session) .match([ - node('rootUser', 'User', { + node('root', 'User', { active: true, id: this.config.rootAdmin.id, }), @@ -330,9 +329,7 @@ export class ProjectService { ...this.permission('teamMember'), ...this.permission('partnership'), ...this.permission('location'), - ]) - .call(addUserToSG, 'rootUser', 'adminSG') - .call(addUserToSG, 'rootUser', 'readerSG'); + ]); if (locationId) { createProject.create([ [ diff --git a/src/components/story/story.service.ts b/src/components/story/story.service.ts index a43f8624f5..8a3f2a9be9 100644 --- a/src/components/story/story.service.ts +++ b/src/components/story/story.service.ts @@ -4,13 +4,12 @@ import { NotFoundException, InternalServerErrorException as ServerException, } from '@nestjs/common'; -import { node, relation } from 'cypher-query-builder'; +import { inArray, node, relation } from 'cypher-query-builder'; import { DateTime } from 'luxon'; import { DuplicateException, ISession } from '../../common'; import { addAllSecureProperties, addBaseNodeMetaPropsWithClause, - addUserToSG, ConfigService, createBaseNode, DatabaseService, @@ -23,6 +22,11 @@ import { OnIndex, runListQuery, } from '../../core'; +import { ScriptureRange } from '../scripture'; +import { + scriptureToVerseRange, + verseToScriptureRange, +} from '../scripture/reference'; import { CreateStory, Story, @@ -151,50 +155,89 @@ export class StoryService { label: 'StoryName', }, ]; - try { - const query = this.db - .query() - .call(matchRequestingUser, session) - .match([ - node('rootUser', 'User', { + // try { + const query = this.db + .query() + .call(matchRequestingUser, session) + .match([ + node('root', 'User', { + active: true, + id: this.config.rootAdmin.id, + }), + ]) + .call(createBaseNode, ['Story', 'Producible'], secureProps, { + owningOrgId: session.owningOrgId, + }) + .create([...this.permission('scriptureReferences', 'node')]); + + if (input.scriptureReferences) { + for (const sr of input.scriptureReferences) { + const verseRange = scriptureToVerseRange(sr); + query.create([ + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('sr', 'ScriptureRange', { + start: verseRange.start, + end: verseRange.end, active: true, - id: this.config.rootAdmin.id, + createdAt: DateTime.local().toString(), }), - ]) - .call(createBaseNode, ['Story', 'Producible'], secureProps, { - owningOrgId: session.owningOrgId, - }) - .create([...this.permission('range', 'node')]) - .call(addUserToSG, 'rootUser', 'adminSG') - .call(addUserToSG, 'rootUser', 'readerSG') - .return('node.id as id'); - const result = await query.first(); - if (!result) { - throw new ServerException('failed to create a story'); + ]); } + } + query.return('node.id as id'); - this.logger.info(`story created`, { id: result.id }); - return await this.readOne(result.id, session); - } catch (err) { - this.logger.error(`Could not create story for user ${session.userId}`); - throw new ServerException('Could not create story'); + const result = await query.first(); + if (!result) { + throw new ServerException('failed to create a story'); } + + this.logger.info(`story created`, { id: result.id }); + return this.readOne(result.id, session); + // } catch (err) { + // this.logger.error(`Could not create story for user ${session.userId}`); + // throw new ServerException('Could not create story'); + // } } async readOne(storyId: string, session: ISession): Promise { - const secureProps = ['name', 'range']; + const secureProps = ['name']; const baseNodeMetaProps = ['id', 'createdAt']; const readStory = this.db .query() .call(matchRequestingUser, session) .call(matchUserPermissions, 'Story', storyId) .call(addAllSecureProperties, ...secureProps) + .optionalMatch([ + node('scriptureReferencesReadPerm', 'Permission', { + property: 'scriptureReferences', + read: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('scriptureReferences', 'ScriptureRange', { active: true }), + ]) + .where({ scriptureReferencesReadPerm: inArray(['permList'], true) }) + .optionalMatch([ + node('scriptureReferencesEditPerm', 'Permission', { + property: 'scriptureReferences', + edit: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + ]) + .where({ scriptureReferencesReadPerm: inArray(['permList'], true) }) .return( ` { ${addBaseNodeMetaPropsWithClause(baseNodeMetaProps)}, ${listWithSecureObject(secureProps)}, - canReadStorys: requestingUser.canReadStorys + canReadStorys: requestingUser.canReadStorys, + canScriptureReferencesRead: scriptureReferencesReadPerm.read, + canScriptureReferencesEdit: scriptureReferencesEditPerm.edit } as story ` ); @@ -210,13 +253,19 @@ export class StoryService { 'User does not have permission to read a story' ); } + + const scriptureReferences = await this.listScriptureReferences( + result.story.id, + session + ); + return { id: result.story.id, name: result.story.name, scriptureReferences: { - canEdit: !!result.story.range.canEdit, - canRead: !!result.story.range.canRead, - value: [], + canEdit: !!result.story.canScriptureReferencesRead, + canRead: !!result.story.canScriptureReferencesEdit, + value: scriptureReferences, }, createdAt: result.story.createdAt, }; @@ -253,7 +302,7 @@ export class StoryService { { filter, ...input }: StoryListInput, session: ISession ): Promise { - const secureProps = ['name', 'range']; + const secureProps = ['name']; const label = 'Story'; const query = this.db @@ -285,4 +334,51 @@ export class StoryService { total: listResult.total, }; } + + async listScriptureReferences( + storyId: string, + session: ISession + ): Promise { + const query = this.db + .query() + .call(matchRequestingUser, session) + .match([ + node('story', 'Story', { + id: storyId, + active: true, + owningOrgId: session.owningOrgId, + }), + relation('out', '', 'scriptureReferences'), + node('scriptureRanges', 'ScriptureRange', { active: true }), + ]) + .with('collect(scriptureRanges) as items') + .return('items'); + const result = await query.first(); + + if (!result) { + throw new NotFoundException('Could not find scripture reference'); + } + + const items: ScriptureRange[] = await Promise.all( + result.items.map( + (item: { + identity: string; + labels: string; + properties: { + start: number; + end: number; + createdAt: string; + active: boolean; + }; + }) => { + return verseToScriptureRange({ + start: item.properties.start, + end: item.properties.end, + }); + } + ) + ); + + return items; + } } From 39ca48bd8751f53270fbb83237729740b5cd8618 Mon Sep 17 00:00:00 2001 From: Travis Date: Sun, 2 Aug 2020 01:57:54 +0200 Subject: [PATCH 2/6] use absolute verse range concept on the film and story service --- src/components/film/film.service.ts | 113 ++++++++++++++++++++++++-- src/components/story/story.service.ts | 81 +++++++++--------- 2 files changed, 144 insertions(+), 50 deletions(-) diff --git a/src/components/film/film.service.ts b/src/components/film/film.service.ts index 1d420ecdf3..3ad29614d5 100644 --- a/src/components/film/film.service.ts +++ b/src/components/film/film.service.ts @@ -4,7 +4,7 @@ import { NotFoundException, InternalServerErrorException as ServerException, } from '@nestjs/common'; -import { node, relation } from 'cypher-query-builder'; +import { inArray, node, relation } from 'cypher-query-builder'; import { DateTime } from 'luxon'; import { DuplicateException, ISession } from '../../common'; import { @@ -22,6 +22,11 @@ import { OnIndex, runListQuery, } from '../../core'; +import { ScriptureRange } from '../scripture'; +import { + scriptureToVerseRange, + verseToScriptureRange, +} from '../scripture/reference'; import { CreateFilm, Film, @@ -170,8 +175,24 @@ export class FilmService { .call(createBaseNode, ['Film', 'Producible'], secureProps, { owningOrgId: session.owningOrgId, }) - .create([...this.permission('range', 'node')]) - .return('node.id as id'); + .create([...this.permission('range', 'node')]); + + if (input.scriptureReferences) { + for (const sr of input.scriptureReferences) { + const verseRange = scriptureToVerseRange(sr); + query.create([ + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('sr', 'ScriptureRange', { + start: verseRange.start, + end: verseRange.end, + active: true, + createdAt: DateTime.local().toString(), + }), + ]); + } + } + query.return('node.id as id'); const result = await query.first(); if (!result) { @@ -187,19 +208,43 @@ export class FilmService { } async readOne(filmId: string, session: ISession): Promise { - const secureProps = ['name', 'range']; + const secureProps = ['name']; const baseNodeMetaProps = ['id', 'createdAt']; const readFilm = this.db .query() .call(matchRequestingUser, session) .call(matchUserPermissions, 'Film', filmId) .call(addAllSecureProperties, ...secureProps) + .optionalMatch([ + node('scriptureReferencesReadPerm', 'Permission', { + property: 'scriptureReferences', + read: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('scriptureReferences', 'ScriptureRange', { active: true }), + ]) + .where({ scriptureReferencesReadPerm: inArray(['permList'], true) }) + .optionalMatch([ + node('scriptureReferencesEditPerm', 'Permission', { + property: 'scriptureReferences', + edit: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + ]) + .where({ scriptureReferencesReadPerm: inArray(['permList'], true) }) .return( ` { ${addBaseNodeMetaPropsWithClause(baseNodeMetaProps)}, ${listWithSecureObject(secureProps)}, - canReadFilms: requestingUser.canReadFilms + canReadFilms: requestingUser.canReadFilms, + canScriptureReferencesRead: scriptureReferencesReadPerm.read, + canScriptureReferencesEdit: scriptureReferencesEditPerm.edit } as film ` ); @@ -215,14 +260,18 @@ export class FilmService { ); } + const scriptureReferences = await this.listScriptureReferences( + result.story.id, + session + ); + return { id: result.film.id, name: result.film.name, scriptureReferences: { - // TODO - canRead: !!result.film.range.canRead, - canEdit: !!result.film.range.canEdit, - value: [], + canRead: !!result.story.canScriptureReferencesRead, + canEdit: !!result.story.canScriptureReferencesEdit, + value: scriptureReferences, }, createdAt: result.film.createdAt, }; @@ -291,4 +340,50 @@ export class FilmService { total: listResult.total, }; } + + async listScriptureReferences( + filmId: string, + session: ISession + ): Promise { + const query = this.db + .query() + .match([ + node('film', 'Film', { + id: filmId, + active: true, + owningOrgId: session.owningOrgId, + }), + relation('out', '', 'scriptureReferences'), + node('scriptureRanges', 'ScriptureRange', { active: true }), + ]) + .with('collect(scriptureRanges) as items') + .return('items'); + const result = await query.first(); + + if (!result) { + throw new NotFoundException('Could not find scripture reference'); + } + + const items: ScriptureRange[] = await Promise.all( + result.items.map( + (item: { + identity: string; + labels: string; + properties: { + start: number; + end: number; + createdAt: string; + active: boolean; + }; + }) => { + return verseToScriptureRange({ + start: item.properties.start, + end: item.properties.end, + }); + } + ) + ); + + return items; + } } diff --git a/src/components/story/story.service.ts b/src/components/story/story.service.ts index 8a3f2a9be9..c56bc9561a 100644 --- a/src/components/story/story.service.ts +++ b/src/components/story/story.service.ts @@ -155,49 +155,49 @@ export class StoryService { label: 'StoryName', }, ]; - // try { - const query = this.db - .query() - .call(matchRequestingUser, session) - .match([ - node('root', 'User', { - active: true, - id: this.config.rootAdmin.id, - }), - ]) - .call(createBaseNode, ['Story', 'Producible'], secureProps, { - owningOrgId: session.owningOrgId, - }) - .create([...this.permission('scriptureReferences', 'node')]); - - if (input.scriptureReferences) { - for (const sr of input.scriptureReferences) { - const verseRange = scriptureToVerseRange(sr); - query.create([ - node('node'), - relation('out', '', 'scriptureReferences', { active: true }), - node('sr', 'ScriptureRange', { - start: verseRange.start, - end: verseRange.end, + try { + const query = this.db + .query() + .call(matchRequestingUser, session) + .match([ + node('root', 'User', { active: true, - createdAt: DateTime.local().toString(), + id: this.config.rootAdmin.id, }), - ]); + ]) + .call(createBaseNode, ['Story', 'Producible'], secureProps, { + owningOrgId: session.owningOrgId, + }) + .create([...this.permission('scriptureReferences', 'node')]); + + if (input.scriptureReferences) { + for (const sr of input.scriptureReferences) { + const verseRange = scriptureToVerseRange(sr); + query.create([ + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('sr', 'ScriptureRange', { + start: verseRange.start, + end: verseRange.end, + active: true, + createdAt: DateTime.local().toString(), + }), + ]); + } } - } - query.return('node.id as id'); + query.return('node.id as id'); - const result = await query.first(); - if (!result) { - throw new ServerException('failed to create a story'); - } + const result = await query.first(); + if (!result) { + throw new ServerException('failed to create a story'); + } - this.logger.info(`story created`, { id: result.id }); - return this.readOne(result.id, session); - // } catch (err) { - // this.logger.error(`Could not create story for user ${session.userId}`); - // throw new ServerException('Could not create story'); - // } + this.logger.info(`story created`, { id: result.id }); + return await this.readOne(result.id, session); + } catch (err) { + this.logger.error(`Could not create story for user ${session.userId}`); + throw new ServerException('Could not create story'); + } } async readOne(storyId: string, session: ISession): Promise { @@ -263,8 +263,8 @@ export class StoryService { id: result.story.id, name: result.story.name, scriptureReferences: { - canEdit: !!result.story.canScriptureReferencesRead, - canRead: !!result.story.canScriptureReferencesEdit, + canRead: !!result.story.canScriptureReferencesRead, + canEdit: !!result.story.canScriptureReferencesEdit, value: scriptureReferences, }, createdAt: result.story.createdAt, @@ -341,7 +341,6 @@ export class StoryService { ): Promise { const query = this.db .query() - .call(matchRequestingUser, session) .match([ node('story', 'Story', { id: storyId, From d740559a2b40b61100676b8a0fee824293bbc3a1 Mon Sep 17 00:00:00 2001 From: Travis Date: Mon, 3 Aug 2020 03:40:05 +0200 Subject: [PATCH 3/6] refactor readOne, create and list with absolute verse and product type --- src/components/film/film.service.ts | 8 +- .../product/dto/list-product.dto.ts | 4 + src/components/product/product.service.ts | 375 +++++++++++------- src/components/story/story.service.ts | 4 +- 4 files changed, 249 insertions(+), 142 deletions(-) diff --git a/src/components/film/film.service.ts b/src/components/film/film.service.ts index 3ad29614d5..1e06153f10 100644 --- a/src/components/film/film.service.ts +++ b/src/components/film/film.service.ts @@ -261,7 +261,7 @@ export class FilmService { } const scriptureReferences = await this.listScriptureReferences( - result.story.id, + result.film.id, session ); @@ -269,8 +269,8 @@ export class FilmService { id: result.film.id, name: result.film.name, scriptureReferences: { - canRead: !!result.story.canScriptureReferencesRead, - canEdit: !!result.story.canScriptureReferencesEdit, + canRead: !!result.film.canScriptureReferencesRead, + canEdit: !!result.film.canScriptureReferencesEdit, value: scriptureReferences, }, createdAt: result.film.createdAt, @@ -361,7 +361,7 @@ export class FilmService { const result = await query.first(); if (!result) { - throw new NotFoundException('Could not find scripture reference'); + return []; } const items: ScriptureRange[] = await Promise.all( diff --git a/src/components/product/dto/list-product.dto.ts b/src/components/product/dto/list-product.dto.ts index 8ff55696ce..f5524c0adb 100644 --- a/src/components/product/dto/list-product.dto.ts +++ b/src/components/product/dto/list-product.dto.ts @@ -24,6 +24,10 @@ export abstract class ProductFilters { }) readonly methodology?: ProductMethodology; + @Field({ + description: 'Only products for this engagement', + nullable: true, + }) readonly engagementId?: string; // TODO } diff --git a/src/components/product/product.service.ts b/src/components/product/product.service.ts index a333ca198d..818a57cb37 100644 --- a/src/components/product/product.service.ts +++ b/src/components/product/product.service.ts @@ -3,15 +3,13 @@ import { NotFoundException, InternalServerErrorException as ServerException, } from '@nestjs/common'; -import { node, Query, relation } from 'cypher-query-builder'; +import { inArray, node, Query, relation } from 'cypher-query-builder'; import { RelationDirection } from 'cypher-query-builder/dist/typings/clauses/relation-pattern'; import { DateTime } from 'luxon'; import { ISession } from '../../common'; import { addAllSecureProperties, addBaseNodeMetaPropsWithClause, - addPropertyCoalesceWithClause, - addShapeForBaseNodeMetaProperty, ConfigService, createBaseNode, DatabaseService, @@ -25,10 +23,16 @@ import { Property, runListQuery, } from '../../core'; +import { ScriptureRange } from '../scripture'; +import { + scriptureToVerseRange, + verseToScriptureRange, +} from '../scripture/reference'; import { AnyProduct, CreateProduct, MethodologyToApproach, + ProducibleType, Product, ProductApproach, ProductListInput, @@ -93,7 +97,7 @@ export class ProductService { { engagementId, ...input }: CreateProduct, session: ISession ): Promise { - const createdAt = DateTime.local(); + const createdAt = DateTime.local().toString(); // create product const secureProps: Property[] = [ { @@ -140,17 +144,33 @@ export class ProductService { ]); } + if (input.produces) { + query.match([ + node('pr', 'Producible', { id: input.produces, active: true }), + ]); + } + query .call(matchRequestingUser, session) - .call(createBaseNode, 'Product', secureProps, { - owningOrgId: session.owningOrgId, - }); + .call( + createBaseNode, + [ + 'Product', + input.produces + ? 'DerivativeScriptureProduct' + : 'DirectScriptureProduct', + ], + secureProps, + { + owningOrgId: session.owningOrgId, + } + ); if (engagementId) { query.create([ [ node('engagement'), - relation('in', '', 'product', { active: true, createdAt }), + relation('out', '', 'product', { active: true, createdAt }), node('node'), ], ...this.permission('product', 'engagement', true), @@ -158,19 +178,55 @@ export class ProductService { } if (input.produces) { - query - .match([ - node('node'), - node('pr', 'Producible', { id: input.produces, active: true }), - ]) - .create([ - node('node'), - relation('out', '', 'produces', { + query.create([ + [ + node('pr'), + relation('in', '', 'produces', { active: true, - createdAt: DateTime.local().toString(), + createdAt, }), - node('pr'), - ]); + node('node'), + ], + ...this.permission('produces', 'node', true), + ]); + } + + if (!input.produces && input.scriptureReferences) { + for (const sr of input.scriptureReferences) { + const verseRange = scriptureToVerseRange(sr); + query + .create([ + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('sr', 'ScriptureRange', { + start: verseRange.start, + end: verseRange.end, + active: true, + createdAt: DateTime.local().toString(), + }), + ]) + .create([...this.permission('scriptureReferences', 'node')]); + } + } + + if (input.produces && input.scriptureReferencesOverride) { + for (const sr of input.scriptureReferencesOverride) { + const verseRange = scriptureToVerseRange(sr); + query + .create([ + node('node'), + relation('out', '', 'scriptureReferencesOverride', { + active: true, + }), + node('sr', 'ScriptureRange', { + start: verseRange.start, + end: verseRange.end, + active: true, + createdAt: DateTime.local().toString(), + }), + ]) + .create([...this.permission('scriptureReferencesOverride', 'node')]); + } } const result = await query.return('node.id as id').first(); @@ -179,58 +235,12 @@ export class ProductService { throw new ServerException('failed to create default product'); } - this.logger.debug(`product created`, { id: result.id }); - + this.logger.info(`product created`, { id: result.id }); return this.readOne(result.id, session); - // try { - // await this.db.createNode({ - // session, - // type: Product, - // input: { - // id, - // ...input, - // ...(input.methodology - // ? { approach: MethodologyToApproach[input.methodology] } - // : {}), - // }, - // acls, - // }); - - // if (input.produces) { - // await this.db - // .query() - // .match([ - // [node('product', 'Product', { id, active: true })], - // [node('pr', 'Producible', { id: input.produces, active: true })], - // ]) - // .create([ - // node('product'), - // relation('out', '', 'produces', { - // active: true, - // createdAt: DateTime.local(), - // }), - // node('pr'), - // ]) - // .run(); - // } - // } catch (e) { - // this.logger.warning('Failed to create product', { - // exception: e, - // }); - - // throw new ServerException('Failed to create product'); - // } - - // return this.readOne(id, session); } async readOne(id: string, session: ISession): Promise { - const props = [ - 'mediums', - 'purposes', - 'methodologies', - 'scriptureReferences', - ]; + const props = ['mediums', 'purposes', 'methodology']; const baseNodeMetaProps = ['id', 'createdAt']; const query = this.db @@ -238,26 +248,129 @@ export class ProductService { .call(matchRequestingUser, session) .call(matchUserPermissions, 'Product', id) .call(addAllSecureProperties, ...props) - .with([ - ...props.map(addPropertyCoalesceWithClause), - ...baseNodeMetaProps.map(addShapeForBaseNodeMetaProperty), + .optionalMatch([ + node('scriptureReferencesReadPerm', 'Permission', { + property: 'scriptureReferences', + read: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + relation('out', '', 'scriptureReferences', { active: true }), + node('scriptureReferences', 'ScriptureRange', { active: true }), ]) - .returnDistinct([...props, 'id', 'createdAt']); + .where({ scriptureReferencesReadPerm: inArray(['permList'], true) }) + .optionalMatch([ + node('scriptureReferencesEditPerm', 'Permission', { + property: 'scriptureReferences', + edit: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + ]) + .where({ scriptureReferencesEditPerm: inArray(['permList'], true) }) + .optionalMatch([ + node('producesReadPerm', 'Permission', { + property: 'produces', + read: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + relation('out', '', 'produces', { active: true }), + node('pr', 'Producible', { active: true }), + ]) + .where({ producesReadPerm: inArray(['permList'], true) }) + .optionalMatch([ + node('producesEditPerm', 'Permission', { + property: 'produces', + edit: true, + active: true, + }), + relation('out', '', 'baseNode'), + node('node'), + ]) + .where({ producesEditPerm: inArray(['permList'], true) }) + .return( + ` + { + ${addBaseNodeMetaPropsWithClause(baseNodeMetaProps)}, + ${listWithSecureObject(props)}, + canScriptureReferencesRead: scriptureReferencesReadPerm.read, + canScriptureReferencesEdit: scriptureReferencesEditPerm.edit, + canProducesRead: producesReadPerm.read, + canProducesEdit: producesEditPerm.edit + } as product + ` + ); - const result = (await query.first()) as Product | undefined; - if (!result || !result.id) { - this.logger.warning(`Could not find product`, { id: id }); + const result = await query.first(); + if (!result) { + this.logger.warning(`Could not find product`, { id }); throw new NotFoundException('Could not find product'); } + const produces = await this.db + .query() + .match([ + node('product', 'Product', { id, active: true }), + relation('out', 'produces', { active: true }), + node('p', 'Producible', { active: true }), + ]) + .return('p') + .first(); + + if (!produces) { + const scriptureReferences = await this.listScriptureReferences( + result.product.id, + session + ); + return { + id: result.product.id, + createdAt: result.product.createdAt, + mediums: result.product.mediums, + purposes: result.product.purposes, + methodology: result.product.methodology, + scriptureReferences: { + canRead: !!result.product.canScriptureReferencesRead, + canEdit: !!result.product.canScriptureReferencesEdit, + value: scriptureReferences, + }, + }; + } + + // const typeName = difference(produces.p.labels, [ + // 'Producible', + // 'BaseNode', + // ])[0]; + const typeName = 'Film'; + return { - ...result, + id: result.product.id, + createdAt: result.product.createdAt, + mediums: result.product.mediums, + purposes: result.product.purposes, + methodology: result.product.methodology, + produces: { + value: { + id: produces.p.properties.id, + createdAt: produces.p.properties.createdAt, + __typename: ProducibleType[typeName], + }, + canRead: !!result.product.canProducesRead, + canEdit: !!result.product.canProducesEdit, + }, scriptureReferences: { - // TODO - canRead: result.scriptureReferences.canRead, - canEdit: result.scriptureReferences.canEdit, + canRead: !!result.product.canScriptureReferencesRead, + canEdit: !!result.product.canScriptureReferencesEdit, value: [], }, + // scriptureReferencesOverride: { + // canRead: !!result.product.canScriptureReferencesRead, + // canEdit: !!result.product.canScriptureReferencesEdit, + // value: [] + // }, }; } @@ -302,13 +415,11 @@ export class ProductService { session: ISession ): Promise { const label = 'Product'; - const baseNodeMetaProps = ['id', 'createdAt']; - const secureProps = ['mediums', 'purposes', 'methodology']; - + const secureProps = ['methodology']; const query = this.db .query() .call(matchRequestingUser, session) - .call(matchUserPermissions, 'Product'); + .call(matchUserPermissions, label); if (filter.methodology) { query.call(filterByString, label, 'methodology', filter.methodology); @@ -329,25 +440,6 @@ export class ProductService { ); } - // match on the rest of the properties of the object requested - query - .call( - addAllSecureProperties, - ...secureProps - //...unsecureProps - ) - - // form return object - // ${listWithUnsecureObject(unsecureProps)}, // removed from a few lines down - .with( - ` - { - ${addBaseNodeMetaPropsWithClause(baseNodeMetaProps)}, - ${listWithSecureObject(secureProps)} - } as node - ` - ); - const result: { items: Array<{ identity: string; @@ -364,41 +456,6 @@ export class ProductService { }) ); - // // TODO this is bad, we should at least fetch the the producible IDs in the - // // list query above. Then we may have to call each service to fully hydrate - // // the object (film, story, song, etc.). - // // This logic also needs to be applied to readOne() - // items = await Promise.all( - // items.map(async (item) => { - // const produces = await this.db - // .query() - // .match([ - // node('product', 'Product', { id: item.id, active: true }), - // relation('out', 'produces', { active: true }), - // node('p', 'Producible', { active: true }), - // ]) - // .return('p') - // .asResult<{ p: Node<{ id: string; createdAt: DateTime }> }>() - // .first(); - // if (!produces) { - // return item; - // } - // return { - // ...item, - // produces: { - // value: { - // id: produces.p.properties.id, - // createdAt: produces.p.properties.createdAt, - // __typename: difference(produces.p.labels, [ - // 'Producible', - // 'BaseNode', - // ])[0], - // }, - // }, - // }; - // }) - // ); - return { items, hasMore: result.hasMore, @@ -406,6 +463,52 @@ export class ProductService { }; } + async listScriptureReferences( + storyId: string, + session: ISession + ): Promise { + const query = this.db + .query() + .match([ + node('product', 'Product', { + id: storyId, + active: true, + owningOrgId: session.owningOrgId, + }), + relation('out', '', 'scriptureReferences'), + node('scriptureRanges', 'ScriptureRange', { active: true }), + ]) + .with('collect(scriptureRanges) as items') + .return('items'); + const result = await query.first(); + + if (!result) { + return []; + } + + const items: ScriptureRange[] = await Promise.all( + result.items.map( + (item: { + identity: string; + labels: string; + properties: { + start: number; + end: number; + createdAt: string; + active: boolean; + }; + }) => { + return verseToScriptureRange({ + start: item.properties.start, + end: item.properties.end, + }); + } + ) + ); + + return items; + } + // used to search a specific engagement's relationship to the target base node // for example, searching all products a engagement is a part of protected filterByEngagement( diff --git a/src/components/story/story.service.ts b/src/components/story/story.service.ts index c56bc9561a..394971caec 100644 --- a/src/components/story/story.service.ts +++ b/src/components/story/story.service.ts @@ -229,7 +229,7 @@ export class StoryService { relation('out', '', 'baseNode'), node('node'), ]) - .where({ scriptureReferencesReadPerm: inArray(['permList'], true) }) + .where({ scriptureReferencesEditPerm: inArray(['permList'], true) }) .return( ` { @@ -355,7 +355,7 @@ export class StoryService { const result = await query.first(); if (!result) { - throw new NotFoundException('Could not find scripture reference'); + return []; } const items: ScriptureRange[] = await Promise.all( From 6ccf30091cb7a39ca791ee647c656f4ddafd936c Mon Sep 17 00:00:00 2001 From: Travis Date: Mon, 3 Aug 2020 17:27:43 +0200 Subject: [PATCH 4/6] fix engagement filter --- src/components/product/product.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/product/product.service.ts b/src/components/product/product.service.ts index 818a57cb37..b1251d9f6d 100644 --- a/src/components/product/product.service.ts +++ b/src/components/product/product.service.ts @@ -434,8 +434,8 @@ export class ProductService { this.filterByEngagement( query, filter.engagementId, - 'engagement', - 'in', + 'product', + 'out', label ); } From 8310b3afe3de25e322ca26404994a63cdf8edce7 Mon Sep 17 00:00:00 2001 From: Travis Date: Mon, 3 Aug 2020 18:57:31 +0200 Subject: [PATCH 5/6] revert the engagement id to the internal filter --- src/components/product/dto/list-product.dto.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/product/dto/list-product.dto.ts b/src/components/product/dto/list-product.dto.ts index f5524c0adb..8ff55696ce 100644 --- a/src/components/product/dto/list-product.dto.ts +++ b/src/components/product/dto/list-product.dto.ts @@ -24,10 +24,6 @@ export abstract class ProductFilters { }) readonly methodology?: ProductMethodology; - @Field({ - description: 'Only products for this engagement', - nullable: true, - }) readonly engagementId?: string; // TODO } From 3cbc8ebe1237fadff80480c8d6b22aedd622a815 Mon Sep 17 00:00:00 2001 From: Travis Date: Mon, 3 Aug 2020 20:05:55 +0200 Subject: [PATCH 6/6] update readOne function --- src/components/product/product.service.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/product/product.service.ts b/src/components/product/product.service.ts index b1251d9f6d..a5591f1a29 100644 --- a/src/components/product/product.service.ts +++ b/src/components/product/product.service.ts @@ -5,6 +5,7 @@ import { } from '@nestjs/common'; import { inArray, node, Query, relation } from 'cypher-query-builder'; import { RelationDirection } from 'cypher-query-builder/dist/typings/clauses/relation-pattern'; +import { difference } from 'lodash'; import { DateTime } from 'luxon'; import { ISession } from '../../common'; import { @@ -324,6 +325,7 @@ export class ProductService { if (!produces) { const scriptureReferences = await this.listScriptureReferences( result.product.id, + 'Product', session ); return { @@ -340,11 +342,10 @@ export class ProductService { }; } - // const typeName = difference(produces.p.labels, [ - // 'Producible', - // 'BaseNode', - // ])[0]; - const typeName = 'Film'; + const typeName = difference(produces.p.labels, [ + 'Producible', + 'BaseNode', + ])[0]; return { id: result.product.id, @@ -356,7 +357,7 @@ export class ProductService { value: { id: produces.p.properties.id, createdAt: produces.p.properties.createdAt, - __typename: ProducibleType[typeName], + __typename: (ProducibleType as any)[typeName], }, canRead: !!result.product.canProducesRead, canEdit: !!result.product.canProducesEdit, @@ -464,14 +465,15 @@ export class ProductService { } async listScriptureReferences( - storyId: string, + id: string, + label: string, session: ISession ): Promise { const query = this.db .query() .match([ - node('product', 'Product', { - id: storyId, + node('node', label, { + id, active: true, owningOrgId: session.owningOrgId, }),