Skip to content

Commit 9bdd8d0

Browse files
committed
Product Mutation Subscriptions (CDC)
1 parent 8acb02e commit 9bdd8d0

13 files changed

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

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)