Skip to content

Commit 402bb85

Browse files
authored
Product Mutation Subscriptions (CDC) (#3639)
1 parent dbb9306 commit 402bb85

13 files changed

+979
-66
lines changed

src/components/product-progress/create-product-connection.resolver.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { Loader, type LoaderOf } from '@seedcompany/data-loader';
23
import { Variant } from '~/common';
34
import { Identity } from '~/core/authentication';
4-
import { ProductCreated } from '../product/dto';
5+
import { ProductCreated } from '../product/dto/product-mutations.dto';
6+
import { ProductLoader } from '../product/product.loader';
57
import { ProductProgressService } from './product-progress.service';
68

79
@Resolver(() => ProductCreated)
@@ -15,12 +17,14 @@ export class ProgressReportCreateProductConnectionResolver {
1517
description: 'All available progress variants for this product',
1618
})
1719
async availableVariants(
18-
@Parent() { product }: ProductCreated,
20+
@Parent() { productId }: ProductCreated,
21+
@Loader(ProductLoader) products: LoaderOf<ProductLoader>,
1922
): Promise<readonly Variant[]> {
2023
// TODO move to auth policy
2124
if (this.identity.isAnonymous) {
2225
return [];
2326
}
27+
const product = await products.load(productId);
2428
return await this.service.getAvailableVariantsForProduct(product);
2529
}
2630
}

src/components/product/dto/create-product.dto.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Field, Float, InputType, ObjectType } from '@nestjs/graphql';
1+
import { Field, Float, InputType } from '@nestjs/graphql';
22
import { Transform, Type } from 'class-transformer';
33
import { IsPositive, ValidateNested } from 'class-validator';
44
import { stripIndent } from 'common-tags';
@@ -14,7 +14,6 @@ import { ProductMedium } from './product-medium.enum';
1414
import { ProductMethodology } from './product-methodology.enum';
1515
import { ProductPurpose } from './product-purpose.enum';
1616
import { ProductStep as Step } from './product-step.enum';
17-
import { type AnyProduct, Product } from './product.dto';
1817
import { ProgressMeasurement } from './progress-measurement.enum';
1918

