A social knowledge tool for researchers built on ATProto

further updating of the domain model for clarity

+65 -61
+3 -4
src/modules/annotations/domain/aggregates/Annotation.ts
··· 7 7 import { Guard, IGuardArgument } from "src/shared/core/Guard"; 8 8 import { 9 9 AnnotationFieldId, 10 - AnnotationFieldId, 11 10 AnnotationId, 12 11 AnnotationNote, 13 12 AnnotationTemplateId, 14 - CuratorId, // Import the new value object 13 + CuratorId, 15 14 PublishedRecordId, 16 15 } from "../value-objects"; 17 16 ··· 21 20 >; 22 21 23 22 export interface AnnotationProps { 24 - curatorId: CuratorId; // Add the curator ID 23 + curatorId: CuratorId; 25 24 url: URI; 26 25 annotationFieldId: AnnotationFieldId; 27 26 value: AnnotationValue; ··· 81 80 id?: UniqueEntityID 82 81 ): Result<Annotation> { 83 82 const guardArgs: IGuardArgument[] = [ 84 - { argument: props.curatorId, argumentName: "curatorId" }, // Add curatorId to guard 83 + { argument: props.curatorId, argumentName: "curatorId" }, 85 84 { argument: props.url, argumentName: "url" }, 86 85 { argument: props.annotationFieldId, argumentName: "annotationFieldId" }, 87 86 { argument: props.value, argumentName: "value" },
+12 -8
src/modules/annotations/domain/aggregates/AnnotationField.ts
··· 6 6 import { AnnotationFieldName } from "../value-objects/AnnotationFieldName"; 7 7 import { AnnotationFieldDescription } from "../value-objects/AnnotationFieldDescription"; 8 8 import { AnnotationFieldDefinition } from "../value-objects/AnnotationFieldDefinition"; // Renamed and refactored 9 + import { CuratorId, PublishedRecordId } from "../value-objects"; 9 10 10 11 // Properties required to construct an AnnotationField 11 12 export interface AnnotationFieldProps { 13 + curatorId: CuratorId; 12 14 name: AnnotationFieldName; 13 15 description: AnnotationFieldDescription; 14 16 definition: AnnotationFieldDefinition; 15 - createdAt?: Date; // Optional on creation, will default 16 - // Add other properties like owner DID, etc. if needed 17 + createdAt?: Date; 18 + publishedRecordId?: PublishedRecordId; 17 19 } 18 20 19 21 export class AnnotationField extends AggregateRoot<AnnotationFieldProps> { 20 - 21 22 get fieldId(): AnnotationFieldId { 22 - // Assuming AnnotationFieldId.create takes UniqueEntityID 23 23 return AnnotationFieldId.create(this._id).getValue(); 24 24 } 25 25 26 + get curatorId(): CuratorId { 27 + return this.props.curatorId; 28 + } 29 + 26 30 get name(): AnnotationFieldName { 27 31 return this.props.name; 28 32 } ··· 36 40 } 37 41 38 42 get createdAt(): Date { 39 - // createdAt is guaranteed by the create method's default 40 43 return this.props.createdAt!; 44 + } 45 + 46 + public updatePublishedRecordId(publishedRecordId: PublishedRecordId): void { 47 + this.props.publishedRecordId = publishedRecordId; 41 48 } 42 49 43 50 private constructor(props: AnnotationFieldProps, id?: UniqueEntityID) { ··· 59 66 if (guardResult.isFailure) { 60 67 return Result.fail<AnnotationField>(guardResult.getErrorValue()); 61 68 } 62 - 63 - // Additional validation can be done here on the value objects themselves if needed, 64 - // although their own `create` methods should handle internal consistency. 65 69 66 70 const defaultValues: AnnotationFieldProps = { 67 71 ...props,
+17 -8
src/modules/annotations/domain/value-objects/AnnotationFieldDescription.ts
··· 17 17 super(props); 18 18 } 19 19 20 - public static create(description: string): Result<AnnotationFieldDescription> { 20 + public static create( 21 + description: string 22 + ): Result<AnnotationFieldDescription> { 21 23 // Description can potentially be empty, so only check length if provided 22 24 const descriptionTrimmed = description?.trim(); 23 25 24 - const guardResult = Guard.againstNullOrUndefined(descriptionTrimmed, "description"); 25 - if (guardResult.isFailure) { 26 - // Allow null/undefined to pass through, representing an empty description 27 - // If description MUST exist, change this logic 28 - return Result.ok<AnnotationFieldDescription>(new AnnotationFieldDescription({ value: "" })); 29 - } 26 + const guardResult = Guard.againstNullOrUndefined( 27 + descriptionTrimmed, 28 + "description" 29 + ); 30 + if (guardResult.isFailure) { 31 + // Allow null/undefined to pass through, representing an empty description 32 + // If description MUST exist, change this logic 33 + return Result.ok<AnnotationFieldDescription>( 34 + new AnnotationFieldDescription({ value: "" }) 35 + ); 36 + } 30 37 31 38 if (descriptionTrimmed.length > this.MAX_LENGTH) { 32 39 return Result.fail<AnnotationFieldDescription>( ··· 34 41 ); 35 42 } 36 43 37 - return Result.ok<AnnotationFieldDescription>(new AnnotationFieldDescription({ value: descriptionTrimmed })); 44 + return Result.ok<AnnotationFieldDescription>( 45 + new AnnotationFieldDescription({ value: descriptionTrimmed }) 46 + ); 38 47 } 39 48 }
+17 -11
src/modules/annotations/domain/value-objects/AnnotationType.ts
··· 4 4 value: string; 5 5 } 6 6 7 - // Define known annotation type strings as constants 8 - const DYAD_VALUE_TYPE = "app.annos.annotation#dyadValue"; 9 - const TRIAD_VALUE_TYPE = "app.annos.annotation#triadValue"; 10 - const RATING_VALUE_TYPE = "app.annos.annotation#ratingValue"; 11 - const SINGLE_SELECT_VALUE_TYPE = "app.annos.annotation#singleSelectValue"; 12 - const MULTI_SELECT_VALUE_TYPE = "app.annos.annotation#multiSelectValue"; 7 + const DYAD_VALUE_TYPE = "dyad"; 8 + const TRIAD_VALUE_TYPE = "triad"; 9 + const RATING_VALUE_TYPE = "rating"; 10 + const SINGLE_SELECT_VALUE_TYPE = "singleSelect"; 11 + const MULTI_SELECT_VALUE_TYPE = "multiSelect"; 13 12 14 13 const VALID_TYPES = new Set([ 15 14 DYAD_VALUE_TYPE, ··· 36 35 return new AnnotationType({ value: type }); 37 36 } 38 37 39 - // Pre-create instances for known types for convenience and potential performance 40 38 public static readonly DYAD = new AnnotationType({ value: DYAD_VALUE_TYPE }); 41 - public static readonly TRIAD = new AnnotationType({ value: TRIAD_VALUE_TYPE }); 42 - public static readonly RATING = new AnnotationType({ value: RATING_VALUE_TYPE }); 43 - public static readonly SINGLE_SELECT = new AnnotationType({ value: SINGLE_SELECT_VALUE_TYPE }); 44 - public static readonly MULTI_SELECT = new AnnotationType({ value: MULTI_SELECT_VALUE_TYPE }); 39 + public static readonly TRIAD = new AnnotationType({ 40 + value: TRIAD_VALUE_TYPE, 41 + }); 42 + public static readonly RATING = new AnnotationType({ 43 + value: RATING_VALUE_TYPE, 44 + }); 45 + public static readonly SINGLE_SELECT = new AnnotationType({ 46 + value: SINGLE_SELECT_VALUE_TYPE, 47 + }); 48 + public static readonly MULTI_SELECT = new AnnotationType({ 49 + value: MULTI_SELECT_VALUE_TYPE, 50 + }); 45 51 }
+4 -20
src/modules/annotations/domain/value-objects/AnnotationValue.ts
··· 1 + import e from "express"; 1 2 import { ValueObject } from "../../../../shared/domain/ValueObject"; 3 + import { Annotation } from "../aggregates"; 2 4 import { AnnotationType } from "./AnnotationType"; 3 5 4 6 // Define interfaces for the props of each value object type ··· 22 24 23 25 // Abstract base class for all annotation values, extending the shared ValueObject 24 26 export abstract class AnnotationValueBase< 25 - T extends object 27 + T extends object, 26 28 > extends ValueObject<T> { 27 29 abstract readonly type: AnnotationType; 28 30 ··· 33 35 * @returns True if the types match, false otherwise. 34 36 */ 35 37 public isSameType(other?: AnnotationValueBase<any>): boolean { 36 - // Use the equals method of the AnnotationType value object 37 38 return !!other && this.type.equals(other.type); 38 39 } 39 40 } ··· 57 58 } 58 59 return new DyadValue({ value }); 59 60 } 60 - 61 - // Base ValueObject.equals should suffice as it compares props 62 61 } 63 62 64 63 export class TriadValue extends AnnotationValueBase<ITriadValueProps> { ··· 73 72 get vertexC(): number { 74 73 return this.props.vertexC; 75 74 } 76 - // sum is an invariant checked at creation, not stored state 77 75 78 76 private constructor(props: ITriadValueProps) { 79 77 super(props); ··· 92 90 } 93 91 return new TriadValue({ vertexA, vertexB, vertexC }); 94 92 } 95 - 96 - // Base ValueObject.equals should suffice 97 93 } 98 94 99 95 export class RatingValue extends AnnotationValueBase<IRatingValueProps> { ··· 115 111 } 116 112 return new RatingValue({ rating }); 117 113 } 118 - 119 - // Base ValueObject.equals should suffice 120 114 } 121 115 122 116 export class SingleSelectValue extends AnnotationValueBase<ISingleSelectValueProps> { ··· 138 132 } 139 133 return new SingleSelectValue({ option }); 140 134 } 141 - 142 - // Base ValueObject.equals should suffice 143 135 } 144 136 145 137 export class MultiSelectValue extends AnnotationValueBase<IMultiSelectValueProps> { ··· 167 159 const uniqueSortedOptions = [...new Set(options)].sort(); 168 160 return new MultiSelectValue({ options: uniqueSortedOptions }); 169 161 } 170 - 171 - // Base ValueObject.equals should suffice because options in props are sorted 172 162 } 173 163 174 - // Union type of all concrete AnnotationValue implementations 175 - export type AnnotationValue = 176 - | DyadValue 177 - | TriadValue 178 - | RatingValue 179 - | SingleSelectValue 180 - | MultiSelectValue; 164 + export type AnnotationValue = AnnotationValueBase<object>;
+4 -2
src/modules/annotations/domain/value-objects/CuratorId.ts
··· 31 31 32 32 const didTrimmed = did.trim(); 33 33 if (didTrimmed.length === 0) { 34 - return Result.fail<CuratorId>("CuratorId cannot be empty."); 34 + return Result.fail<CuratorId>("CuratorId cannot be empty."); 35 35 } 36 36 37 37 if (!DID_REGEX.test(didTrimmed)) { 38 - return Result.fail<CuratorId>(`Invalid CuratorId format (must be a valid DID): ${didTrimmed}`); 38 + return Result.fail<CuratorId>( 39 + `Invalid CuratorId format (must be a valid DID): ${didTrimmed}` 40 + ); 39 41 } 40 42 41 43 return Result.ok<CuratorId>(new CuratorId({ value: didTrimmed }));
src/modules/annotations/domain/value-objects/FieldDefinition.ts src/modules/annotations/domain/value-objects/AnnotationFieldDefinition.ts
+2 -1
src/modules/annotations/domain/value-objects/index.ts
··· 1 1 export * from "./AnnotationValue"; 2 - export * from "./FieldDefinition"; 2 + export * from "./AnnotationFieldDefinition"; 3 3 export * from "./Identifier"; 4 4 export * from "./URI"; 5 5 export * from "./AnnotationFieldId"; ··· 7 7 export * from "./AnnotationNote"; 8 8 export * from "./AnnotationId"; 9 9 export * from "./PublishedRecordId"; 10 + export * from "./CuratorId";
+5 -6
src/shared/domain/ValueObject.ts
··· 1 - 2 1 interface ValueObjectProps { 3 2 [index: string]: any; 4 3 } ··· 11 10 export abstract class ValueObject<T extends ValueObjectProps> { 12 11 public props: T; 13 12 14 - constructor (props: T) { 13 + constructor(props: T) { 15 14 let baseProps: any = { 16 - ...props, 17 - } 15 + ...props, 16 + }; 18 17 19 18 this.props = baseProps; 20 19 } 21 20 22 - public equals (vo?: ValueObject<T>) : boolean { 21 + public equals(vo?: ValueObject<T>): boolean { 23 22 if (vo === null || vo === undefined) { 24 23 return false; 25 24 } ··· 28 27 } 29 28 return JSON.stringify(this.props) === JSON.stringify(vo.props); 30 29 } 31 - } 30 + }
+1 -1
src/user/domain/value-objects/DID.ts
··· 30 30 31 31 const didTrimmed = did.trim(); 32 32 if (didTrimmed.length === 0) { 33 - return Result.fail<DID>("DID cannot be empty."); 33 + return Result.fail<DID>("DID cannot be empty."); 34 34 } 35 35 36 36 if (!DID_REGEX.test(didTrimmed)) {