Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 311 lines 9.6 kB view raw
1import { 2 ContainerTypes, 3 getScalarType, 4 ScalarTypes, 5 type Field, 6 type ScalarType, 7} from '@roostorg/types'; 8import _ from 'lodash'; 9 10import { inject } from '../../iocContainer/index.js'; 11import { assertUnreachable } from '../../utils/misc.js'; 12import { 13 CoopInput, 14 type ItemSchema, 15} from '../moderationConfigService/index.js'; 16import { 17 type SignalInputType, 18 type SignalOutputType, 19} from '../signalsService/index.js'; 20import { 21 derivedFieldRecipes, 22 getDerivedFieldInputTypes, 23 getDerivedFieldIsEnabled, 24 getDerivedFieldOutputType, 25 type DerivedFieldRecipe, 26 type DerivedFieldSpec, 27 type DerivedFieldType, 28} from './helpers.js'; 29 30type AnnotatedDerivedFieldRecipe = { 31 type: DerivedFieldType; 32 recipe: DerivedFieldRecipe; 33 outputType: SignalOutputType; 34}; 35 36type DerivedField = Pick<Field, 'type' | 'container' | 'name'> & { 37 spec: DerivedFieldSpec; 38}; 39 40export const makeDerivedFieldsService = inject( 41 ['SignalsService'], 42 function (signalsService) { 43 const getSignal = signalsService.getSignalOrThrow.bind(signalsService); 44 const getSignalDisabled = 45 signalsService.getSignalDisabledForOrg.bind(signalsService); 46 47 const recipesByFieldTypeEntries = Object.entries(derivedFieldRecipes) as [ 48 DerivedFieldType, 49 DerivedFieldRecipe, 50 ][]; 51 52 const getAnnotatedDerivedFieldRecipesByInputType = async ( 53 orgId: string, 54 ): Promise<{ 55 [K in SignalInputType]?: AnnotatedDerivedFieldRecipe[]; 56 }> => { 57 const annotatedDerivedFieldRecipes = await Promise.all( 58 recipesByFieldTypeEntries.map(async ([fieldType, recipe]) => { 59 const enabled = await getDerivedFieldIsEnabled( 60 getSignalDisabled, 61 fieldType, 62 orgId, 63 ); 64 if (!enabled) { 65 return []; 66 } 67 const inputTypes = await getDerivedFieldInputTypes( 68 getSignal, 69 fieldType, 70 orgId, 71 ); 72 return Promise.all( 73 inputTypes.map(async (inputType) => ({ 74 type: fieldType, 75 recipe, 76 inputType, 77 outputType: await getDerivedFieldOutputType( 78 getSignal, 79 fieldType, 80 orgId, 81 ), 82 })), 83 ); 84 }), 85 ).then((it) => it.flat()); 86 87 return _.groupBy(annotatedDerivedFieldRecipes, (it) => { 88 // _.groupBy will only work faithfully if the inputTypes ae strings, 89 // as it's gonna use inputType as an object key. Flag that so we don't 90 // forget if we expand the valid input types later. 91 if (typeof it.inputType !== 'string') { 92 throw new Error('Code expected inputType to be a string'); 93 } 94 return it.inputType; 95 }); 96 }; 97 98 const getAnnotatedRecipesForInputType = async ( 99 it: SignalInputType, 100 orgId: string, 101 ) => { 102 return ( 103 (await getAnnotatedDerivedFieldRecipesByInputType(orgId))[it] ?? [] 104 ); 105 }; 106 107 const hasFieldOfScalarType = (schema: ItemSchema, type: ScalarType) => 108 schema.some((field) => getScalarType(field) === type); 109 110 return { 111 async getDerivedFields( 112 contentTypeId: string, 113 schema: ItemSchema, 114 orgId: string, 115 ): Promise<DerivedField[]> { 116 const derivedFieldsFromIndividualFields = await Promise.all( 117 schema.map(async (field) => { 118 const fieldScalarType = getScalarType(field); 119 const applicableRecipes = await getAnnotatedRecipesForInputType( 120 fieldScalarType, 121 orgId, 122 ); 123 124 return applicableRecipes.map((it): DerivedField => { 125 const spec = { 126 derivationType: it.type, 127 source: { 128 type: 'CONTENT_FIELD' as const, 129 name: field.name, 130 contentTypeId, 131 }, 132 }; 133 return { 134 type: it.outputType.scalarType, 135 container: field.container, 136 name: getNameForDerivedField(spec), 137 spec, 138 }; 139 }); 140 }), 141 ).then((it) => it.flat()); 142 143 const derivedFieldsFromCoopInputs = await Promise.all( 144 Object.values(CoopInput).map( 145 async (it): Promise<DerivedField[]> => { 146 // using a switch here is a good way to get exhaustiveness 147 // checking for when we inevitably add new CoopInputs. 148 switch (it) { 149 case CoopInput.AUTHOR_USER: 150 return ( 151 await getAnnotatedRecipesForInputType( 152 ScalarTypes.USER_ID, 153 orgId, 154 ) 155 ).map((recipe) => 156 makeCoopInputDerivedFieldSpec( 157 recipe, 158 CoopInput.AUTHOR_USER, 159 ), 160 ); 161 162 case CoopInput.ALL_TEXT: 163 return hasFieldOfScalarType(schema, ScalarTypes.STRING) 164 ? ( 165 await getAnnotatedRecipesForInputType( 166 ScalarTypes.STRING, 167 orgId, 168 ) 169 ).map((recipe) => 170 makeCoopInputDerivedFieldSpec( 171 recipe, 172 CoopInput.ALL_TEXT, 173 ), 174 ) 175 : []; 176 177 case CoopInput.ANY_GEOHASH: 178 return hasFieldOfScalarType(schema, ScalarTypes.GEOHASH) 179 ? ( 180 await getAnnotatedRecipesForInputType( 181 ScalarTypes.GEOHASH, 182 orgId, 183 ) 184 ).map((recipe) => 185 makeCoopInputDerivedFieldSpec( 186 recipe, 187 CoopInput.ANY_GEOHASH, 188 ), 189 ) 190 : []; 191 192 case CoopInput.ANY_IMAGE: 193 return hasFieldOfScalarType(schema, ScalarTypes.IMAGE) 194 ? ( 195 await getAnnotatedRecipesForInputType( 196 ScalarTypes.IMAGE, 197 orgId, 198 ) 199 ).map((recipe) => 200 makeCoopInputDerivedFieldSpec( 201 recipe, 202 CoopInput.ANY_IMAGE, 203 ), 204 ) 205 : []; 206 207 case CoopInput.ANY_VIDEO: 208 return hasFieldOfScalarType(schema, ScalarTypes.VIDEO) 209 ? ( 210 await getAnnotatedRecipesForInputType( 211 ScalarTypes.VIDEO, 212 orgId, 213 ) 214 ).map((recipe) => 215 makeCoopInputDerivedFieldSpec( 216 recipe, 217 CoopInput.ANY_VIDEO, 218 ), 219 ) 220 : []; 221 case CoopInput.POLICY_ID: 222 case CoopInput.SOURCE: 223 return []; 224 default: 225 assertUnreachable(it); 226 } 227 }, 228 ), 229 ).then((it) => it.flat()); 230 231 return [ 232 ...derivedFieldsFromIndividualFields, 233 ...derivedFieldsFromCoopInputs, 234 ]; 235 }, 236 }; 237 }, 238); 239 240export type DerivedFieldsService = ReturnType<typeof makeDerivedFieldsService>; 241 242function getDisplayStringForDerivationType(derivationType: DerivedFieldType) { 243 switch (derivationType) { 244 case 'VIDEO_TRANSCRIPTION': 245 return 'Transcription'; 246 case 'ENGLISH_TRANSLATION': 247 return 'English Translation'; 248 default: 249 assertUnreachable(derivationType); 250 } 251} 252 253/** 254 * This returns a human-readable name (which may eventually be localized) for 255 * the field defined by a derived field's spec. 256 */ 257export function getNameForDerivedField(spec: DerivedFieldSpec) { 258 const { source, derivationType } = spec; 259 const derivationTypeName = getDisplayStringForDerivationType(derivationType); 260 261 switch (source.type) { 262 case 'CONTENT_FIELD': 263 case 'CONTENT_COOP_INPUT': 264 return `${source.name}'s ${derivationTypeName}`; 265 case 'FULL_ITEM': 266 return "Content's " + derivationTypeName; 267 default: 268 assertUnreachable(source); 269 } 270} 271 272function makeCoopInputDerivedFieldSpec( 273 recipe: AnnotatedDerivedFieldRecipe, 274 source: CoopInput, 275): DerivedField { 276 const isScalar = (() => { 277 switch (source) { 278 case CoopInput.ALL_TEXT: 279 case CoopInput.AUTHOR_USER: 280 case CoopInput.POLICY_ID: 281 case CoopInput.SOURCE: 282 return true; 283 case CoopInput.ANY_GEOHASH: 284 case CoopInput.ANY_VIDEO: 285 case CoopInput.ANY_IMAGE: 286 return false; 287 default: 288 assertUnreachable(source); 289 } 290 })(); 291 292 const spec = { 293 derivationType: recipe.type, 294 source: { type: 'CONTENT_COOP_INPUT' as const, name: source }, 295 }; 296 297 return { 298 ...(isScalar 299 ? { type: recipe.outputType.scalarType, container: null } 300 : { 301 type: ContainerTypes.ARRAY, 302 container: { 303 containerType: ContainerTypes.ARRAY, 304 valueScalarType: recipe.outputType.scalarType, 305 keyScalarType: null, 306 }, 307 }), 308 name: getNameForDerivedField(spec), 309 spec, 310 }; 311}