2019
@InputType({
@@ -41,8 +40,8 @@ export abstract class CreateBaseProduct {
4140
@Transform(({ value }) => uniq(value))
4241
readonly steps?: readonly Step[];
4342

44-
@Field({ nullable: true })
45-
readonly describeCompletion?: string;
43+
@Field(() => String, { nullable: true })
44+
readonly describeCompletion?: string | null;
4645

4746
@Field(() => ProgressMeasurement, {
4847
nullable: true,
@@ -109,7 +108,7 @@ export abstract class CreateDerivativeScriptureProduct extends CreateBaseProduct
109108
this property can be set (and read) to "override" the \`producible\`'s list.
110109
`,
111110
})
112-
readonly scriptureReferencesOverride?: readonly ScriptureRangeInput[];
111+
readonly scriptureReferencesOverride?: readonly ScriptureRangeInput[] | null;
113112

114113
@Field({
115114
description: stripIndent`
@@ -129,9 +128,3 @@ export abstract class CreateOtherProduct extends CreateBaseProduct {
129128
@Field(() => String, { nullable: true })
130129
readonly description?: string | null;
131130
}
132-
133-
@ObjectType()
134-
export abstract class ProductCreated {
135-
@Field(() => Product)
136-
readonly product: AnyProduct;
137-
}

src/components/product/dto/delete-product.dto.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/components/product/dto/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export * from './producible.dto';
77
export * from './create-product.dto';
88
export * from './product.dto';
99
export * from './update-product.dto';
10-
export * from './delete-product.dto';
10+
export * from './product-mutations.dto';
1111
export * from './list-product.dto';
1212
export * from './available-steps';
1313
export * from './completion-description.dto';
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { Field, InterfaceType, ObjectType } from '@nestjs/graphql';
2+
import {
3+
AsUpdateType,
4+
Grandparent,
5+
type ID,
6+
IdField,
7+
type Secured,
8+
} from '~/common';
9+
import { LanguageEngagement } from '../../engagement/dto';
10+
import { LanguageEngagementMutation } from '../../engagement/dto/engagement-mutations.dto';
11+
import {
12+
SecuredScriptureRanges,
13+
SecuredScriptureRangesOverride,
14+
SecuredUnspecifiedScripturePortion,
15+
} from '../../scripture/dto';
16+
import { type ProducibleRef, SecuredProducible } from './producible.dto';
17+
import {
18+
DerivativeScriptureProduct,
19+
DirectScriptureProduct,
20+
OtherProduct,
21+
} from './product.dto';
22+
import {
23+
UpdateBaseProduct,
24+
UpdateDerivativeScriptureProduct,
25+
UpdateDirectScriptureProduct,
26+
UpdateOtherProduct,
27+
} from './update-product.dto';
28+
29+
@InterfaceType({ implements: [LanguageEngagementMutation] })
30+
export class ProductMutationOrDeletion extends LanguageEngagementMutation {
31+
/** Why here? See {@link EngagementMutation.projectId} */
32+
@IdField()
33+
readonly productId: ID<'Product'>;
34+
}
35+
36+
@InterfaceType({ implements: [ProductMutationOrDeletion] })
37+
export class ProductMutation extends ProductMutationOrDeletion {}
38+
39+
@InterfaceType({ implements: [ProductMutation] })
40+
export class DirectScriptureProductMutation extends ProductMutation {
41+
@Field(() => DirectScriptureProduct)
42+
readonly product?: never;
43+
}
44+
45+
@InterfaceType({ implements: [ProductMutation] })
46+
export class DerivativeScriptureProductMutation extends ProductMutation {
47+
@Field(() => DerivativeScriptureProduct)
48+
readonly product?: never;
49+
}
50+
51+
@InterfaceType({ implements: [ProductMutation] })
52+
export class OtherProductMutation extends ProductMutation {
53+
@Field(() => OtherProduct)
54+
readonly product?: never;
55+
}
56+
57+
@InterfaceType({ implements: [ProductMutation] })
58+
export abstract class ProductCreated extends ProductMutation {}
59+
60+
@ObjectType({
61+
implements: [DirectScriptureProductMutation, ProductCreated],
62+
})
63+
export class DirectScriptureProductCreated extends ProductCreated {
64+
declare readonly __typename: 'DirectScriptureProductCreated';
65+
66+
@Field(() => LanguageEngagement)
67+
declare readonly engagement?: never;
68+
69+
@Field(() => DirectScriptureProduct)
70+
readonly product?: never;
71+
}
72+
73+
@ObjectType({
74+
implements: [DerivativeScriptureProductMutation, ProductCreated],
75+
})
76+
export class DerivativeScriptureProductCreated extends ProductCreated {
77+
declare readonly __typename: 'DerivativeScriptureProductCreated';
78+
79+
@Field(() => LanguageEngagement)
80+
declare readonly engagement?: never;
81+
82+
@Field(() => DerivativeScriptureProduct)
83+
readonly product?: never;
84+
}
85+
86+
@ObjectType({ implements: [OtherProductMutation, ProductCreated] })
87+
export class OtherProductCreated extends ProductCreated {
88+
declare readonly __typename: 'OtherProductCreated';
89+
90+
@Field(() => LanguageEngagement)
91+
declare readonly engagement?: never;
92+
93+
@Field(() => OtherProduct)
94+
readonly product?: never;
95+
}
96+
97+
@InterfaceType()
98+
export class ProductUpdate extends AsUpdateType(UpdateBaseProduct, {
99+
omit: ['id'],
100+
links: [],
101+
}) {}
102+
103+
@ObjectType({ implements: [ProductUpdate] })
104+
export class DirectScriptureProductUpdate extends AsUpdateType(
105+
UpdateDirectScriptureProduct,
106+
{
107+
omit: [
108+
'id',
109+
'scriptureReferences',
110+
'unspecifiedScripture',
111+
'totalVerses',
112+
'totalVerseEquivalents',
113+
],
114+
links: [],
115+
},
116+
) {
117+
@Field({ nullable: true })
118+
readonly scriptureReferences?: SecuredScriptureRanges;
119+
120+
@Field({ nullable: true })
121+
readonly unspecifiedScripture?: SecuredUnspecifiedScripturePortion;
122+
}
123+
124+
@ObjectType({ implements: [ProductUpdate] })
125+
export class DerivativeScriptureProductUpdate extends AsUpdateType(
126+
UpdateDerivativeScriptureProduct,
127+
{
128+
omit: [
129+
'id',
130+
'produces',
131+
'scriptureReferencesOverride',
132+
'totalVerses',
133+
'totalVerseEquivalents',
134+
],
135+
links: [],
136+
},
137+
) {
138+
@Field(() => SecuredProducible, { nullable: true })
139+
readonly produces?: Secured<ProducibleRef>;
140+
141+
@Field(() => SecuredScriptureRangesOverride, { nullable: true })
142+
readonly scriptureReferencesOverride?: SecuredScriptureRangesOverride;
143+
}
144+
145+
@ObjectType({ implements: [ProductUpdate] })
146+
export class OtherProductUpdate extends AsUpdateType(UpdateOtherProduct, {
147+
omit: ['id'],
148+
links: [],
149+
}) {}
150+
151+
@InterfaceType({ implements: [ProductMutation] })
152+
export class ProductUpdated extends ProductMutation {
153+
@Field({ middleware: [Grandparent.store] })
154+
readonly previous: ProductUpdate;
155+
156+
@Field({ middleware: [Grandparent.store] })
157+
readonly updated: ProductUpdate;
158+
}
159+
160+
@ObjectType({
161+
implements: [DirectScriptureProductMutation, ProductUpdated],
162+
})
163+
export class DirectScriptureProductUpdated extends ProductUpdated {
164+
declare readonly __typename: 'DirectScriptureProductUpdated';
165+
166+
@Field(() => LanguageEngagement)
167+
declare readonly engagement?: never;
168+
169+
@Field({ middleware: [Grandparent.store] })
170+
declare readonly previous: DirectScriptureProductUpdate;
171+
172+
@Field({ middleware: [Grandparent.store] })
173+
declare readonly updated: DirectScriptureProductUpdate;
174+
175+
@Field(() => DirectScriptureProduct)
176+
readonly product?: never;
177+
}
178+
179+
@ObjectType({
180+
implements: [DerivativeScriptureProductMutation, ProductUpdated],
181+
})
182+
export class DerivativeScriptureProductUpdated extends ProductUpdated {
183+
declare readonly __typename: 'DerivativeScriptureProductUpdated';
184+
185+
@Field(() => LanguageEngagement)
186+
declare readonly engagement?: never;
187+
188+
@Field({ middleware: [Grandparent.store] })
189+
declare readonly previous: DerivativeScriptureProductUpdate;
190+
191+
@Field({ middleware: [Grandparent.store] })
192+
declare readonly updated: DerivativeScriptureProductUpdate;
193+
194+
@Field(() => DerivativeScriptureProduct)
195+
readonly product?: never;
196+
}
197+
198+
@ObjectType({ implements: [OtherProductMutation, ProductUpdated] })
199+
export class OtherProductUpdated extends ProductUpdated {
200+
declare readonly __typename: 'OtherProductUpdated';
201+
202+
@Field(() => LanguageEngagement)
203+
declare readonly engagement?: never;
204+
205+
@Field({ middleware: [Grandparent.store] })
206+
declare readonly previous: OtherProductUpdate;
207+
208+
@Field({ middleware: [Grandparent.store] })
209+
declare readonly updated: OtherProductUpdate;
210+
211+
@Field(() => OtherProduct)
212+
readonly product?: never;
213+
}
214+
215+
@InterfaceType({ implements: [ProductMutationOrDeletion] })
216+
export class ProductDeleted extends ProductMutationOrDeletion {}
217+
218+
@ObjectType({ implements: [ProductDeleted] })
219+
export class DirectScriptureProductDeleted extends ProductDeleted {
220+
declare readonly __typename: 'DirectScriptureProductDeleted';
221+
222+
@Field(() => LanguageEngagement)
223+
declare readonly engagement?: never;
224+
}
225+
226+
@ObjectType({ implements: [ProductDeleted] })
227+
export class DerivativeScriptureProductDeleted extends ProductDeleted {
228+
declare readonly __typename: 'DerivativeScriptureProductDeleted';
229+
230+
@Field(() => LanguageEngagement)
231+
declare readonly engagement?: never;
232+
}
233+
234+
@ObjectType({ implements: [ProductDeleted] })
235+
export class OtherProductDeleted extends ProductDeleted {
236+
declare readonly __typename: 'OtherProductDeleted';
237+
238+
@Field(() => LanguageEngagement)
239+
declare readonly engagement?: never;
240+
}

src/components/product/dto/update-product.dto.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Field, InputType, ObjectType } from '@nestjs/graphql';
1+
import { Field, InputType } from '@nestjs/graphql';
22
import { stripIndent } from 'common-tags';
33
import {
44
type ID,
@@ -13,7 +13,6 @@ import {
1313
CreateDerivativeScriptureProduct,
1414
CreateDirectScriptureProduct,
1515
} from './create-product.dto';
16-
import { type AnyProduct, Product } from './product.dto';
1716

1817
@InputType()
1918
export abstract class UpdateBaseProduct extends OmitType(CreateBaseProduct, [
@@ -65,9 +64,3 @@ export abstract class UpdateOtherProduct extends UpdateBaseProduct {
6564
@Field(() => String, { nullable: true })
6665
readonly description?: string | null;
6766
}
68-
69-
@ObjectType()
70-
export abstract class ProductUpdated {
71-
@Field(() => Product)
72-
readonly product: AnyProduct;
73-
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { Loader, type LoaderOf } from '@seedcompany/data-loader';
3+
import { type AnyProduct, Product } from './dto';
4+
import { ProductMutation } from './dto/product-mutations.dto';
5+
import { ProductLoader } from './product.loader';
6+
7+
@Resolver(ProductMutation)
8+
export class ProductMutationLinksResolver {
9+
@ResolveField(() => Product)
10+
async product(
11+
@Parent() change: ProductMutation,
12+
@Loader(ProductLoader) products: LoaderOf<ProductLoader>,
13+
): Promise<AnyProduct> {
14+
return await products.load(change.productId);
15+
}
16+
}

0 commit comments

Comments
 (0)