11/**
22 * This wraps the higher level logic with schema proposals.
33 */
4+ import { SchemaProposalChangeDetailsMapper } from '../module.graphql.mappers' ;
5+ import DataLoader from 'dataloader' ;
46import { Inject , Injectable , Scope } from 'graphql-modules' ;
5- import { TargetReferenceInput } from 'packages/libraries/core/src/client/__generated__/types' ;
7+ import type { TargetReferenceInput } from 'packages/libraries/core/src/client/__generated__/types' ;
8+ import { isChangeEqual } from '@graphql-inspector/compare-changes' ;
9+ import { Change } from '@graphql-inspector/core' ;
10+ import { traceFn } from '@hive/service-common' ;
11+ import { SchemaChangeType } from '@hive/storage' ;
612import type { SchemaProposalStage } from '../../../__generated__/types' ;
713import { HiveError } from '../../../shared/errors' ;
14+ import { cache } from '../../../shared/helpers' ;
815import { Session } from '../../auth/lib/authz' ;
916import { IdTranslator } from '../../shared/providers/id-translator' ;
1017import { Logger } from '../../shared/providers/logger' ;
1118import { PUB_SUB_CONFIG , type HivePubSub } from '../../shared/providers/pub-sub' ;
1219import { Storage } from '../../shared/providers/storage' ;
1320import { SchemaProposalStorage } from './schema-proposal-storage' ;
1421
22+ type ChangeSelector = {
23+ targetId : string ;
24+ change : SchemaChangeType ;
25+ } ;
26+
1527@Injectable ( {
1628 scope : Scope . Operation ,
1729} )
1830export class SchemaProposalManager {
1931 private logger : Logger ;
32+ private approvedChangeProposalLoader : DataLoader <
33+ ChangeSelector ,
34+ SchemaProposalChangeDetailsMapper
35+ > ;
2036
2137 constructor (
2238 logger : Logger ,
@@ -27,6 +43,69 @@ export class SchemaProposalManager {
2743 @Inject ( PUB_SUB_CONFIG ) private pubSub : HivePubSub ,
2844 ) {
2945 this . logger = logger . child ( { source : 'SchemaProposalsManager' } ) ;
46+
47+ const cacheKeyFn = function ( selector : ChangeSelector ) {
48+ return `${ selector . targetId } /${ selector . change . id } ` ;
49+ } ;
50+ this . approvedChangeProposalLoader = new DataLoader (
51+ async selectors => {
52+ // map the list of keys in the order in which they are passed in so it can
53+ // be mapped back to.
54+ const keys = selectors . map ( cacheKeyFn ) ;
55+
56+ // combine selectors by targetId
57+ const groupedSelectors = selectors . reduce ( ( groups , selector ) => {
58+ const group = groups . get ( selector . targetId ) ;
59+ if ( ! group ) {
60+ groups . set ( selector . targetId , [ selector . change ] ) ;
61+ } else {
62+ group . push ( selector . change ) ;
63+ }
64+ return groups ;
65+ } , new Map < string , SchemaChangeType [ ] > ( ) ) ;
66+
67+ // fetch the groups
68+ const approvedProposedChangesByCacheId = await Promise . all (
69+ groupedSelectors . entries ( ) . map ( async ( [ targetId , changes ] ) => {
70+ const result = await this . proposalStorage . getEquivalentUnimplementedApprovedChanges ( {
71+ changes : changes as unknown as Change < any > [ ] ,
72+ targetId,
73+ } ) ;
74+ const entries = result
75+ . map ( ( apc , i ) => {
76+ if ( apc ) {
77+ return [ cacheKeyFn ( { change : changes [ i ] , targetId } ) , apc ] as const ;
78+ }
79+ } )
80+ . filter ( e => e !== undefined ) ;
81+ return Object . fromEntries ( entries ) ;
82+ } ) ,
83+ ) ;
84+
85+ // map back to the original selectors
86+ return keys . map ( ( cacheKey ) : SchemaProposalChangeDetailsMapper => {
87+ for ( const targetChangesByCacheId of approvedProposedChangesByCacheId ) {
88+ const approvedRecord = targetChangesByCacheId [ cacheKey ] ;
89+ if ( approvedRecord ) {
90+ return {
91+ schemaProposal : {
92+ id : approvedRecord . proposalId ,
93+ } ,
94+ implementedBy : approvedRecord . schemaVersionId
95+ ? {
96+ id : approvedRecord . schemaVersionId ,
97+ }
98+ : null ,
99+ } ;
100+ }
101+ }
102+ return null ;
103+ } ) ;
104+ } ,
105+ {
106+ cacheKeyFn,
107+ } ,
108+ ) ;
30109 }
31110
32111 async subscribeToSchemaProposalCompositions ( args : { proposalId : string } ) {
@@ -116,6 +195,7 @@ export class SchemaProposalManager {
116195 } ;
117196 }
118197
198+ @cache < { id : string } > ( ( { id } ) => id )
119199 async getProposal ( args : { id : string } ) {
120200 const proposal = await this . proposalStorage . getProposal ( args ) ;
121201
@@ -255,4 +335,90 @@ export class SchemaProposalManager {
255335
256336 throw new HiveError ( 'Not implemented' ) ;
257337 }
338+
339+ @traceFn ( 'SchemaProposalManager.getChangeDetailsForCheck' , {
340+ initAttributes : input => ( {
341+ 'hive.schema_proposal.id' : input . schemaProposalId || '' ,
342+ 'hive.target.id' : input . targetId ,
343+ } ) ,
344+ resultAttributes : result => ( {
345+ 'hive.proposals.details.length' : result ?. length || 0 ,
346+ } ) ,
347+ } )
348+ @cache < {
349+ schemaProposalId : string ;
350+ targetId : string ;
351+ } > ( ( { schemaProposalId, targetId } ) => `${ targetId } /${ schemaProposalId } ` )
352+ async _getImplementedVersionsByProposalId ( args : { schemaProposalId : string ; targetId : string } ) {
353+ return this . proposalStorage . getImplementedVersionsByProposalId ( {
354+ schemaProposalId : args . schemaProposalId ,
355+ targetId : args . targetId ,
356+ } ) ;
357+ }
358+
359+ /**
360+ * If dealing with a non-schema proposal related check or schema version,
361+ * then this function can search to find if any of the check's or historic changes
362+ * are matches for approved schema proposal changes.
363+ *
364+ * Note that if you need the ChangeDetails object for a schema proposal's changes, then use
365+ * `getProposalChangeDetails`.
366+ **/
367+ async getMatchingApprovedProposalChangeDetails ( {
368+ targetId,
369+ change,
370+ } : {
371+ targetId : string ;
372+ change : SchemaChangeType ;
373+ } ) {
374+ return this . approvedChangeProposalLoader . load ( { targetId, change } ) ;
375+ }
376+
377+ /**
378+ * Schema proposals use checks under the hood which makes this a bit confusing.
379+ * This function is for finding `proposalChangeDetails` for a schema proposal's
380+ * associated check's changes -- effectively the proposed changes.
381+ */
382+ async getProposalChangeDetails ( {
383+ schemaProposalId,
384+ targetId,
385+ change,
386+ } : {
387+ targetId : string ;
388+
389+ // The ID of the schema proposal that is proposing this change
390+ schemaProposalId : string ;
391+
392+ // The change record. Used to compare against the returned implemented change
393+ // to verify that they are equal.
394+ change : SchemaChangeType ;
395+ } ) {
396+ // `_getImplementedVersionsByProposalId` is cached so it's safe to call multiple times.
397+ // This is intended to be used by the SchemaChange resolver and would be called on every
398+ // SchemaChange.
399+ const implementedChanges = await this . _getImplementedVersionsByProposalId ( {
400+ schemaProposalId,
401+ targetId,
402+ } ) ;
403+
404+ // Verify which versions implement the associated changes using an exact compare
405+ const implementation = implementedChanges . find (
406+ implementedChange =>
407+ isChangeEqual (
408+ implementedChange . change as unknown as Change < any > ,
409+ change as unknown as Change < any > ,
410+ ) && implementedChange . schemaVersionId ,
411+ ) ;
412+ if ( implementation ) {
413+ return {
414+ schemaProposal : { id : schemaProposalId } ,
415+ implementedBy : implementation ?. schemaVersionId
416+ ? {
417+ id : implementation . schemaVersionId ,
418+ }
419+ : null ,
420+ } ;
421+ }
422+ return null ;
423+ }
258424}
0 commit comments