diff --git a/api/openapi.yaml b/api/openapi.yaml index 03c1f684b..7ee24bf27 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1902,6 +1902,201 @@ components: - last_at - cache_revision type: object + Employment: + additionalProperties: true + properties: + address_id: + format: int64 + type: integer + confidence: + format: double + type: number + created_at: + format: date-time + type: string + department: + type: string + description: + type: string + end_date: + $ref: "#/components/schemas/PartialDate" + id: + format: int64 + type: integer + is_current: + type: boolean + is_primary: + type: boolean + location: + type: string + organization_id: + format: int64 + type: integer + person_id: + format: int64 + type: integer + revision: + format: int64 + type: integer + role: + type: string + source: + type: string + source_ref: + type: string + start_date: + $ref: "#/components/schemas/PartialDate" + title: + type: string + updated_at: + format: date-time + type: string + required: + - id + - person_id + - organization_id + - is_current + - is_primary + - source + - revision + - created_at + - updated_at + type: object + EmploymentBody: + additionalProperties: false + properties: + address_id: + format: int64 + type: + - integer + - "null" + confidence: + format: double + type: + - number + - "null" + department: + type: + - string + - "null" + description: + type: + - string + - "null" + end_date: + type: + - string + - "null" + is_current: + type: + - boolean + - "null" + is_primary: + type: + - boolean + - "null" + location: + type: + - string + - "null" + organization_id: + format: int64 + type: integer + person_id: + format: int64 + type: integer + role: + type: + - string + - "null" + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + type: + - string + - "null" + start_date: + type: + - string + - "null" + title: + type: + - string + - "null" + required: + - person_id + - organization_id + - source + type: object + EmploymentProjectionResponse: + additionalProperties: true + properties: + department: + type: string + employment_id: + format: int64 + type: integer + organization_id: + format: int64 + type: integer + organization_name: + type: string + role: + type: string + title: + type: string + vcard: + $ref: "#/components/schemas/EmploymentVCard" + required: + - employment_id + - organization_id + - organization_name + - vcard + type: object + EmploymentVCard: + additionalProperties: true + properties: + org: + items: + type: string + type: + - array + - "null" + role: + type: string + title: + type: string + type: object + EmploymentsResponse: + additionalProperties: true + properties: + employments: + items: + $ref: "#/components/schemas/Employment" + type: + - array + - "null" + projection: + $ref: "#/components/schemas/EmploymentProjectionResponse" + required: + - employments + type: object + EndEmploymentBody: + additionalProperties: false + properties: + end_date: + type: string + required: + - end_date + type: object EntryRow: additionalProperties: true properties: @@ -3388,6 +3583,19 @@ components: required: - email type: object + MergeOrganizationBody: + additionalProperties: false + properties: + losing_organization_id: + format: int64 + type: integer + losing_revision: + format: int64 + type: integer + required: + - losing_organization_id + - losing_revision + type: object MessageDetail: additionalProperties: true properties: @@ -3594,259 +3802,202 @@ components: required: - busy type: object - PartialDate: - additionalProperties: false + Organization: + additionalProperties: true properties: - day: + created_at: + format: date-time + type: string + description: + type: string + id: format: int64 type: integer - month: + kind: + type: string + merged_into_id: format: int64 type: integer - year: + name: + type: string + primary_domain: + type: string + retired_at: + format: date-time + type: string + revision: format: int64 type: integer + updated_at: + format: date-time + type: string + required: + - id + - name + - kind + - revision + - created_at + - updated_at type: object - ParticipantContactObservation: + OrganizationAddress: additionalProperties: true properties: address_kind: type: string + country_code: + type: string + country_name: + type: string envelope: $ref: "#/components/schemas/ValueEnvelope" - normalization: + extended_address: type: string - normalization_version: - format: int64 - type: integer - normalized_value: + extended_components: type: string - observed_at: - format: date-time + free_text: type: string - original_value: + geo_uri: type: string - participant_id: + label: + type: string + locality: + type: string + organization_id: format: int64 type: integer - provider_user_id: + original_value: type: string - scope_kind: + place_uri: type: string - scope_value: + post_office_box: type: string - service_slug: + postal_code: + type: string + region: + type: string + street_address: + type: string + timezone: type: string - source_id: - format: int64 - type: integer required: - envelope - - participant_id + - organization_id - address_kind - original_value - - normalized_value - - normalization - - normalization_version type: object - PatchAttributeDefinitionRequest: + OrganizationAddressBody: additionalProperties: false properties: - description: + address_kind: + type: string + country_code: type: - string - "null" - display_order: - format: int64 - type: integer - is_active: - type: boolean - label: - type: string - type: object - PatchPersonRequest: - additionalProperties: false - properties: - display_name: + country_name: type: - string - "null" - required: - - display_name - type: object - PatchSavedViewRequest: - additionalProperties: false - properties: - canonical_state: - $ref: "#/components/schemas/SavedViewStateEnvelope" - description: - type: string - name: - type: string - schema_version: - format: int64 - type: integer - type: object - Person: - additionalProperties: true - properties: - created_at: - format: date-time - type: string - display_name: - type: string - id: - format: int64 - type: integer - participant_ids: - items: - format: int64 - type: integer + extended_address: type: - - array + - string - "null" - revision: - format: int64 - type: integer - updated_at: - format: date-time - type: string - vcard_uid: - type: string - required: - - id - - vcard_uid - - revision - - participant_ids - - created_at - - updated_at - type: object - PersonAddress: - additionalProperties: true - properties: - address_kind: - type: string - country_code: - type: string - country_name: - type: string - envelope: - $ref: "#/components/schemas/ValueEnvelope" - extended_address: - type: string extended_components: - type: string + type: + - string + - "null" free_text: - type: string + type: + - string + - "null" geo_uri: - type: string + type: + - string + - "null" label: - type: string + type: + - string + - "null" locality: - type: string - original_value: - type: string - person_id: + type: + - string + - "null" + ordinal: format: int64 + minimum: 0 type: integer - place_uri: - type: string - post_office_box: - type: string - postal_code: - type: string - region: - type: string - street_address: - type: string - timezone: - type: string - required: - - envelope - - person_id - - address_kind - - original_value - type: object - PersonAddressInputRequest: - additionalProperties: false - properties: - address_kind: - type: string - country_code: - type: string - country_name: - type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - extended_address: - type: string - extended_components: - type: string - free_text: - type: string - geo_uri: - type: string - label: - type: string - locality: - type: string original_value: type: string place_uri: - type: string + type: + - string + - "null" post_office_box: - type: string + type: + - string + - "null" postal_code: - type: string + type: + - string + - "null" + pref: + format: int64 + type: + - integer + - "null" region: + type: + - string + - "null" + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string + source_ref: + type: + - string + - "null" street_address: - type: string + type: + - string + - "null" timezone: - type: string - required: - - address_kind - - envelope - type: object - PersonAddressPatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonAddressInputRequest" type: - - array + - string - "null" - supersede: - items: - format: int64 - type: integer + vcard_altid: type: - - array + - string - "null" - type: object - PersonAttributeGroup: - additionalProperties: true - properties: - current: - items: - $ref: "#/components/schemas/PersonAttributeValue" + vcard_group: type: - - array + - string - "null" - definition: - $ref: "#/components/schemas/AttributeDefinition" - history: + vcard_pid: items: - $ref: "#/components/schemas/PersonAttributeValue" + type: string type: - array - "null" + vcard_prop_id: + type: + - string + - "null" + vcard_property: + type: + - string + - "null" required: - - definition - - current + - address_kind + - source type: object - PersonAttributeValue: + OrganizationAttributeValue: additionalProperties: true properties: active_from: @@ -3874,7 +4025,7 @@ components: ordinal: format: int64 type: integer - person_id: + organization_id: format: int64 type: integer source: @@ -3888,7 +4039,7 @@ components: $ref: "#/components/schemas/AttributeValue" required: - id - - person_id + - organization_id - definition_id - definition_slug - ordinal @@ -3897,118 +4048,130 @@ components: - created_at - source type: object - PersonAttributeWrite: + OrganizationAttributeWrite: additionalProperties: true properties: dry_run: type: boolean superseded: - $ref: "#/components/schemas/PersonAttributeValue" + $ref: "#/components/schemas/OrganizationAttributeValue" value: - $ref: "#/components/schemas/PersonAttributeValue" + $ref: "#/components/schemas/OrganizationAttributeValue" required: - dry_run type: object - PersonAttributesResponse: + OrganizationAttributesResponse: additionalProperties: true properties: - attributes: + values: items: - $ref: "#/components/schemas/PersonAttributeGroup" + $ref: "#/components/schemas/OrganizationAttributeValue" type: - array - "null" - person_id: - format: int64 - type: integer required: - - person_id - - attributes + - values type: object - PersonCategory: + OrganizationBody: + additionalProperties: false + properties: + description: + type: + - string + - "null" + kind: + enum: + - company + - nonprofit + - school + - government + - household + - other + type: string + name: + type: string + primary_domain: + type: + - string + - "null" + retired: + type: boolean + required: + - name + type: object + OrganizationCategory: additionalProperties: true properties: - envelope: - $ref: "#/components/schemas/ValueEnvelope" - normalized_value: + category: type: string - original_value: + category_normalized: type: string - person_id: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + organization_id: format: int64 type: integer required: - envelope - - person_id - - original_value - - normalized_value + - organization_id + - category + - category_normalized type: object - PersonCategoryInputRequest: + OrganizationCategoryBody: additionalProperties: false properties: - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - original_value: + category: type: string - required: - - original_value - - envelope - type: object - PersonCategoryPatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonCategoryInputRequest" + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 type: - - array + - integer - "null" - supersede: - items: - format: int64 - type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: type: - - array + - string - "null" - type: object - PersonCluster: - additionalProperties: true - properties: - canonical_id: - format: int64 - type: integer - edges: - items: - $ref: "#/components/schemas/PersonClusterEdge" + vcard_altid: type: - - array + - string - "null" - member_ids: + vcard_group: + type: + - string + - "null" + vcard_pid: items: - format: int64 - type: integer + type: string type: - array - "null" + vcard_prop_id: + type: + - string + - "null" + vcard_property: + type: + - string + - "null" required: - - canonical_id - - member_ids - - edges - type: object - PersonClusterEdge: - additionalProperties: true - properties: - participant_a: - format: int64 - type: integer - participant_b: - format: int64 - type: integer - required: - - participant_a - - participant_b + - category + - source type: object - PersonContactPoint: + OrganizationContactPoint: additionalProperties: true properties: address_kind: @@ -4022,11 +4185,11 @@ components: type: integer normalized_value: type: string - original_value: - type: string - person_id: + organization_id: format: int64 type: integer + original_value: + type: string scope_kind: type: string scope_value: @@ -4037,157 +4200,268 @@ components: type: string required: - envelope - - person_id + - organization_id - address_kind - original_value - normalized_value - normalization - normalization_version type: object - PersonContactPointInputRequest: + OrganizationContactPointBody: additionalProperties: false properties: - address_kind: + contact_kind: + enum: + - email + - phone + - username + - impp + - url + - social + - calendar + - contact_uri + - org_directory + - language type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" + ordinal: + format: int64 + minimum: 0 + type: integer original_value: type: string + pref: + format: int64 + type: + - integer + - "null" scope_kind: - type: string + type: + - string + - "null" scope_value: - type: string + type: + - string + - "null" service_slug: + type: + - string + - "null" + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string + source_ref: + type: + - string + - "null" uri: - type: string - required: - - address_kind - - original_value - - envelope - type: object - PersonContactPointPatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonContactPointInputRequest" type: - - array + - string - "null" - supersede: + vcard_altid: + type: + - string + - "null" + vcard_group: + type: + - string + - "null" + vcard_pid: items: - format: int64 - type: integer + type: string type: - array - "null" + vcard_prop_id: + type: + - string + - "null" + vcard_property: + type: + - string + - "null" + required: + - contact_kind + - original_value + - source type: object - PersonContextSummaryHTTPResponse: - additionalProperties: true + OrganizationCreateBody: + additionalProperties: false properties: - cache_revision: + description: + type: + - string + - "null" + kind: + enum: + - company + - nonprofit + - school + - government + - household + - other type: string - candidate_snapshot_id: + name: type: string - search_provenance: - $ref: "#/components/schemas/SearchProvenance" - summary: - $ref: "#/components/schemas/PersonSummary" + primary_domain: + type: + - string + - "null" required: - - summary - - cache_revision - - search_provenance + - name type: object - PersonDate: + OrganizationDuplicateSuggestion: additionalProperties: true properties: - calendar_scale: - type: string - date: - $ref: "#/components/schemas/PartialDate" - date_kind: - type: string - date_text: + confidence: + format: double + type: number + created_at: + format: date-time type: string - envelope: - $ref: "#/components/schemas/ValueEnvelope" - label: + criterion: type: string - original_value: + evidence: type: string - person_id: + id: format: int64 type: integer - required: - - envelope - - person_id - - date_kind - - date - - original_value - type: object - PersonDateInputRequest: - additionalProperties: false - properties: - calendar_scale: + organization_a_id: + format: int64 + type: integer + organization_b_id: + format: int64 + type: integer + resolution_note: type: string - date: - $ref: "#/components/schemas/PartialDate" - date_kind: + resolved_at: + format: date-time type: string - date_text: + resolved_by: type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - label: + source: type: string - original_value: + status: + type: string + updated_at: + format: date-time type: string required: - - date_kind - - envelope + - id + - organization_a_id + - organization_b_id + - criterion + - evidence + - confidence + - status + - source + - created_at + - updated_at type: object - PersonDatePatchRequest: - additionalProperties: false + OrganizationDuplicateSuggestionsResponse: + additionalProperties: true properties: - add: - items: - $ref: "#/components/schemas/PersonDateInputRequest" - type: - - array - - "null" - supersede: + suggestions: items: - format: int64 - type: integer + $ref: "#/components/schemas/OrganizationDuplicateSuggestion" type: - array - "null" + required: + - suggestions type: object - PersonIdentifier: + OrganizationIdentifier: additionalProperties: true properties: - display_value: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + identifier_kind: type: string - is_primary: - type: boolean - participant_id: - format: int64 - type: integer - provenance: + identifier_value: type: string - type: + normalized_value: type: string - value: + organization_id: + format: int64 + type: integer + required: + - envelope + - organization_id + - identifier_kind + - identifier_value + - normalized_value + type: object + OrganizationIdentifierBody: + additionalProperties: false + properties: + identifier_kind: + enum: + - domain + - linkedin + - duns + - tax_id + - registry + - other + type: string + identifier_value: + type: string + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 + type: + - integer + - "null" + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string + source_ref: + type: + - string + - "null" + vcard_altid: + type: + - string + - "null" + vcard_group: + type: + - string + - "null" + vcard_pid: + items: + type: string + type: + - array + - "null" + vcard_prop_id: + type: + - string + - "null" + vcard_property: + type: + - string + - "null" required: - - type - - value - - is_primary - - provenance - - participant_id + - identifier_kind + - identifier_value + - source type: object - PersonMedia: + OrganizationMedia: additionalProperties: true properties: byte_size: @@ -4203,2034 +4477,1899 @@ components: type: string media_type: type: string - original_value: - type: string - person_id: + organization_id: format: int64 type: integer + original_value: + type: string uri: type: string required: - envelope - - person_id + - organization_id - media_kind - has_data - original_value type: object - PersonMediaInputRequest: + OrganizationMediaBody: additionalProperties: false properties: data: contentEncoding: base64 type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" media_kind: + enum: + - photo + - logo + - sound + - key type: string media_type: - type: string + type: + - string + - "null" + ordinal: + format: int64 + minimum: 0 + type: integer original_value: type: string - uri: + pref: + format: int64 + type: + - integer + - "null" + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string - required: - - media_kind - - envelope - type: object - PersonMediaPatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonMediaInputRequest" + source_ref: type: - - array + - string - "null" - supersede: + uri: + type: + - string + - "null" + vcard_altid: + type: + - string + - "null" + vcard_group: + type: + - string + - "null" + vcard_pid: items: - format: int64 - type: integer + type: string type: - array - "null" + vcard_prop_id: + type: + - string + - "null" + vcard_property: + type: + - string + - "null" + required: + - media_kind + - source type: object - PersonName: + OrganizationName: additionalProperties: true properties: - additional_names: - type: string envelope: $ref: "#/components/schemas/ValueEnvelope" - family_name: - type: string - formatted: - type: string - generation: - type: string - given_name: - type: string - honorific_prefixes: - type: string - honorific_suffixes: - type: string - is_derived: - type: boolean - language: + name: type: string name_kind: type: string - original_value: + name_normalized: type: string - person_id: + organization_id: format: int64 type: integer - phonetic_script: - type: string - phonetic_system: - type: string - script: - type: string - secondary_surname: - type: string - sort_as: - type: string required: - envelope - - person_id + - organization_id + - name + - name_normalized - name_kind - - is_derived - - original_value type: object - PersonNameInputRequest: + OrganizationNameBody: additionalProperties: false properties: - additional_names: - type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - family_name: - type: string - formatted: - type: string - generation: - type: string - given_name: - type: string - honorific_prefixes: - type: string - honorific_suffixes: - type: string - is_derived: - type: boolean - language: + name: type: string name_kind: + enum: + - alias + - legal + - former + - abbreviation + - sort type: string - original_value: - type: string - phonetic_script: - type: string - phonetic_system: - type: string - script: - type: string - secondary_surname: - type: string - sort_as: + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 + type: + - integer + - "null" + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string + source_ref: + type: + - string + - "null" + vcard_altid: + type: + - string + - "null" + vcard_group: + type: + - string + - "null" + vcard_pid: + items: + type: string + type: + - array + - "null" + vcard_prop_id: + type: + - string + - "null" + vcard_property: + type: + - string + - "null" required: + - name - name_kind - - envelope + - source type: object - PersonNamePatchRequest: - additionalProperties: false + OrganizationProfile: + additionalProperties: true properties: - add: + addresses: items: - $ref: "#/components/schemas/PersonNameInputRequest" + $ref: "#/components/schemas/OrganizationAddress" type: - array - "null" - supersede: + categories: items: - format: int64 - type: integer + $ref: "#/components/schemas/OrganizationCategory" type: - array - "null" - type: object - PersonProfile: - additionalProperties: true - properties: - display_name: - type: string - id: - format: int64 - type: integer - revision: - format: int64 - type: integer + contact_points: + items: + $ref: "#/components/schemas/OrganizationContactPoint" + type: + - array + - "null" + identifiers: + items: + $ref: "#/components/schemas/OrganizationIdentifier" + type: + - array + - "null" + media: + items: + $ref: "#/components/schemas/OrganizationMedia" + type: + - array + - "null" + names: + items: + $ref: "#/components/schemas/OrganizationName" + type: + - array + - "null" + organization: + $ref: "#/components/schemas/Organization" required: - - id - - revision + - organization + - names + - identifiers + - addresses + - contact_points + - media + - categories type: object - PersonProfileHistory: - additionalProperties: true + OrganizationProfileBody: + additionalProperties: false properties: addresses: items: - $ref: "#/components/schemas/PersonAddress" + $ref: "#/components/schemas/OrganizationAddressBody" type: - array - "null" categories: items: - $ref: "#/components/schemas/PersonCategory" + $ref: "#/components/schemas/OrganizationCategoryBody" type: - array - "null" contact_points: items: - $ref: "#/components/schemas/PersonContactPoint" + $ref: "#/components/schemas/OrganizationContactPointBody" type: - array - "null" - dates: + identifiers: items: - $ref: "#/components/schemas/PersonDate" + $ref: "#/components/schemas/OrganizationIdentifierBody" type: - array - "null" media: items: - $ref: "#/components/schemas/PersonMedia" + $ref: "#/components/schemas/OrganizationMediaBody" type: - array - "null" names: items: - $ref: "#/components/schemas/PersonName" + $ref: "#/components/schemas/OrganizationNameBody" type: - array - "null" - observations: + type: object + OrganizationsResponse: + additionalProperties: true + properties: + limit: + format: int64 + type: integer + offset: + format: int64 + type: integer + organizations: items: - $ref: "#/components/schemas/ParticipantContactObservation" + $ref: "#/components/schemas/Organization" type: - array - "null" - person: - $ref: "#/components/schemas/Person" + total: + format: int64 + type: integer required: - - person - - names - - contact_points - - addresses - - dates - - categories - - media - - observations + - organizations + - total + - limit + - offset type: object - PersonProfilePatchRequest: + PartialDate: additionalProperties: false properties: - addresses: - $ref: "#/components/schemas/PersonAddressPatchRequest" - categories: - $ref: "#/components/schemas/PersonCategoryPatchRequest" - contact_points: - $ref: "#/components/schemas/PersonContactPointPatchRequest" - dates: - $ref: "#/components/schemas/PersonDatePatchRequest" - media: - $ref: "#/components/schemas/PersonMediaPatchRequest" - names: - $ref: "#/components/schemas/PersonNamePatchRequest" + day: + format: int64 + type: integer + month: + format: int64 + type: integer + year: + format: int64 + type: integer type: object - PersonSearchHTTPResponse: + ParticipantContactObservation: additionalProperties: true properties: - cache_revision: + address_kind: type: string - candidate_snapshot_id: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + normalization: type: string - next_cursor: + normalization_version: + format: int64 + type: integer + normalized_value: type: string - rows: - items: - $ref: "#/components/schemas/PersonSummary" - type: - - array - - "null" - search_provenance: - $ref: "#/components/schemas/SearchProvenance" - total_count: + observed_at: + format: date-time + type: string + original_value: + type: string + participant_id: + format: int64 + type: integer + provider_user_id: + type: string + scope_kind: + type: string + scope_value: + type: string + service_slug: + type: string + source_id: format: int64 type: integer required: - - rows - - total_count - - cache_revision - - search_provenance + - envelope + - participant_id + - address_kind + - original_value + - normalized_value + - normalization + - normalization_version type: object - PersonSummary: - additionalProperties: true + PatchAttributeDefinitionRequest: + additionalProperties: false properties: - activity_count: + description: + type: + - string + - "null" + display_order: format: int64 type: integer - cache_revision: - type: string - cluster: - $ref: "#/components/schemas/PersonCluster" - display_label: + is_active: + type: boolean + label: type: string + type: object + PatchPersonRequest: + additionalProperties: false + properties: display_name: + type: + - string + - "null" + required: + - display_name + type: object + PatchSavedViewRequest: + additionalProperties: false + properties: + canonical_state: + $ref: "#/components/schemas/SavedViewStateEnvelope" + description: type: string - file_count: + name: + type: string + schema_version: format: int64 type: integer - first_at: + type: object + Person: + additionalProperties: true + properties: + created_at: format: date-time type: string + display_name: + type: string id: format: int64 type: integer - identifiers: + participant_ids: items: - $ref: "#/components/schemas/PersonIdentifier" + format: int64 + type: integer type: - array - "null" - last_at: + revision: + format: int64 + type: integer + updated_at: format: date-time type: string - partial_label: - type: boolean - profile: - $ref: "#/components/schemas/PersonProfile" - source_counts: - items: - $ref: "#/components/schemas/SourceCount" - type: - - array - - "null" + vcard_uid: + type: string required: - id - - display_label - - partial_label - - identifiers - - activity_count - - file_count - - source_counts - - first_at - - last_at - - cache_revision - type: object - PersonsResponse: - additionalProperties: true - properties: - persons: - items: - $ref: "#/components/schemas/Person" - type: - - array - - "null" - required: - - persons + - vcard_uid + - revision + - participant_ids + - created_at + - updated_at type: object - PingInfo: + PersonAddress: additionalProperties: true properties: - ok: - type: boolean - pid: - format: int64 - type: integer - service: + address_kind: type: string - version: + country_code: type: string - required: - - ok - type: object - Progress: - additionalProperties: true - properties: - done: - format: int64 - type: integer - total: + country_name: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelope" + extended_address: + type: string + extended_components: + type: string + free_text: + type: string + geo_uri: + type: string + label: + type: string + locality: + type: string + original_value: + type: string + person_id: format: int64 type: integer + place_uri: + type: string + post_office_box: + type: string + postal_code: + type: string + region: + type: string + street_address: + type: string + timezone: + type: string required: - - done - - total + - envelope + - person_id + - address_kind + - original_value type: object - QueryRequest: + PersonAddressInputRequest: additionalProperties: false properties: - sql: + address_kind: + type: string + country_code: + type: string + country_name: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + extended_address: + type: string + extended_components: + type: string + free_text: + type: string + geo_uri: + type: string + label: + type: string + locality: + type: string + original_value: + type: string + place_uri: + type: string + post_office_box: + type: string + postal_code: + type: string + region: + type: string + street_address: + type: string + timezone: type: string required: - - sql + - address_kind + - envelope type: object - QueryResult: - additionalProperties: true + PersonAddressPatchRequest: + additionalProperties: false properties: - columns: + add: items: - type: string + $ref: "#/components/schemas/PersonAddressInputRequest" type: - array - "null" - row_count: - format: int64 - type: integer - rows: + supersede: items: - items: {} - type: - - array - - "null" + format: int64 + type: integer type: - array - "null" - required: - - columns - - rows - - row_count type: object - RejectedCandidate: + PersonAttributeGroup: additionalProperties: true properties: - identifier: - type: string - reason: - type: string + current: + items: + $ref: "#/components/schemas/PersonAttributeValue" + type: + - array + - "null" + definition: + $ref: "#/components/schemas/AttributeDefinition" + history: + items: + $ref: "#/components/schemas/PersonAttributeValue" + type: + - array + - "null" required: - - identifier - - reason + - definition + - current type: object - RelationshipRow: + PersonAttributeValue: additionalProperties: true properties: - canonical_id: - format: int64 - type: integer - display_label: + active_from: + format: date-time type: string - last_at: + active_until: format: date-time type: string - member_ids: - items: - format: int64 - type: integer - type: - - array - - "null" - score: + actor: + type: string + confidence: format: double type: number - signals: - $ref: "#/components/schemas/RelationshipSignals" - required: - - canonical_id - - display_label - - member_ids - - score - - signals - - last_at - type: object - RelationshipSignals: - additionalProperties: true - properties: - last_interaction_at: + created_at: format: date-time type: string - meeting_count: + definition_id: format: int64 type: integer - meetings_together: - format: double - type: number - modalities: + definition_slug: + type: string + id: format: int64 type: integer - received_from_them: - format: double - type: number - sent_count: + ordinal: format: int64 type: integer - sent_to_them: - format: double - type: number + person_id: + format: int64 + type: integer + source: + type: string + source_ref: + type: string + superseded_at: + format: date-time + type: string + value: + $ref: "#/components/schemas/AttributeValue" required: - - sent_to_them - - received_from_them - - meetings_together - - sent_count - - meeting_count - - modalities - - last_interaction_at + - id + - person_id + - definition_id + - definition_slug + - ordinal + - value + - active_from + - created_at + - source type: object - RelationshipTimelineHTTPRequest: - additionalProperties: false + PersonAttributeWrite: + additionalProperties: true properties: - cursor: - type: string - filters: + dry_run: + type: boolean + superseded: + $ref: "#/components/schemas/PersonAttributeValue" + value: + $ref: "#/components/schemas/PersonAttributeValue" + required: + - dry_run + type: object + PersonAttributesResponse: + additionalProperties: true + properties: + attributes: items: - $ref: "#/components/schemas/ExploreFilter" + $ref: "#/components/schemas/PersonAttributeGroup" type: - array - "null" - limit: + person_id: format: int64 - maximum: 500 - minimum: 0 type: integer - timezone: - type: string + required: + - person_id + - attributes type: object - RelationshipTimelineHTTPResponse: + PersonCategory: additionalProperties: true properties: - cache_revision: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + normalized_value: type: string - canonical_id: - format: int64 - type: integer - identity_revision: - format: int64 - type: integer - next_cursor: - type: string - rows: - items: - $ref: "#/components/schemas/TimelineRow" - type: - - array - - "null" - total_count: + original_value: + type: string + person_id: format: int64 type: integer required: - - canonical_id - - rows - - total_count - - cache_revision - - identity_revision + - envelope + - person_id + - original_value + - normalized_value type: object - RelationshipsHTTPRequest: + PersonCategoryInputRequest: additionalProperties: false properties: - cursor: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + original_value: type: string - filters: + required: + - original_value + - envelope + type: object + PersonCategoryPatchRequest: + additionalProperties: false + properties: + add: items: - $ref: "#/components/schemas/ExploreFilter" + $ref: "#/components/schemas/PersonCategoryInputRequest" + type: + - array + - "null" + supersede: + items: + format: int64 + type: integer type: - array - "null" - limit: - format: int64 - maximum: 500 - minimum: 0 - type: integer - show_all: - type: boolean type: object - RelationshipsHTTPResponse: + PersonCluster: additionalProperties: true properties: - cache_revision: - type: string - identity_revision: + canonical_id: format: int64 type: integer - next_cursor: - type: string - rows: + edges: items: - $ref: "#/components/schemas/RelationshipRow" + $ref: "#/components/schemas/PersonClusterEdge" + type: + - array + - "null" + member_ids: + items: + format: int64 + type: integer type: - array - "null" - total_count: - format: int64 - type: integer - required: - - rows - - total_count - - cache_revision - - identity_revision - type: object - RemoteImageRequest: - additionalProperties: false - properties: - url: - description: Absolute http(s) URL of the consented remote image - type: string required: - - url + - canonical_id + - member_ids + - edges type: object - RemoveRequest: - additionalProperties: false + PersonClusterEdge: + additionalProperties: true properties: - account: - type: string - identifier: - type: string - source_id: + participant_a: format: int64 type: integer - required: - - identifier - type: object - RemoveResult: - additionalProperties: true - properties: - account: - type: string - cache_state: - enum: - - ready - - stale - type: string - identifier: - type: string - no_identity: - type: boolean - removed: + participant_b: format: int64 type: integer required: - - account - - identifier - - removed + - participant_a + - participant_b type: object - SavedView: + PersonContactPoint: additionalProperties: true properties: - canonical_state: - $ref: "#/components/schemas/SavedViewStateEnvelope" - created_at: - format: date-time + address_kind: type: string - description: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + normalization: type: string - id: + normalization_version: format: int64 type: integer - name: + normalized_value: type: string - revision: - format: int64 - type: integer - schema_version: + original_value: + type: string + person_id: format: int64 type: integer - updated_at: - format: date-time + scope_kind: type: string - required: - - id - - name - - canonical_state - - schema_version - - revision - - created_at - - updated_at - type: object - SavedViewFilter: - additionalProperties: false - properties: - field: + scope_value: type: string - operator: + service_slug: + type: string + uri: type: string - values: - description: Exact filter values; numeric identifiers use decimal strings - items: - type: string - type: array required: - - field - - operator - - values + - envelope + - person_id + - address_kind + - original_value + - normalized_value + - normalization + - normalization_version type: object - SavedViewSort: + PersonContactPointInputRequest: additionalProperties: false properties: - direction: - enum: - - asc - - desc + address_kind: type: string - field: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + original_value: type: string - required: - - field - - direction - type: object - SavedViewStateEnvelope: - additionalProperties: false - properties: - columns: - items: - type: string - type: array - filters: - items: - $ref: "#/components/schemas/SavedViewFilter" - type: array - grouping: - items: - type: string - type: array - inspector_pinned: - type: boolean - presentation: - enum: - - table - - timeline - - files + scope_kind: type: string - query: + scope_value: type: string - search_mode: + service_slug: type: string - sort: - items: - $ref: "#/components/schemas/SavedViewSort" - type: array + uri: + type: string + required: + - address_kind + - original_value + - envelope type: object - SavedViewsResponse: - additionalProperties: true + PersonContactPointPatchRequest: + additionalProperties: false properties: - saved_views: + add: items: - $ref: "#/components/schemas/SavedView" + $ref: "#/components/schemas/PersonContactPointInputRequest" type: - array - "null" - required: - - saved_views - type: object - SchedulerStatusResponse: - additionalProperties: true - properties: - accounts: + supersede: items: - $ref: "#/components/schemas/AccountStatus" + format: int64 + type: integer type: - array - "null" - running: - type: boolean - required: - - running - - accounts type: object - ScoreBreakdown: + PersonContextSummaryHTTPResponse: additionalProperties: true properties: - bm25: - format: double - type: number - rrf: - format: double - type: number - subject_boosted: - type: boolean - vector: - format: double - type: number - type: object - SearchCoverageRequest: - additionalProperties: false - properties: - filters: - items: - $ref: "#/components/schemas/ExploreFilter" - type: - - array - - "null" + cache_revision: + type: string + candidate_snapshot_id: + type: string + search_provenance: + $ref: "#/components/schemas/SearchProvenance" + summary: + $ref: "#/components/schemas/PersonSummary" + required: + - summary + - cache_revision + - search_provenance type: object - SearchCoverageResponse: + PersonDate: additionalProperties: true properties: - actions: - items: - enum: - - retry - - build_index - type: string - type: array - cache_revision: + calendar_scale: type: string - detail: + date: + $ref: "#/components/schemas/PartialDate" + date_kind: type: string - eligible_count: - format: int64 - type: integer - embedded_count: - format: int64 - type: integer - percentage: - format: double - type: number - status: - enum: - - disabled - - initializing - - stale - - incomplete - - unavailable - - ready + date_text: type: string - vector_fingerprint: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + label: type: string - vector_generation: + original_value: + type: string + person_id: format: int64 type: integer required: - - eligible_count - - embedded_count - - percentage - - cache_revision - - status - - actions + - envelope + - person_id + - date_kind + - date + - original_value type: object - SearchFastResponse: - additionalProperties: true + PersonDateInputRequest: + additionalProperties: false properties: - applied_source_ids: + calendar_scale: + type: string + date: + $ref: "#/components/schemas/PartialDate" + date_kind: + type: string + date_text: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + label: + type: string + original_value: + type: string + required: + - date_kind + - envelope + type: object + PersonDatePatchRequest: + additionalProperties: false + properties: + add: items: - format: int64 - type: integer + $ref: "#/components/schemas/PersonDateInputRequest" type: - array - "null" - messages: + supersede: items: - $ref: "#/components/schemas/MessageSummary" + format: int64 + type: integer type: - array - "null" - query: - type: string - stats: - $ref: "#/components/schemas/TotalStatsResponse" - total_count: - format: int64 - type: integer - required: - - query - - messages - - total_count type: object - SearchProvenance: - additionalProperties: false + PersonIdentifier: + additionalProperties: true properties: - lexical_index_revision: + display_value: type: string - vector_generation: + is_primary: + type: boolean + participant_id: format: int64 type: integer + provenance: + type: string + type: + type: string + value: + type: string + required: + - type + - value + - is_primary + - provenance + - participant_id type: object - SearchResult: + PersonMedia: additionalProperties: true properties: - messages: - items: - $ref: "#/components/schemas/MessageSummary" - type: - - array - - "null" - page: - format: int64 - type: integer - page_size: + byte_size: format: int64 type: integer - query: + content_hash: type: string - total: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + has_data: + type: boolean + media_kind: + type: string + media_type: + type: string + original_value: + type: string + person_id: format: int64 type: integer + uri: + type: string required: - - query - - total - - page - - page_size - - messages - type: object - SecretSettingState: - additionalProperties: true - properties: - configured: - type: boolean - required: - - configured + - envelope + - person_id + - media_kind + - has_data + - original_value type: object - SecretSettingUpdate: + PersonMediaInputRequest: additionalProperties: false properties: - action: - enum: - - set - - clear + data: + contentEncoding: base64 type: string - value: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + media_kind: type: string - required: - - action - type: object - SenderCount: - additionalProperties: false - properties: - count: - format: int64 - type: integer - sender: + media_type: + type: string + original_value: + type: string + uri: type: string required: - - sender - - count + - media_kind + - envelope type: object - SessionLoginRequest: + PersonMediaPatchRequest: additionalProperties: false properties: - api_key: - type: string - required: - - api_key + add: + items: + $ref: "#/components/schemas/PersonMediaInputRequest" + type: + - array + - "null" + supersede: + items: + format: int64 + type: integer + type: + - array + - "null" type: object - SessionStatus: + PersonName: additionalProperties: true properties: - auth_mode: - enum: - - loopback - - api_key - - session - - required + additional_names: type: string - csrf_token: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + family_name: type: string - https: - type: boolean - plain_http_warning: + formatted: + type: string + generation: + type: string + given_name: + type: string + honorific_prefixes: + type: string + honorific_suffixes: + type: string + is_derived: type: boolean - required: - - auth_mode - - https - - plain_http_warning - type: object - SetPersonAttributeRequest: - additionalProperties: false - properties: - active_from: - format: date-time + language: type: string - active_until: - format: date-time + name_kind: type: string - actor: + original_value: type: string - confidence: - format: double - type: number - expected_value_id: - format: int64 - type: integer - ordinal: + person_id: format: int64 type: integer - source: - enum: - - user - - carddav_import - - vcard_import - - archive_observation - - extraction - - enrichment - - system + phonetic_script: type: string - source_ref: + phonetic_system: type: string - value: - $ref: "#/components/schemas/AttributeValue" - required: - - value - type: object - Setting: - additionalProperties: true - properties: - group: - enum: - - browser - - server - - archive - - search - - sources - - integrations + script: type: string - key: + secondary_surname: type: string - kind: - enum: - - string - - integer - - number - - boolean - - string_array - - secret + sort_as: type: string - options: - items: - type: string - type: - - array - - "null" - read_only: - type: boolean - restart_required: - type: boolean - secret: - $ref: "#/components/schemas/SecretSettingState" - testable: - type: boolean - value: - $ref: "#/components/schemas/SettingValue" required: - - key - - group - - kind - - restart_required + - envelope + - person_id + - name_kind + - is_derived + - original_value type: object - SettingUpdate: + PersonNameInputRequest: additionalProperties: false properties: - key: + additional_names: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + family_name: + type: string + formatted: + type: string + generation: + type: string + given_name: + type: string + honorific_prefixes: + type: string + honorific_suffixes: + type: string + is_derived: + type: boolean + language: + type: string + name_kind: + type: string + original_value: + type: string + phonetic_script: + type: string + phonetic_system: + type: string + script: + type: string + secondary_surname: + type: string + sort_as: type: string - secret: - $ref: "#/components/schemas/SecretSettingUpdate" - value: - $ref: "#/components/schemas/SettingValue" required: - - key + - name_kind + - envelope type: object - SettingValue: - oneOf: - - additionalProperties: false - properties: - string: - type: string - required: - - string - type: object - - additionalProperties: false - properties: - integer: - format: int64 - type: integer - required: - - integer - type: object - - additionalProperties: false - properties: - number: - format: double - type: number - required: - - number - type: object - - additionalProperties: false - properties: - boolean: - type: boolean - required: - - boolean - type: object - - additionalProperties: false - properties: - strings: - items: - type: string - type: array - required: - - strings - type: object - SettingsPatchRequest: + PersonNamePatchRequest: additionalProperties: false properties: - confirm_api_key_restart: - type: boolean - updates: + add: items: - $ref: "#/components/schemas/SettingUpdate" - minItems: 1 - type: array - required: - - updates + $ref: "#/components/schemas/PersonNameInputRequest" + type: + - array + - "null" + supersede: + items: + format: int64 + type: integer + type: + - array + - "null" type: object - SettingsResponse: + PersonProfile: additionalProperties: true properties: - pending_restart: - type: boolean - settings: - items: - $ref: "#/components/schemas/Setting" - type: array + display_name: + type: string + id: + format: int64 + type: integer + revision: + format: int64 + type: integer required: - - settings - - pending_restart + - id + - revision type: object - SimilarSearchResponse: + PersonProfileHistory: additionalProperties: true properties: - generation: - $ref: "#/components/schemas/HybridGenerationSummary" - messages: + addresses: items: - $ref: "#/components/schemas/MessageSummary" + $ref: "#/components/schemas/PersonAddress" type: - array - "null" - returned: - format: int64 - type: integer - seed_message_id: - format: int64 - type: integer - required: - - seed_message_id - - returned - - generation - - messages + categories: + items: + $ref: "#/components/schemas/PersonCategory" + type: + - array + - "null" + contact_points: + items: + $ref: "#/components/schemas/PersonContactPoint" + type: + - array + - "null" + dates: + items: + $ref: "#/components/schemas/PersonDate" + type: + - array + - "null" + media: + items: + $ref: "#/components/schemas/PersonMedia" + type: + - array + - "null" + names: + items: + $ref: "#/components/schemas/PersonName" + type: + - array + - "null" + observations: + items: + $ref: "#/components/schemas/ParticipantContactObservation" + type: + - array + - "null" + person: + $ref: "#/components/schemas/Person" + required: + - person + - names + - contact_points + - addresses + - dates + - categories + - media + - observations type: object - Source: + PersonProfilePatchRequest: additionalProperties: false properties: - account_email: - format: email - type: string - display_name: - maxLength: 256 - type: string - identifier: - maxLength: 128 - type: string - required: - - identifier - - account_email + addresses: + $ref: "#/components/schemas/PersonAddressPatchRequest" + categories: + $ref: "#/components/schemas/PersonCategoryPatchRequest" + contact_points: + $ref: "#/components/schemas/PersonContactPointPatchRequest" + dates: + $ref: "#/components/schemas/PersonDatePatchRequest" + media: + $ref: "#/components/schemas/PersonMediaPatchRequest" + names: + $ref: "#/components/schemas/PersonNamePatchRequest" type: object - SourceCount: + PersonSearchHTTPResponse: additionalProperties: true properties: - count: - format: int64 - type: integer - source_type: + cache_revision: type: string - required: - - source_type - - count - type: object - SourceIdentitiesResponse: - additionalProperties: true - properties: - account: + candidate_snapshot_id: type: string - identities: + next_cursor: + type: string + rows: items: - $ref: "#/components/schemas/SourceIdentityResponse" - type: array - source_id: + $ref: "#/components/schemas/PersonSummary" + type: + - array + - "null" + search_provenance: + $ref: "#/components/schemas/SearchProvenance" + total_count: format: int64 type: integer required: - - source_id - - account - - identities + - rows + - total_count + - cache_revision + - search_provenance type: object - SourceIdentityResponse: + PersonSummary: additionalProperties: true properties: - confirmed_at: - format: date-time + activity_count: + format: int64 + type: integer + cache_revision: type: string - identifier: + cluster: + $ref: "#/components/schemas/PersonCluster" + display_label: type: string - signals: - items: - type: string - type: array - required: - - identifier - - signals - - confirmed_at - type: object - SourceStatus: - additionalProperties: true - properties: - active_sync: - oneOf: - - $ref: "#/components/schemas/SyncRunStatus" - - type: "null" - can_sync: - type: boolean display_name: - type: - - string - - "null" - id: + type: string + file_count: format: int64 type: integer - identifier: + first_at: + format: date-time type: string - last_successful_sync: - oneOf: - - $ref: "#/components/schemas/SyncRunStatus" - - type: "null" - last_sync_at: - type: - - string - - "null" - latest_sync: - oneOf: - - $ref: "#/components/schemas/SyncRunStatus" - - type: "null" - next_sync_at: + id: + format: int64 + type: integer + identifiers: + items: + $ref: "#/components/schemas/PersonIdentifier" type: - - string + - array - "null" - schedule: + last_at: + format: date-time type: string - scheduled: + partial_label: type: boolean - scheduler_last_error: - type: string - source_type: - type: string - sync_unavailable_reason: - type: string - updated_at: - type: string - required: - - id - - source_type - - identifier - - display_name - - last_sync_at - - updated_at - - active_sync - - latest_sync - - last_successful_sync - - can_sync - - scheduled - - next_sync_at - type: object - SourceStatusResponse: - additionalProperties: true - properties: - sources: + profile: + $ref: "#/components/schemas/PersonProfile" + source_counts: items: - $ref: "#/components/schemas/SourceStatus" + $ref: "#/components/schemas/SourceCount" type: - array - "null" required: - - sources + - id + - display_label + - partial_label + - identifiers + - activity_count + - file_count + - source_counts + - first_at + - last_at + - cache_revision type: object - SourcesRequest: - additionalProperties: false + PersonsResponse: + additionalProperties: true properties: - accounts: + persons: items: - type: string + $ref: "#/components/schemas/Person" type: - array - "null" required: - - accounts + - persons type: object - StageDeletionFilter: - additionalProperties: false + PingInfo: + additionalProperties: true properties: - after: - type: string - before: - type: string - domain: - type: string - label: - type: string - recipient: - type: string - recipient_name: - type: string - sender: - type: string - sender_name: - type: string - source_id: + ok: + type: boolean + pid: format: int64 type: integer - type: object - StageDeletionRequest: + service: + type: string + version: + type: string + required: + - ok + type: object + Progress: + additionalProperties: true + properties: + done: + format: int64 + type: integer + total: + format: int64 + type: integer + required: + - done + - total + type: object + QueryRequest: additionalProperties: false properties: - description: + sql: type: string - dry_run: - type: boolean - filter: - $ref: "#/components/schemas/StageDeletionFilter" - message_ids: + required: + - sql + type: object + QueryResult: + additionalProperties: true + properties: + columns: items: - format: int64 - type: integer + type: string type: - array - "null" - operation_token: - type: string - selection: - $ref: "#/components/schemas/ExploreSelection" + row_count: + format: int64 + type: integer + rows: + items: + items: {} + type: + - array + - "null" + type: + - array + - "null" + required: + - columns + - rows + - row_count type: object - StageDeletionResponse: + RefreshOrganizationDuplicateSuggestionsResponse: additionalProperties: true properties: - account: + created: + format: int64 + type: integer + required: + - created + type: object + RejectedCandidate: + additionalProperties: true + properties: + identifier: type: string - dry_run: - type: boolean - id: + reason: type: string - message_count: + required: + - identifier + - reason + type: object + RelationshipRow: + additionalProperties: true + properties: + canonical_id: format: int64 type: integer - sample_gmail_ids: + display_label: + type: string + last_at: + format: date-time + type: string + member_ids: items: - type: string + format: int64 + type: integer type: - array - "null" - status: - type: string + score: + format: double + type: number + signals: + $ref: "#/components/schemas/RelationshipSignals" required: - - dry_run - - message_count + - canonical_id + - display_label + - member_ids + - score + - signals + - last_at type: object - StatsResponse: + RelationshipSignals: additionalProperties: true properties: - active_messages: - format: int64 - type: integer - database_size_bytes: - format: int64 - type: integer - source_deleted_messages: - format: int64 - type: integer - total_accounts: - format: int64 - type: integer - total_attachments: - format: int64 - type: integer - total_labels: + last_interaction_at: + format: date-time + type: string + meeting_count: format: int64 type: integer - total_messages: + meetings_together: + format: double + type: number + modalities: format: int64 type: integer - total_threads: + received_from_them: + format: double + type: number + sent_count: format: int64 type: integer - vector_search: - $ref: "#/components/schemas/StatsView" - vector_status: - type: string + sent_to_them: + format: double + type: number required: - - total_messages - - active_messages - - source_deleted_messages - - total_threads - - total_accounts - - total_labels - - total_attachments - - database_size_bytes + - sent_to_them + - received_from_them + - meetings_together + - sent_count + - meeting_count + - modalities + - last_interaction_at type: object - StatsView: - additionalProperties: true + RelationshipTimelineHTTPRequest: + additionalProperties: false properties: - active_generation: - $ref: "#/components/schemas/GenerationSummary" - building_generation: - $ref: "#/components/schemas/BuildingSummary" - enabled: - type: boolean - missing_embeddings_total: + cursor: + type: string + filters: + items: + $ref: "#/components/schemas/ExploreFilter" + type: + - array + - "null" + limit: format: int64 + maximum: 500 + minimum: 0 type: integer - required: - - enabled - - active_generation - - missing_embeddings_total + timezone: + type: string type: object - StatusMessageResponse: + RelationshipTimelineHTTPResponse: additionalProperties: true properties: - message: + cache_revision: type: string - status: + canonical_id: + format: int64 + type: integer + identity_revision: + format: int64 + type: integer + next_cursor: type: string - required: - - status - - message - type: object - StructuredPersonProfile: - additionalProperties: true - properties: - addresses: + rows: items: - $ref: "#/components/schemas/PersonAddress" + $ref: "#/components/schemas/TimelineRow" type: - array - "null" - categories: + total_count: + format: int64 + type: integer + required: + - canonical_id + - rows + - total_count + - cache_revision + - identity_revision + type: object + RelationshipsHTTPRequest: + additionalProperties: false + properties: + cursor: + type: string + filters: items: - $ref: "#/components/schemas/PersonCategory" + $ref: "#/components/schemas/ExploreFilter" type: - array - "null" - contact_points: + limit: + format: int64 + maximum: 500 + minimum: 0 + type: integer + show_all: + type: boolean + type: object + RelationshipsHTTPResponse: + additionalProperties: true + properties: + cache_revision: + type: string + identity_revision: + format: int64 + type: integer + next_cursor: + type: string + rows: items: - $ref: "#/components/schemas/PersonContactPoint" + $ref: "#/components/schemas/RelationshipRow" type: - array - "null" - dates: - items: - $ref: "#/components/schemas/PersonDate" - type: - - array - - "null" - media: - items: - $ref: "#/components/schemas/PersonMedia" - type: - - array - - "null" - names: - items: - $ref: "#/components/schemas/PersonName" - type: - - array - - "null" - person: - $ref: "#/components/schemas/Person" + total_count: + format: int64 + type: integer required: - - person - - names - - contact_points - - addresses - - dates - - categories - - media + - rows + - total_count + - cache_revision + - identity_revision type: object - Summary: + RemoteImageRequest: additionalProperties: false properties: - accounts: - items: - type: string - type: - - array - - "null" - date_range: - items: - type: string - maxItems: 2 - minItems: 2 - type: - - array - - "null" - message_count: - format: int64 - type: integer - top_senders: - items: - $ref: "#/components/schemas/SenderCount" - type: - - array - - "null" - total_size_bytes: + url: + description: Absolute http(s) URL of the consented remote image + type: string + required: + - url + type: object + RemoveRequest: + additionalProperties: false + properties: + account: + type: string + identifier: + type: string + source_id: format: int64 type: integer required: - - message_count - - total_size_bytes - - date_range - - accounts - - top_senders + - identifier type: object - SyncRunItemStatus: + RemoveResult: additionalProperties: true properties: - created_at: - type: string - error_kind: - type: string - error_message: + account: type: string - phase: + cache_state: + enum: + - ready + - stale type: string - source_message_id: + identifier: type: string + no_identity: + type: boolean + removed: + format: int64 + type: integer required: - - source_message_id - - phase - - error_kind - - error_message - - created_at + - account + - identifier + - removed type: object - SyncRunStatus: - additionalProperties: true + ResolveOrganizationDuplicateSuggestionBody: + additionalProperties: false properties: - completed_at: - type: - - string - - "null" - cursor_after: + note: type: - string - "null" - cursor_before: - type: - - string - - "null" - error_message: - type: - - string - - "null" - errors_count: - format: int64 - type: integer - id: - format: int64 - type: integer - item_errors: - items: - $ref: "#/components/schemas/SyncRunItemStatus" - type: - - array - - "null" - messages_added: - format: int64 - type: integer - messages_processed: - format: int64 - type: integer - messages_updated: - format: int64 - type: integer - skipped_count: - format: int64 - type: integer - source_id: - format: int64 - type: integer - started_at: - type: string status: + enum: + - accepted + - rejected type: string required: - - id - - source_id - - started_at - - completed_at - status - - messages_processed - - messages_added - - messages_updated - - errors_count - - error_message - - cursor_before - - cursor_after type: object - TaskIntegrationStatusResponse: + SavedView: additionalProperties: true properties: - message: - description: Credential-free status explanation + canonical_state: + $ref: "#/components/schemas/SavedViewStateEnvelope" + created_at: + format: date-time type: string - project: - description: Configured project when available + description: type: string - security_note: - description: Platform-specific local socket security limitation + id: + format: int64 + type: integer + name: type: string - state: - description: Current task integration state - enum: - - disabled - - not_found - - authentication_required - - unreachable - - incompatible - - wrong_project - - ready + revision: + format: int64 + type: integer + schema_version: + format: int64 + type: integer + updated_at: + format: date-time type: string required: - - state - - message + - id + - name + - canonical_state + - schema_version + - revision + - created_at + - updated_at type: object - TaskLinkLookupResponse: - additionalProperties: true + SavedViewFilter: + additionalProperties: false properties: - complete: - type: boolean - last_scan: - format: date-time - type: string - outbound_metadata: - $ref: "#/components/schemas/TaskLinkOutboundMetadata" - reason: - type: string - remote_revision: + field: type: string - state: + operator: type: string - tasks: + values: + description: Exact filter values; numeric identifiers use decimal strings items: - $ref: "#/components/schemas/TaskSummary" + type: string type: array required: - - tasks - - outbound_metadata - - state - - complete - - last_scan + - field + - operator + - values type: object - TaskLinkMutationRequest: + SavedViewSort: additionalProperties: false properties: - added_at: - description: Browser-generated retry-stable RFC3339 timestamp for the outbound mail link + direction: + enum: + - asc + - desc type: string - description: + field: type: string - labels: + required: + - field + - direction + type: object + SavedViewStateEnvelope: + additionalProperties: false + properties: + columns: items: type: string - type: - - array - - "null" - priority: + type: array + filters: + items: + $ref: "#/components/schemas/SavedViewFilter" + type: array + grouping: + items: + type: string + type: array + inspector_pinned: + type: boolean + presentation: + enum: + - table + - timeline + - files type: string - task_id: - description: Existing task to link; omit to create a task + query: type: string - title: + search_mode: type: string - required: - - added_at + sort: + items: + $ref: "#/components/schemas/SavedViewSort" + type: array type: object - TaskLinkMutationResponse: + SavedViewsResponse: additionalProperties: true properties: - task: - $ref: "#/components/schemas/TaskLinkTask" + saved_views: + items: + $ref: "#/components/schemas/SavedView" + type: + - array + - "null" required: - - task + - saved_views type: object - TaskLinkOutboundMetadata: + SchedulerStatusResponse: additionalProperties: true properties: - archive_uid: - type: string - conversation_id: - format: int64 - type: integer - from: - type: string - message_id: - format: int64 - type: integer - sent_at: - type: string - source_identifier: - type: string - source_message_id: - type: string - source_type: - type: string - subject: - type: string + accounts: + items: + $ref: "#/components/schemas/AccountStatus" + type: + - array + - "null" + running: + type: boolean required: - - archive_uid - - message_id - - conversation_id - - subject - - from - - sent_at - - source_type - - source_identifier - - source_message_id + - running + - accounts type: object - TaskLinkTask: + ScoreBreakdown: additionalProperties: true properties: - id: - type: string - project: - type: string - revision: - type: string - title: - type: string - required: - - id - - project - - title + bm25: + format: double + type: number + rrf: + format: double + type: number + subject_boosted: + type: boolean + vector: + format: double + type: number type: object - TaskSearchResponse: - additionalProperties: true + SearchCoverageRequest: + additionalProperties: false properties: - tasks: + filters: items: - $ref: "#/components/schemas/TaskSummary" + $ref: "#/components/schemas/ExploreFilter" type: - array - "null" - required: - - tasks - type: object - TaskSummary: - additionalProperties: true - properties: - id: - type: string - revision: - type: string - title: - type: string - required: - - id - - title type: object - TextConversationRow: + SearchCoverageResponse: additionalProperties: true properties: - conversation_id: - format: int64 - type: integer - last_message_at: + actions: + items: + enum: + - retry + - build_index + type: string + type: array + cache_revision: type: string - last_preview: + detail: type: string - message_count: + eligible_count: format: int64 type: integer - participant_count: + embedded_count: format: int64 type: integer - source_type: + percentage: + format: double + type: number + status: + enum: + - disabled + - initializing + - stale + - incomplete + - unavailable + - ready type: string - title: + vector_fingerprint: type: string + vector_generation: + format: int64 + type: integer required: - - conversation_id - - title - - source_type - - message_count - - participant_count - - last_preview + - eligible_count + - embedded_count + - percentage + - cache_revision + - status + - actions type: object - TextConversationsResponse: + SearchFastResponse: additionalProperties: true properties: - conversations: + applied_source_ids: items: - $ref: "#/components/schemas/TextConversationRow" + format: int64 + type: integer type: - array - "null" - count: - format: int64 - type: integer - has_more: - type: boolean - limit: - format: int64 - type: integer - offset: + messages: + items: + $ref: "#/components/schemas/MessageSummary" + type: + - array + - "null" + query: + type: string + stats: + $ref: "#/components/schemas/TotalStatsResponse" + total_count: format: int64 type: integer required: - - count - - has_more - - offset - - limit - - conversations + - query + - messages + - total_count type: object - TextMessagesResponse: - additionalProperties: true + SearchProvenance: + additionalProperties: false properties: - count: - format: int64 - type: integer - has_more: - type: boolean - limit: + lexical_index_revision: + type: string + vector_generation: format: int64 type: integer + type: object + SearchResult: + additionalProperties: true + properties: messages: items: - $ref: "#/components/schemas/CLIQueryMessageSummary" + $ref: "#/components/schemas/MessageSummary" type: - array - "null" - offset: + page: + format: int64 + type: integer + page_size: + format: int64 + type: integer + query: + type: string + total: format: int64 type: integer required: - - count - - has_more - - offset - - limit + - query + - total + - page + - page_size - messages type: object - TimelineRow: + SecretSettingState: additionalProperties: true properties: - anchor_message_id: - format: int64 - type: integer - conversation_id: - format: int64 - type: integer - first_at: - format: date-time - type: string - has_attachments: + configured: type: boolean - key: - type: string - kind: - type: string - message_count: - format: int64 - type: integer - occurred_at: - format: date-time - type: string - preview: - type: string - source_id: - format: int64 - type: integer - title: - type: string required: - - key - - kind - - title - - preview - - occurred_at - - message_count - - source_id - - has_attachments + - configured type: object - TokenUploadRequest: + SecretSettingUpdate: additionalProperties: false properties: - access_token: - type: string - client_id: - type: string - expiry: - format: date-time - type: string - refresh_token: - type: string - scopes: - items: - type: string - type: - - array - - "null" - tenant_id: + action: + enum: + - set + - clear type: string - token_type: + value: type: string required: - - refresh_token + - action type: object - TotalStatsResponse: - additionalProperties: true + SenderCount: + additionalProperties: false properties: - account_count: - format: int64 - type: integer - active_messages: - format: int64 - type: integer - applied_search_scope: - type: boolean - applied_source_ids: - items: - format: int64 - type: integer - type: - - array - - "null" - attachment_count: - format: int64 - type: integer - attachment_size: - format: int64 - type: integer - label_count: - format: int64 - type: integer - message_count: - format: int64 - type: integer - source_deleted_messages: - format: int64 - type: integer - total_size: + count: format: int64 type: integer - required: - - message_count - - active_messages - - source_deleted_messages - - total_size - - attachment_count - - attachment_size - - label_count - - account_count - type: object - TranscriptSegment: - additionalProperties: false - properties: - offset_seconds: - format: double - minimum: 0 - type: number - speaker: - type: string - text: + sender: type: string required: - - speaker - - text + - sender + - count type: object - UpdateRequest: + SessionLoginRequest: additionalProperties: false properties: - display_name: - type: string - email: + api_key: type: string required: - - email - - display_name + - api_key type: object - UpdateResult: + SessionStatus: additionalProperties: true properties: - display_name: + auth_mode: + enum: + - loopback + - api_key + - session + - required type: string - email: + csrf_token: type: string + https: + type: boolean + plain_http_warning: + type: boolean required: - - email - - display_name + - auth_mode + - https + - plain_http_warning type: object - VCardIdentity: + SetOrganizationAttributeBody: additionalProperties: false properties: - altid: - type: string - group: - type: string - pid: - items: - type: string + actor: type: - - array + - string - "null" - prop_id: - type: string - property: - type: string - type: object - ValueEnvelope: - additionalProperties: true - properties: - active_from: - format: date-time - type: string - active_until: - format: date-time - type: string confidence: format: double - type: number - created_at: - format: date-time + type: + - number + - "null" + definition_slug: type: string - id: + dry_run: + type: boolean + expected_value_id: format: int64 - type: integer + type: + - integer + - "null" ordinal: format: int64 - type: integer - pref: - format: int64 - type: integer + type: + - integer + - "null" source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string source_ref: - type: string - superseded_at: - format: date-time - type: string - type_label: - type: string - type_tokens: - items: - type: string type: - - array + - string - "null" - updated_at: - format: date-time - type: string - vcard: - $ref: "#/components/schemas/VCardIdentity" + value: + $ref: "#/components/schemas/AttributeValue" required: - - id - - ordinal - - vcard + - definition_slug + - value - source - - created_at - - updated_at type: object - ValueEnvelopeInput: + SetPersonAttributeRequest: additionalProperties: false properties: active_from: @@ -6239,101 +6378,2003 @@ components: active_until: format: date-time type: string + actor: + type: string confidence: format: double type: number - ordinal: + expected_value_id: format: int64 - minimum: 0 type: integer - pref: + ordinal: format: int64 type: integer source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string source_ref: type: string - type_label: - type: string - type_tokens: - items: - type: string - type: - - array - - "null" - vcard: - $ref: "#/components/schemas/VCardIdentity" + value: + $ref: "#/components/schemas/AttributeValue" required: - - source + - value type: object - VectorHealth: + Setting: additionalProperties: true properties: - error: + group: + enum: + - browser + - server + - archive + - search + - sources + - integrations type: string - status: + key: + type: string + kind: + enum: + - string + - integer + - number + - boolean + - string_array + - secret type: string + options: + items: + type: string + type: + - array + - "null" + read_only: + type: boolean + restart_required: + type: boolean + secret: + $ref: "#/components/schemas/SecretSettingState" + testable: + type: boolean + value: + $ref: "#/components/schemas/SettingValue" required: - - status + - key + - group + - kind + - restart_required type: object - securitySchemes: - apiKey: - in: header - name: X-Api-Key - type: apiKey -info: - title: msgvault API - version: 1.38.0 -openapi: 3.1.0 -paths: - /api/ping: - get: - operationId: daemonPing + SettingUpdate: + additionalProperties: false + properties: + key: + type: string + secret: + $ref: "#/components/schemas/SecretSettingUpdate" + value: + $ref: "#/components/schemas/SettingValue" + required: + - key + type: object + SettingValue: + oneOf: + - additionalProperties: false + properties: + string: + type: string + required: + - string + type: object + - additionalProperties: false + properties: + integer: + format: int64 + type: integer + required: + - integer + type: object + - additionalProperties: false + properties: + number: + format: double + type: number + required: + - number + type: object + - additionalProperties: false + properties: + boolean: + type: boolean + required: + - boolean + type: object + - additionalProperties: false + properties: + strings: + items: + type: string + type: array + required: + - strings + type: object + SettingsPatchRequest: + additionalProperties: false + properties: + confirm_api_key_restart: + type: boolean + updates: + items: + $ref: "#/components/schemas/SettingUpdate" + minItems: 1 + type: array + required: + - updates + type: object + SettingsResponse: + additionalProperties: true + properties: + pending_restart: + type: boolean + settings: + items: + $ref: "#/components/schemas/Setting" + type: array + required: + - settings + - pending_restart + type: object + SimilarSearchResponse: + additionalProperties: true + properties: + generation: + $ref: "#/components/schemas/HybridGenerationSummary" + messages: + items: + $ref: "#/components/schemas/MessageSummary" + type: + - array + - "null" + returned: + format: int64 + type: integer + seed_message_id: + format: int64 + type: integer + required: + - seed_message_id + - returned + - generation + - messages + type: object + Source: + additionalProperties: false + properties: + account_email: + format: email + type: string + display_name: + maxLength: 256 + type: string + identifier: + maxLength: 128 + type: string + required: + - identifier + - account_email + type: object + SourceCount: + additionalProperties: true + properties: + count: + format: int64 + type: integer + source_type: + type: string + required: + - source_type + - count + type: object + SourceIdentitiesResponse: + additionalProperties: true + properties: + account: + type: string + identities: + items: + $ref: "#/components/schemas/SourceIdentityResponse" + type: array + source_id: + format: int64 + type: integer + required: + - source_id + - account + - identities + type: object + SourceIdentityResponse: + additionalProperties: true + properties: + confirmed_at: + format: date-time + type: string + identifier: + type: string + signals: + items: + type: string + type: array + required: + - identifier + - signals + - confirmed_at + type: object + SourceStatus: + additionalProperties: true + properties: + active_sync: + oneOf: + - $ref: "#/components/schemas/SyncRunStatus" + - type: "null" + can_sync: + type: boolean + display_name: + type: + - string + - "null" + id: + format: int64 + type: integer + identifier: + type: string + last_successful_sync: + oneOf: + - $ref: "#/components/schemas/SyncRunStatus" + - type: "null" + last_sync_at: + type: + - string + - "null" + latest_sync: + oneOf: + - $ref: "#/components/schemas/SyncRunStatus" + - type: "null" + next_sync_at: + type: + - string + - "null" + schedule: + type: string + scheduled: + type: boolean + scheduler_last_error: + type: string + source_type: + type: string + sync_unavailable_reason: + type: string + updated_at: + type: string + required: + - id + - source_type + - identifier + - display_name + - last_sync_at + - updated_at + - active_sync + - latest_sync + - last_successful_sync + - can_sync + - scheduled + - next_sync_at + type: object + SourceStatusResponse: + additionalProperties: true + properties: + sources: + items: + $ref: "#/components/schemas/SourceStatus" + type: + - array + - "null" + required: + - sources + type: object + SourcesRequest: + additionalProperties: false + properties: + accounts: + items: + type: string + type: + - array + - "null" + required: + - accounts + type: object + StageDeletionFilter: + additionalProperties: false + properties: + after: + type: string + before: + type: string + domain: + type: string + label: + type: string + recipient: + type: string + recipient_name: + type: string + sender: + type: string + sender_name: + type: string + source_id: + format: int64 + type: integer + type: object + StageDeletionRequest: + additionalProperties: false + properties: + description: + type: string + dry_run: + type: boolean + filter: + $ref: "#/components/schemas/StageDeletionFilter" + message_ids: + items: + format: int64 + type: integer + type: + - array + - "null" + operation_token: + type: string + selection: + $ref: "#/components/schemas/ExploreSelection" + type: object + StageDeletionResponse: + additionalProperties: true + properties: + account: + type: string + dry_run: + type: boolean + id: + type: string + message_count: + format: int64 + type: integer + sample_gmail_ids: + items: + type: string + type: + - array + - "null" + status: + type: string + required: + - dry_run + - message_count + type: object + StatsResponse: + additionalProperties: true + properties: + active_messages: + format: int64 + type: integer + database_size_bytes: + format: int64 + type: integer + source_deleted_messages: + format: int64 + type: integer + total_accounts: + format: int64 + type: integer + total_attachments: + format: int64 + type: integer + total_labels: + format: int64 + type: integer + total_messages: + format: int64 + type: integer + total_threads: + format: int64 + type: integer + vector_search: + $ref: "#/components/schemas/StatsView" + vector_status: + type: string + required: + - total_messages + - active_messages + - source_deleted_messages + - total_threads + - total_accounts + - total_labels + - total_attachments + - database_size_bytes + type: object + StatsView: + additionalProperties: true + properties: + active_generation: + $ref: "#/components/schemas/GenerationSummary" + building_generation: + $ref: "#/components/schemas/BuildingSummary" + enabled: + type: boolean + missing_embeddings_total: + format: int64 + type: integer + required: + - enabled + - active_generation + - missing_embeddings_total + type: object + StatusMessageResponse: + additionalProperties: true + properties: + message: + type: string + status: + type: string + required: + - status + - message + type: object + StructuredPersonProfile: + additionalProperties: true + properties: + addresses: + items: + $ref: "#/components/schemas/PersonAddress" + type: + - array + - "null" + categories: + items: + $ref: "#/components/schemas/PersonCategory" + type: + - array + - "null" + contact_points: + items: + $ref: "#/components/schemas/PersonContactPoint" + type: + - array + - "null" + dates: + items: + $ref: "#/components/schemas/PersonDate" + type: + - array + - "null" + media: + items: + $ref: "#/components/schemas/PersonMedia" + type: + - array + - "null" + names: + items: + $ref: "#/components/schemas/PersonName" + type: + - array + - "null" + person: + $ref: "#/components/schemas/Person" + required: + - person + - names + - contact_points + - addresses + - dates + - categories + - media + type: object + Summary: + additionalProperties: false + properties: + accounts: + items: + type: string + type: + - array + - "null" + date_range: + items: + type: string + maxItems: 2 + minItems: 2 + type: + - array + - "null" + message_count: + format: int64 + type: integer + top_senders: + items: + $ref: "#/components/schemas/SenderCount" + type: + - array + - "null" + total_size_bytes: + format: int64 + type: integer + required: + - message_count + - total_size_bytes + - date_range + - accounts + - top_senders + type: object + SyncRunItemStatus: + additionalProperties: true + properties: + created_at: + type: string + error_kind: + type: string + error_message: + type: string + phase: + type: string + source_message_id: + type: string + required: + - source_message_id + - phase + - error_kind + - error_message + - created_at + type: object + SyncRunStatus: + additionalProperties: true + properties: + completed_at: + type: + - string + - "null" + cursor_after: + type: + - string + - "null" + cursor_before: + type: + - string + - "null" + error_message: + type: + - string + - "null" + errors_count: + format: int64 + type: integer + id: + format: int64 + type: integer + item_errors: + items: + $ref: "#/components/schemas/SyncRunItemStatus" + type: + - array + - "null" + messages_added: + format: int64 + type: integer + messages_processed: + format: int64 + type: integer + messages_updated: + format: int64 + type: integer + skipped_count: + format: int64 + type: integer + source_id: + format: int64 + type: integer + started_at: + type: string + status: + type: string + required: + - id + - source_id + - started_at + - completed_at + - status + - messages_processed + - messages_added + - messages_updated + - errors_count + - error_message + - cursor_before + - cursor_after + type: object + TaskIntegrationStatusResponse: + additionalProperties: true + properties: + message: + description: Credential-free status explanation + type: string + project: + description: Configured project when available + type: string + security_note: + description: Platform-specific local socket security limitation + type: string + state: + description: Current task integration state + enum: + - disabled + - not_found + - authentication_required + - unreachable + - incompatible + - wrong_project + - ready + type: string + required: + - state + - message + type: object + TaskLinkLookupResponse: + additionalProperties: true + properties: + complete: + type: boolean + last_scan: + format: date-time + type: string + outbound_metadata: + $ref: "#/components/schemas/TaskLinkOutboundMetadata" + reason: + type: string + remote_revision: + type: string + state: + type: string + tasks: + items: + $ref: "#/components/schemas/TaskSummary" + type: array + required: + - tasks + - outbound_metadata + - state + - complete + - last_scan + type: object + TaskLinkMutationRequest: + additionalProperties: false + properties: + added_at: + description: Browser-generated retry-stable RFC3339 timestamp for the outbound mail link + type: string + description: + type: string + labels: + items: + type: string + type: + - array + - "null" + priority: + type: string + task_id: + description: Existing task to link; omit to create a task + type: string + title: + type: string + required: + - added_at + type: object + TaskLinkMutationResponse: + additionalProperties: true + properties: + task: + $ref: "#/components/schemas/TaskLinkTask" + required: + - task + type: object + TaskLinkOutboundMetadata: + additionalProperties: true + properties: + archive_uid: + type: string + conversation_id: + format: int64 + type: integer + from: + type: string + message_id: + format: int64 + type: integer + sent_at: + type: string + source_identifier: + type: string + source_message_id: + type: string + source_type: + type: string + subject: + type: string + required: + - archive_uid + - message_id + - conversation_id + - subject + - from + - sent_at + - source_type + - source_identifier + - source_message_id + type: object + TaskLinkTask: + additionalProperties: true + properties: + id: + type: string + project: + type: string + revision: + type: string + title: + type: string + required: + - id + - project + - title + type: object + TaskSearchResponse: + additionalProperties: true + properties: + tasks: + items: + $ref: "#/components/schemas/TaskSummary" + type: + - array + - "null" + required: + - tasks + type: object + TaskSummary: + additionalProperties: true + properties: + id: + type: string + revision: + type: string + title: + type: string + required: + - id + - title + type: object + TextConversationRow: + additionalProperties: true + properties: + conversation_id: + format: int64 + type: integer + last_message_at: + type: string + last_preview: + type: string + message_count: + format: int64 + type: integer + participant_count: + format: int64 + type: integer + source_type: + type: string + title: + type: string + required: + - conversation_id + - title + - source_type + - message_count + - participant_count + - last_preview + type: object + TextConversationsResponse: + additionalProperties: true + properties: + conversations: + items: + $ref: "#/components/schemas/TextConversationRow" + type: + - array + - "null" + count: + format: int64 + type: integer + has_more: + type: boolean + limit: + format: int64 + type: integer + offset: + format: int64 + type: integer + required: + - count + - has_more + - offset + - limit + - conversations + type: object + TextMessagesResponse: + additionalProperties: true + properties: + count: + format: int64 + type: integer + has_more: + type: boolean + limit: + format: int64 + type: integer + messages: + items: + $ref: "#/components/schemas/CLIQueryMessageSummary" + type: + - array + - "null" + offset: + format: int64 + type: integer + required: + - count + - has_more + - offset + - limit + - messages + type: object + TimelineRow: + additionalProperties: true + properties: + anchor_message_id: + format: int64 + type: integer + conversation_id: + format: int64 + type: integer + first_at: + format: date-time + type: string + has_attachments: + type: boolean + key: + type: string + kind: + type: string + message_count: + format: int64 + type: integer + occurred_at: + format: date-time + type: string + preview: + type: string + source_id: + format: int64 + type: integer + title: + type: string + required: + - key + - kind + - title + - preview + - occurred_at + - message_count + - source_id + - has_attachments + type: object + TokenUploadRequest: + additionalProperties: false + properties: + access_token: + type: string + client_id: + type: string + expiry: + format: date-time + type: string + refresh_token: + type: string + scopes: + items: + type: string + type: + - array + - "null" + tenant_id: + type: string + token_type: + type: string + required: + - refresh_token + type: object + TotalStatsResponse: + additionalProperties: true + properties: + account_count: + format: int64 + type: integer + active_messages: + format: int64 + type: integer + applied_search_scope: + type: boolean + applied_source_ids: + items: + format: int64 + type: integer + type: + - array + - "null" + attachment_count: + format: int64 + type: integer + attachment_size: + format: int64 + type: integer + label_count: + format: int64 + type: integer + message_count: + format: int64 + type: integer + source_deleted_messages: + format: int64 + type: integer + total_size: + format: int64 + type: integer + required: + - message_count + - active_messages + - source_deleted_messages + - total_size + - attachment_count + - attachment_size + - label_count + - account_count + type: object + TranscriptSegment: + additionalProperties: false + properties: + offset_seconds: + format: double + minimum: 0 + type: number + speaker: + type: string + text: + type: string + required: + - speaker + - text + type: object + UpdateRequest: + additionalProperties: false + properties: + display_name: + type: string + email: + type: string + required: + - email + - display_name + type: object + UpdateResult: + additionalProperties: true + properties: + display_name: + type: string + email: + type: string + required: + - email + - display_name + type: object + VCardIdentity: + additionalProperties: false + properties: + altid: + type: string + group: + type: string + pid: + items: + type: string + type: + - array + - "null" + prop_id: + type: string + property: + type: string + type: object + ValueEnvelope: + additionalProperties: true + properties: + active_from: + format: date-time + type: string + active_until: + format: date-time + type: string + confidence: + format: double + type: number + created_at: + format: date-time + type: string + id: + format: int64 + type: integer + ordinal: + format: int64 + type: integer + pref: + format: int64 + type: integer + source: + type: string + source_ref: + type: string + superseded_at: + format: date-time + type: string + type_label: + type: string + type_tokens: + items: + type: string + type: + - array + - "null" + updated_at: + format: date-time + type: string + vcard: + $ref: "#/components/schemas/VCardIdentity" + required: + - id + - ordinal + - vcard + - source + - created_at + - updated_at + type: object + ValueEnvelopeInput: + additionalProperties: false + properties: + active_from: + format: date-time + type: string + active_until: + format: date-time + type: string + confidence: + format: double + type: number + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 + type: integer + source: + type: string + source_ref: + type: string + type_label: + type: string + type_tokens: + items: + type: string + type: + - array + - "null" + vcard: + $ref: "#/components/schemas/VCardIdentity" + required: + - source + type: object + VectorHealth: + additionalProperties: true + properties: + error: + type: string + status: + type: string + required: + - status + type: object + securitySchemes: + apiKey: + in: header + name: X-Api-Key + type: apiKey +info: + title: msgvault API + version: 1.39.0 +openapi: 3.1.0 +paths: + /api/ping: + get: + operationId: daemonPing + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PingInfo" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Daemon discovery ping + tags: + - Daemon + /api/session: + delete: + operationId: logoutSession + responses: + "204": + description: No Content + "429": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Delete the current browser session + tags: + - Session + get: + operationId: getSession + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionStatus" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Get browser authentication status + tags: + - Session + /api/session/login: + post: + operationId: loginSession + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionLoginRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionStatus" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Create an in-memory browser session + tags: + - Session + /api/v1/accounts: + get: + operationId: listAccounts + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AccountListResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List scheduler-configured accounts (with sync schedules); use /cli/accounts for all archived sources + tags: + - API + post: + operationId: addAccount + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddAccountRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/StatusMessageResponse" + description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/StatusMessageResponse" + description: Created + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Add an account + tags: + - API + /api/v1/aggregates: + get: + operationId: getAggregates + parameters: + - description: Aggregate view type + in: query + name: view_type + schema: + type: string + - description: "Sort field: count, size, attachment_size, or name" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string + - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) + in: query + name: limit + schema: + format: int64 + type: integer + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Search query + in: query + name: search_query + schema: + type: string + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AggregateResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get aggregate rows + tags: + - API + /api/v1/aggregates/sub: + get: + operationId: getSubAggregates + parameters: + - description: Aggregate view type + in: query + name: view_type + required: true + schema: + type: string + - description: "Sort field: count, size, attachment_size, or name" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string + - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) + in: query + name: limit + schema: + format: int64 + type: integer + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Search query + in: query + name: search_query + schema: + type: string + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + - description: Sender email/address filter + in: query + name: sender + schema: + type: string + - description: Sender display-name filter + in: query + name: sender_name + schema: + type: string + - description: Recipient email/address filter + in: query + name: recipient + schema: + type: string + - description: Recipient display-name filter + in: query + name: recipient_name + schema: + type: string + - description: Domain filter + in: query + name: domain + schema: + type: string + - description: Label filter + in: query + name: label + schema: + type: string + - description: Message type filter + in: query + name: message_type + schema: + type: string + - description: Named time period + in: query + name: time_period + schema: + type: string + - description: Conversation ID + in: query + name: conversation_id + schema: + format: int64 + type: integer + - description: Comma-separated aggregate view names to match empty values + in: query + name: empty_targets + schema: + type: string + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AggregateResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get nested aggregate rows + tags: + - API + /api/v1/attachments/{hash}/content: + get: + operationId: getAttachmentContent + parameters: + - description: Attachment SHA-256 content hash + in: path + name: hash + required: true + schema: + type: string + responses: + "200": + content: + "*/*": + schema: + contentMediaType: application/octet-stream + format: binary + type: string + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Download attachment content by SHA-256 hash + tags: + - API + /api/v1/attachments/{id}: + get: + operationId: getAttachment + parameters: + - description: Attachment ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttachmentInfo" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get attachment metadata + tags: + - API + /api/v1/attribute-definitions: + get: + operationId: listAttributeDefinitions + parameters: + - description: Filter by object type + in: query + name: object_type + schema: + type: string + - description: Include deactivated definitions + in: query + name: include_hidden + schema: + type: boolean + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinitionsResponse" + description: OK + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List portable attribute definitions + tags: + - API + post: + operationId: createAttributeDefinition + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateAttributeDefinitionRequest" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinition" + description: Created + headers: + ETag: + description: Strong attribute definition revision tag + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Create a user attribute definition + tags: + - API + /api/v1/attribute-definitions/{id}: + delete: + operationId: deleteAttributeDefinition + parameters: + - description: Attribute definition ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest definition read + in: header + name: If-Match + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Delete a user attribute definition + tags: + - API + get: + operationId: getAttributeDefinition + parameters: + - description: Attribute definition ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinition" + description: OK + headers: + ETag: + description: Strong attribute definition revision tag + schema: + type: string + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get one attribute definition + tags: + - API + patch: + operationId: patchAttributeDefinition + parameters: + - description: Attribute definition ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest definition read + in: header + name: If-Match + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PatchAttributeDefinitionRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinition" + description: OK + headers: + ETag: + description: Strong attribute definition revision tag + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Update mutable attribute definition fields + tags: + - API + /api/v1/auth/token/{email}: + post: + operationId: uploadToken + parameters: + - description: Account email address + in: path + name: email + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TokenUploadRequest" + required: true responses: - "200": + "201": content: application/json: schema: - $ref: "#/components/schemas/PingInfo" - description: OK + $ref: "#/components/schemas/StatusMessageResponse" + description: Created default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Daemon discovery ping + security: + - apiKey: [] + summary: Upload an OAuth token tags: - - Daemon - /api/session: - delete: - operationId: logoutSession + - API + /api/v1/backup/freeze/begin: + post: + operationId: beginBackupFreeze responses: - "204": - description: No Content - "429": + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BackupFreezeBeginResponse" + description: OK + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error + security: + - apiKey: [] + summary: Begin a backup freeze window + tags: + - API + /api/v1/backup/freeze/end: + post: + operationId: endBackupFreeze + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/BackupFreezeEndRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BackupFreezeEndResponse" + description: OK default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Delete the current browser session + security: + - apiKey: [] + summary: End a backup freeze window tags: - - Session + - API + /api/v1/cli/account: + post: + operationId: updateCLIAccount + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateResult" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable + security: + - apiKey: [] + summary: Update an account for CLI use + tags: + - CLI + /api/v1/cli/accounts: get: - operationId: getSession + operationId: listCLIAccounts responses: "200": content: application/json: schema: - $ref: "#/components/schemas/SessionStatus" + $ref: "#/components/schemas/CliAccountsResponse" description: OK default: content: @@ -6341,24 +8382,162 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Get browser authentication status + security: + - apiKey: [] + summary: List accounts for CLI output tags: - - Session - /api/session/login: + - API + /api/v1/cli/add-calendar/plan: post: - operationId: loginSession + operationId: planCLIAddCalendar requestBody: content: application/json: schema: - $ref: "#/components/schemas/SessionLoginRequest" + $ref: "#/components/schemas/CLIAddCalendarPlanRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/SessionStatus" + $ref: "#/components/schemas/CLIAddCalendarPlanResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Plan CLI Calendar account setup + tags: + - API + /api/v1/cli/attachment: + get: + operationId: getCLIAttachment + parameters: + - description: Attachment SHA-256 content hash + in: query + name: content_hash + required: true + schema: + type: string + responses: + "200": + content: + application/octet-stream: + schema: + contentMediaType: application/octet-stream + format: binary + type: string + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get one attachment for CLI export + tags: + - API + /api/v1/cli/build-cache: + post: + operationId: buildCLICache + parameters: + - description: Rebuild all cache files from scratch + in: query + name: full_rebuild + schema: + type: boolean + responses: + "200": + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/CLICacheBuildEvent" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Build the CLI analytics cache + tags: + - API + /api/v1/cli/cache-stats: + get: + operationId: getCLICacheStats + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CacheStats" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get CLI-compatible analytics cache statistics + tags: + - API + /api/v1/cli/collection: + get: + operationId: getCLICollection + parameters: + - description: Collection name + in: query + name: name + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CliCollectionEnvelope" description: OK default: content: @@ -6366,18 +8545,20 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Create an in-memory browser session + security: + - apiKey: [] + summary: Get one collection for CLI output tags: - - Session - /api/v1/accounts: + - API + /api/v1/cli/collections: get: - operationId: listAccounts + operationId: listCLICollections responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AccountListResponse" + $ref: "#/components/schemas/CliCollectionsResponse" description: OK default: content: @@ -6387,483 +8568,440 @@ paths: description: Error security: - apiKey: [] - summary: List scheduler-configured accounts (with sync schedules); use /cli/accounts for all archived sources + summary: List collections for CLI output tags: - API post: - operationId: addAccount + operationId: createCLICollection requestBody: content: application/json: schema: - $ref: "#/components/schemas/AddAccountRequest" + $ref: "#/components/schemas/CreateRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/StatusMessageResponse" + $ref: "#/components/schemas/MutationResult" description: OK - "201": + "400": content: application/json: schema: - $ref: "#/components/schemas/StatusMessageResponse" - description: Created - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - security: - - apiKey: [] - summary: Add an account - tags: - - API - /api/v1/aggregates: - get: - operationId: getAggregates - parameters: - - description: Aggregate view type - in: query - name: view_type - schema: - type: string - - description: "Sort field: count, size, attachment_size, or name" - in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction - schema: - type: string - - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) - in: query - name: limit - schema: - format: int64 - type: integer - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Source ID - in: query - name: source_id - schema: - format: int64 - type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages - in: query - name: hide_deleted - schema: - type: boolean - - description: Search query - in: query - name: search_query - schema: - type: string - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - responses: - "200": + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": content: application/json: schema: - $ref: "#/components/schemas/AggregateResponse" - description: OK - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get aggregate rows + summary: Create a collection for CLI use tags: - - API - /api/v1/aggregates/sub: - get: - operationId: getSubAggregates + - CLI + /api/v1/cli/collections/{name}: + delete: + operationId: deleteCLICollection parameters: - - description: Aggregate view type - in: query - name: view_type + - description: Collection name + in: path + name: name required: true schema: + description: Collection name type: string - - description: "Sort field: count, size, attachment_size, or name" - in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction - schema: - type: string - - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) - in: query - name: limit - schema: - format: int64 - type: integer - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Source ID - in: query - name: source_id - schema: - format: int64 - type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages - in: query - name: hide_deleted - schema: - type: boolean - - description: Search query - in: query - name: search_query - schema: - type: string - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - - description: Sender email/address filter - in: query - name: sender - schema: - type: string - - description: Sender display-name filter - in: query - name: sender_name - schema: - type: string - - description: Recipient email/address filter - in: query - name: recipient - schema: - type: string - - description: Recipient display-name filter - in: query - name: recipient_name - schema: - type: string - - description: Domain filter - in: query - name: domain - schema: - type: string - - description: Label filter - in: query - name: label - schema: - type: string - - description: Message type filter - in: query - name: message_type - schema: - type: string - - description: Named time period - in: query - name: time_period - schema: - type: string - - description: Conversation ID - in: query - name: conversation_id - schema: - format: int64 - type: integer - - description: Comma-separated aggregate view names to match empty values - in: query - name: empty_targets - schema: - type: string - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AggregateResponse" + $ref: "#/components/schemas/MutationResult" description: OK - default: + "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get nested aggregate rows + summary: Delete a CLI collection tags: - - API - /api/v1/attachments/{hash}/content: - get: - operationId: getAttachmentContent + - CLI + /api/v1/cli/collections/{name}/sources: + delete: + operationId: removeCLICollectionSources parameters: - - description: Attachment SHA-256 content hash + - description: Collection name in: path - name: hash + name: name required: true schema: + description: Collection name type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SourcesRequest" + required: true responses: "200": content: - "*/*": + application/json: schema: - contentMediaType: application/octet-stream - format: binary - type: string + $ref: "#/components/schemas/MutationResult" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "401": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "404": + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Download attachment content by SHA-256 hash + summary: Remove accounts from a CLI collection tags: - - API - /api/v1/attachments/{id}: - get: - operationId: getAttachment + - CLI + patch: + operationId: addCLICollectionSources parameters: - - description: Attachment ID + - description: Collection name in: path - name: id + name: name required: true schema: - format: int64 - type: integer + description: Collection name + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SourcesRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttachmentInfo" + $ref: "#/components/schemas/MutationResult" description: OK - default: + "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get attachment metadata + summary: Add accounts to a CLI collection tags: - - API - /api/v1/attribute-definitions: - get: - operationId: listAttributeDefinitions - parameters: - - description: Filter by object type - in: query - name: object_type - schema: - type: string - - description: Include deactivated definitions - in: query - name: include_hidden - schema: - type: boolean + - CLI + /api/v1/cli/deduplicate/plan: + post: + operationId: planCLIDeduplicate + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIDeduplicatePlanRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinitionsResponse" + $ref: "#/components/schemas/CLIDeduplicatePlanResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable + security: + - apiKey: [] + summary: Plan interactive deduplication for CLI use + tags: + - CLI + /api/v1/cli/delete-deduped: + post: + operationId: executeCLIDeleteDeduped + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CliDeleteDedupedExecuteRequest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/CliDeleteDedupedExecuteResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Conflict + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: List portable attribute definitions + summary: Delete dedup-hidden messages for CLI use tags: - - API + - CLI + /api/v1/cli/delete-deduped/plan: post: - operationId: createAttributeDefinition + operationId: planCLIDeleteDeduped requestBody: content: application/json: schema: - $ref: "#/components/schemas/CreateAttributeDefinitionRequest" + $ref: "#/components/schemas/CliDeleteDedupedPlanRequest" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinition" - description: Created - headers: - ETag: - description: Strong attribute definition revision tag - schema: - type: string + $ref: "#/components/schemas/CliDeleteDedupedPlanResponse" + description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Create a user attribute definition + summary: Plan deletion of dedup-hidden messages for CLI use tags: - - API - /api/v1/attribute-definitions/{id}: - delete: - operationId: deleteAttributeDefinition - parameters: - - description: Attribute definition ID - in: path - name: id - required: true - schema: - format: int64 - type: integer - - description: Strong ETag returned by the latest definition read - in: header - name: If-Match - required: true - schema: - type: string + - CLI + /api/v1/cli/delete-staged/plan: + post: + operationId: planCLIDeleteStaged + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIDeleteStagedPlanRequest" + required: true responses: - "204": - description: No Content - "400": + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "404": + $ref: "#/components/schemas/CLIDeleteStagedPlanResponse" + description: OK + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + security: + - apiKey: [] + summary: Plan CLI staged deletion execution + tags: + - API + /api/v1/cli/deletion-manifests: + post: + operationId: createCLIDeletionManifest + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Manifest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "428": + $ref: "#/components/schemas/CLIDeletionManifestResponse" + description: OK + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: Create a staged deletion manifest + tags: + - API + /api/v1/cli/embeddings/plan: + post: + operationId: planCLIEmbeddings + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIEmbeddingsPlanRequest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/CLIEmbeddingsPlanResponse" + description: OK default: content: application/json: @@ -6872,199 +9010,181 @@ paths: description: Error security: - apiKey: [] - summary: Delete a user attribute definition + summary: Plan CLI embeddings management tags: - API - get: - operationId: getAttributeDefinition - parameters: - - description: Attribute definition ID - in: path - name: id - required: true - schema: - format: int64 - type: integer + /api/v1/cli/identities: + delete: + operationId: removeCLIIdentity + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemoveRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinition" + $ref: "#/components/schemas/RemoveResult" description: OK - headers: - ETag: - description: Strong attribute definition revision tag + "400": + content: + application/json: schema: - type: string - "404": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get one attribute definition + summary: Remove a confirmed identifier from an account identity tags: - - API - patch: - operationId: patchAttributeDefinition + - CLI + get: + operationId: listCLIIdentities parameters: - - description: Attribute definition ID - in: path - name: id - required: true + - description: Restrict to a single account + explode: false + in: query + name: account + schema: + description: Restrict to a single account + type: string + - description: Restrict to all member accounts of one collection + explode: false + in: query + name: collection + schema: + description: Restrict to all member accounts of one collection + type: string + - description: Restrict to one source by numeric ID + explode: false + in: query + name: source_id schema: + description: Restrict to one source by numeric ID format: int64 type: integer - - description: Strong ETag returned by the latest definition read - in: header - name: If-Match - required: true + - description: For account scope, return only the primary source instead of related sources + explode: false + in: query + name: primary_only schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/PatchAttributeDefinitionRequest" - required: true + description: For account scope, return only the primary source instead of related sources + type: boolean responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinition" + $ref: "#/components/schemas/CliIdentitiesResponse" description: OK - headers: - ETag: - description: Strong attribute definition revision tag - schema: - type: string "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "428": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Update mutable attribute definition fields + summary: List confirmed account identities tags: - - API - /api/v1/auth/token/{email}: + - CLI post: - operationId: uploadToken - parameters: - - description: Account email address - in: path - name: email - required: true - schema: - type: string + operationId: addCLIIdentity requestBody: content: application/json: schema: - $ref: "#/components/schemas/TokenUploadRequest" + $ref: "#/components/schemas/AddRequest" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/StatusMessageResponse" - description: Created - default: + $ref: "#/components/schemas/AddResult" + description: OK + "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - security: - - apiKey: [] - summary: Upload an OAuth token - tags: - - API - /api/v1/backup/freeze/begin: - post: - operationId: beginBackupFreeze - responses: - "200": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/BackupFreezeBeginResponse" - description: OK - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Begin a backup freeze window + summary: Add a confirmed identifier to an account identity tags: - - API - /api/v1/backup/freeze/end: + - CLI + /api/v1/cli/identities/discover: post: - operationId: endBackupFreeze + operationId: discoverCLIIdentities requestBody: content: application/json: schema: - $ref: "#/components/schemas/BackupFreezeEndRequest" + $ref: "#/components/schemas/DiscoverRequest" required: true responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/BackupFreezeEndResponse" + $ref: "#/components/schemas/DiscoverEvent" description: OK default: content: @@ -7074,24 +9194,24 @@ paths: description: Error security: - apiKey: [] - summary: End a backup freeze window + summary: Discover source-scoped account identities tags: - API - /api/v1/cli/account: + /api/v1/cli/identities/import: post: - operationId: updateCLIAccount + operationId: importCLIIdentities requestBody: content: application/json: schema: - $ref: "#/components/schemas/UpdateRequest" + $ref: "#/components/schemas/ImportRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/UpdateResult" + $ref: "#/components/schemas/ImportResult" description: OK "400": content: @@ -7099,12 +9219,6 @@ paths: schema: $ref: "#/components/schemas/ApiHTTPError" description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found "422": content: application/json: @@ -7125,18 +9239,18 @@ paths: description: Service Unavailable security: - apiKey: [] - summary: Update an account for CLI use + summary: Preview or apply parsed source-scoped identities tags: - CLI - /api/v1/cli/accounts: - get: - operationId: listCLIAccounts + /api/v1/cli/init-db: + post: + operationId: initCLIArchive responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliAccountsResponse" + $ref: "#/components/schemas/CliInitDBResponse" description: OK default: content: @@ -7146,24 +9260,25 @@ paths: description: Error security: - apiKey: [] - summary: List accounts for CLI output + summary: Initialize the archive for CLI use tags: - API - /api/v1/cli/add-calendar/plan: - post: - operationId: planCLIAddCalendar - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CLIAddCalendarPlanRequest" - required: true + /api/v1/cli/message: + get: + operationId: getCLIMessage + parameters: + - description: Message numeric ID or source message ID + in: query + name: id + required: true + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIAddCalendarPlanResponse" + $ref: "#/components/schemas/CliMessageResponse" description: OK default: content: @@ -7173,23 +9288,23 @@ paths: description: Error security: - apiKey: [] - summary: Plan CLI Calendar account setup + summary: Get one message for CLI output tags: - API - /api/v1/cli/attachment: + /api/v1/cli/message/raw: get: - operationId: getCLIAttachment + operationId: getCLIMessageRaw parameters: - - description: Attachment SHA-256 content hash + - description: Message numeric ID or source message ID in: query - name: content_hash + name: id required: true schema: type: string responses: "200": content: - application/octet-stream: + message/rfc822: schema: contentMediaType: application/octet-stream format: binary @@ -7233,24 +9348,18 @@ paths: description: Error security: - apiKey: [] - summary: Get one attachment for CLI export + summary: Get one raw message for CLI export tags: - API - /api/v1/cli/build-cache: + /api/v1/cli/rebuild-fts: post: - operationId: buildCLICache - parameters: - - description: Rebuild all cache files from scratch - in: query - name: full_rebuild - schema: - type: boolean + operationId: rebuildCLIFTS responses: "200": content: application/x-ndjson: schema: - $ref: "#/components/schemas/CLICacheBuildEvent" + $ref: "#/components/schemas/CliRebuildFTSEvent" description: OK default: content: @@ -7260,18 +9369,45 @@ paths: description: Error security: - apiKey: [] - summary: Build the CLI analytics cache + summary: Rebuild the CLI full-text search index tags: - API - /api/v1/cli/cache-stats: - get: - operationId: getCLICacheStats + /api/v1/cli/repair-encoding: + post: + operationId: repairEncodingCLI responses: "200": + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/CLIRepairEncodingEvent" + description: OK + default: content: application/json: schema: - $ref: "#/components/schemas/CacheStats" + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Repair CLI archive encoding + tags: + - API + /api/v1/cli/run: + post: + operationId: runCLI + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIRunRequest" + required: true + responses: + "200": + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/CLIRunEvent" description: OK default: content: @@ -7281,25 +9417,84 @@ paths: description: Error security: - apiKey: [] - summary: Get CLI-compatible analytics cache statistics + summary: Run an allowlisted CLI command tags: - API - /api/v1/cli/collection: + /api/v1/cli/search: get: - operationId: getCLICollection + operationId: searchCLI parameters: - - description: Collection name + - description: Search query in: query - name: name + name: q required: true schema: type: string + - description: Maximum number of rows to return + in: query + name: limit + schema: + format: int64 + type: integer + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + - description: Message type filter; repeat or comma-separate for multiple values + in: query + name: message_type + schema: + type: string + - description: Restrict to one account/source + in: query + name: account + schema: + type: string + - description: Restrict to one collection + in: query + name: collection + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CliSearchResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Search messages for CLI output + tags: + - API + /api/v1/cli/stats: + get: + operationId: getCLIStats + parameters: + - description: Restrict to one account/source + in: query + name: account + schema: + type: string + - description: Restrict to one collection + in: query + name: collection + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliCollectionEnvelope" + $ref: "#/components/schemas/CliStatsResponse" description: OK default: content: @@ -7309,18 +9504,38 @@ paths: description: Error security: - apiKey: [] - summary: Get one collection for CLI output + summary: Get CLI-compatible archive statistics tags: - API - /api/v1/cli/collections: - get: - operationId: listCLICollections + /api/v1/cli/sync: + post: + operationId: syncCLI + parameters: + - description: Account email or display name to sync + in: query + name: email + schema: + type: string + - description: IMAP folder names to include (repeatable) + in: query + name: folder + schema: + items: + type: string + type: array + - description: IMAP folder names to exclude (repeatable) + in: query + name: skip-folder + schema: + items: + type: string + type: array responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/CliCollectionsResponse" + $ref: "#/components/schemas/CLISyncEvent" description: OK default: content: @@ -7330,385 +9545,338 @@ paths: description: Error security: - apiKey: [] - summary: List collections for CLI output + summary: Run CLI incremental sync tags: - API + /api/v1/cli/sync-full: post: - operationId: createCLICollection - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateRequest" - required: true - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/MutationResult" - description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable - security: - - apiKey: [] - summary: Create a collection for CLI use - tags: - - CLI - /api/v1/cli/collections/{name}: - delete: - operationId: deleteCLICollection + operationId: syncFullCLI parameters: - - description: Collection name - in: path - name: name - required: true + - description: Account email or display name to sync + in: query + name: email + schema: + type: string + - description: Gmail search query + in: query + name: query + schema: + type: string + - description: Only messages on or after this YYYY-MM-DD date + in: query + name: after + schema: + type: string + - description: Only messages before this YYYY-MM-DD date + in: query + name: before schema: - description: Collection name type: string + - description: Maximum messages to sync + in: query + name: limit + schema: + format: int64 + type: integer + - description: Ignore checkpoints and start fresh + in: query + name: noresume + schema: + type: boolean + - description: IMAP folder names to include (repeatable) + in: query + name: folder + schema: + items: + type: string + type: array + - description: IMAP folder names to exclude (repeatable) + in: query + name: skip-folder + schema: + items: + type: string + type: array responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/MutationResult" + $ref: "#/components/schemas/CLISyncEvent" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Delete a CLI collection + summary: Run CLI full sync tags: - - CLI - /api/v1/cli/collections/{name}/sources: - delete: - operationId: removeCLICollectionSources + - API + /api/v1/cli/verify: + post: + operationId: verifyCLI parameters: - - description: Collection name - in: path - name: name + - description: Account email to verify + in: query + name: email required: true schema: - description: Collection name type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/SourcesRequest" - required: true + - description: Number of messages to sample for MIME verification + in: query + name: sample + schema: + format: int64 + type: integer + - description: Skip SQLite integrity check + in: query + name: skip_db_check + schema: + type: boolean + - description: Emit JSON summary output + in: query + name: json + schema: + type: boolean responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/MutationResult" + $ref: "#/components/schemas/CLIVerifyEvent" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Remove accounts from a CLI collection + summary: Verify the CLI archive against Gmail tags: - - CLI - patch: - operationId: addCLICollectionSources + - API + /api/v1/communication-services: + get: + description: Lists the small open service catalog without pagination, including aliases and normalization policy. + operationId: listCommunicationServices parameters: - - description: Collection name - in: path - name: name - required: true + - description: Include inactive catalog entries + in: query + name: include_inactive schema: - description: Collection name - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/SourcesRequest" - required: true + default: false + type: boolean responses: "200": content: application/json: schema: - $ref: "#/components/schemas/MutationResult" + $ref: "#/components/schemas/CommunicationServicesResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Add accounts to a CLI collection + summary: List communication services tags: - - CLI - /api/v1/cli/deduplicate/plan: + - API post: - operationId: planCLIDeduplicate + description: Registers an unknown or custom service without a schema migration. Re-registering a slug is idempotent. + operationId: createCommunicationService requestBody: content: application/json: schema: - $ref: "#/components/schemas/CLIDeduplicatePlanRequest" + $ref: "#/components/schemas/CreateCommunicationServiceRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIDeduplicatePlanResponse" + $ref: "#/components/schemas/CommunicationService" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error + $ref: "#/components/schemas/ErrorResponse" + description: Error "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Plan interactive deduplication for CLI use + summary: Register a communication service tags: - - CLI - /api/v1/cli/delete-deduped: + - API + /api/v1/content/remote-image: post: - operationId: executeCLIDeleteDeduped + operationId: getRemoteImage requestBody: content: application/json: schema: - $ref: "#/components/schemas/CliDeleteDedupedExecuteRequest" + $ref: "#/components/schemas/RemoteImageRequest" required: true responses: "200": content: - application/json: + image/*: schema: - $ref: "#/components/schemas/CliDeleteDedupedExecuteResponse" + contentMediaType: application/octet-stream + format: binary + type: string description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "409": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Conflict - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "415": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "502": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Delete dedup-hidden messages for CLI use + summary: Fetch a consented remote mail image through the SSRF-hardened daemon proxy tags: - - CLI - /api/v1/cli/delete-deduped/plan: - post: - operationId: planCLIDeleteDeduped - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CliDeleteDedupedPlanRequest" - required: true + - API + /api/v1/conversations/{id}: + get: + operationId: getConversation + parameters: + - description: Conversation ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Selected message ID anchoring the chronological window + in: query + name: anchor + required: true + schema: + format: int64 + type: integer + - description: Messages before the anchor (default 25, max 50) + in: query + name: before + schema: + format: int64 + type: integer + - description: Messages after the anchor (default 25, max 50) + in: query + name: after + schema: + format: int64 + type: integer + - description: Lower UTC bound, inclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) + in: query + name: start + schema: + type: string + - description: Upper UTC bound, exclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) + in: query + name: end + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliDeleteDedupedPlanResponse" + $ref: "#/components/schemas/ConversationResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Plan deletion of dedup-hidden messages for CLI use + summary: Get a bounded containing conversation tags: - - CLI - /api/v1/cli/delete-staged/plan: - post: - operationId: planCLIDeleteStaged - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CLIDeleteStagedPlanRequest" - required: true + - API + /api/v1/deletions: + get: + operationId: listDeletions + parameters: + - description: Filter manifests by status (pending, in_progress, completed, failed, cancelled) + in: query + name: status + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIDeleteStagedPlanResponse" + $ref: "#/components/schemas/ListDeletionsResponse" description: OK default: content: @@ -7718,24 +9886,57 @@ paths: description: Error security: - apiKey: [] - summary: Plan CLI staged deletion execution + summary: List staged deletion manifests tags: - API - /api/v1/cli/deletion-manifests: post: - operationId: createCLIDeletionManifest + operationId: stageDeletion requestBody: content: application/json: schema: - $ref: "#/components/schemas/Manifest" + $ref: "#/components/schemas/StageDeletionRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIDeletionManifestResponse" + $ref: "#/components/schemas/StageDeletionResponse" + description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/StageDeletionResponse" + description: Created + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Stage messages for deletion + tags: + - API + /api/v1/deletions/{id}: + delete: + operationId: cancelDeletion + parameters: + - description: Deletion manifest ID + in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CancelDeletionResponse" description: OK default: content: @@ -7745,24 +9946,24 @@ paths: description: Error security: - apiKey: [] - summary: Create a staged deletion manifest + summary: Cancel a staged deletion manifest tags: - API - /api/v1/cli/embeddings/plan: - post: - operationId: planCLIEmbeddings - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CLIEmbeddingsPlanRequest" - required: true + get: + operationId: getDeletion + parameters: + - description: Deletion manifest ID + in: path + name: id + required: true + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIEmbeddingsPlanResponse" + $ref: "#/components/schemas/DeletionManifestDetail" description: OK default: content: @@ -7772,182 +9973,181 @@ paths: description: Error security: - apiKey: [] - summary: Plan CLI embeddings management + summary: Inspect a staged deletion manifest tags: - API - /api/v1/cli/identities: - delete: - operationId: removeCLIIdentity + /api/v1/domains/search: + post: + operationId: searchDomains requestBody: content: application/json: schema: - $ref: "#/components/schemas/RemoveRequest" + $ref: "#/components/schemas/IdentitySearchHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/RemoveResult" + $ref: "#/components/schemas/DomainSearchHTTPResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Remove a confirmed identifier from an account identity + summary: Search analytical domains tags: - - CLI + - Exploration + /api/v1/domains/{domain}: get: - operationId: listCLIIdentities + operationId: getDomain parameters: - - description: Restrict to a single account - explode: false - in: query - name: account - schema: - description: Restrict to a single account - type: string - - description: Restrict to all member accounts of one collection - explode: false - in: query - name: collection + - description: Exact normalized domain fact + in: path + name: domain + required: true schema: - description: Restrict to all member accounts of one collection type: string - - description: Restrict to one source by numeric ID - explode: false - in: query - name: source_id - schema: - description: Restrict to one source by numeric ID - format: int64 - type: integer - - description: For account scope, return only the primary source instead of related sources - explode: false - in: query - name: primary_only - schema: - description: For account scope, return only the primary source instead of related sources - type: boolean responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliIdentitiesResponse" + $ref: "#/components/schemas/DomainSummary" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: List confirmed account identities + summary: Get one analytical domain tags: - - CLI + - API + /api/v1/domains/{domain}/files/search: post: - operationId: addCLIIdentity + operationId: searchDomainFiles + parameters: + - description: Exact normalized domain fact + in: path + name: domain + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/AddRequest" + $ref: "#/components/schemas/FileSearchHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AddResult" + $ref: "#/components/schemas/FileSearchHTTPResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Add a confirmed identifier to an account identity + summary: Search one domain's analytical files tags: - - CLI - /api/v1/cli/identities/discover: + - Exploration + /api/v1/domains/{domain}/summary: post: - operationId: discoverCLIIdentities + operationId: getDomainContextSummary + parameters: + - description: Exact normalized domain fact + in: path + name: domain + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/DiscoverRequest" + $ref: "#/components/schemas/ExploreHTTPRequest" required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/DiscoverEvent" + $ref: "#/components/schemas/DomainContextSummaryHTTPResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -7956,92 +10156,106 @@ paths: description: Error security: - apiKey: [] - summary: Discover source-scoped account identities + summary: Get one domain's contextual analytical summary tags: - - API - /api/v1/cli/identities/import: + - Exploration + /api/v1/domains/{domain}/timeline: post: - operationId: importCLIIdentities + operationId: getDomainTimeline + parameters: + - description: Exact normalized domain fact + in: path + name: domain + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/ImportRequest" + $ref: "#/components/schemas/ExploreHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ImportResult" + $ref: "#/components/schemas/ExploreHTTPResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Preview or apply parsed source-scoped identities + summary: Get one domain's canonical activity timeline tags: - - CLI - /api/v1/cli/init-db: + - Exploration + /api/v1/employments: post: - operationId: initCLIArchive + operationId: createEmployment + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EmploymentBody" + required: true responses: - "200": + "201": content: application/json: schema: - $ref: "#/components/schemas/CliInitDBResponse" - description: OK - default: + $ref: "#/components/schemas/Employment" + description: Created + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + Location: + description: Canonical URL of the created employment record + schema: + type: string + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Initialize the archive for CLI use - tags: - - API - /api/v1/cli/message: - get: - operationId: getCLIMessage - parameters: - - description: Message numeric ID or source message ID - in: query - name: id - required: true - schema: - type: string - responses: - "200": + "409": content: application/json: schema: - $ref: "#/components/schemas/CliMessageResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8050,47 +10264,48 @@ paths: description: Error security: - apiKey: [] - summary: Get one message for CLI output + summary: Create an employment record tags: - API - /api/v1/cli/message/raw: - get: - operationId: getCLIMessageRaw + /api/v1/employments/{id}: + delete: + operationId: deleteEmployment parameters: - - description: Message numeric ID or source message ID - in: query + - description: Employment ID + in: path name: id required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true schema: type: string responses: - "200": - content: - message/rfc822: - schema: - contentMediaType: application/octet-stream - format: binary - type: string - description: OK + "204": + description: No Content "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + "428": content: application/json: schema: @@ -8110,40 +10325,49 @@ paths: description: Error security: - apiKey: [] - summary: Get one raw message for CLI export + summary: Delete an employment record tags: - API - /api/v1/cli/rebuild-fts: - post: - operationId: rebuildCLIFTS + get: + operationId: getEmployment + parameters: + - description: Employment ID + in: path + name: id + required: true + schema: + format: int64 + type: integer responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CliRebuildFTSEvent" + $ref: "#/components/schemas/Employment" description: OK - default: + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Rebuild the CLI full-text search index - tags: - - API - /api/v1/cli/repair-encoding: - post: - operationId: repairEncodingCLI - responses: - "200": + "404": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLIRepairEncodingEvent" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8152,25 +10376,73 @@ paths: description: Error security: - apiKey: [] - summary: Repair CLI archive encoding + summary: Get an employment record tags: - API - /api/v1/cli/run: - post: - operationId: runCLI + patch: + operationId: patchEmployment + parameters: + - description: Employment ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/CLIRunRequest" + $ref: "#/components/schemas/EmploymentBody" required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLIRunEvent" + $ref: "#/components/schemas/Employment" description: OK + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8179,85 +10451,74 @@ paths: description: Error security: - apiKey: [] - summary: Run an allowlisted CLI command + summary: Replace an employment record's mutable fields tags: - API - /api/v1/cli/search: - get: - operationId: searchCLI + /api/v1/employments/{id}/end: + post: + operationId: endEmployment parameters: - - description: Search query - in: query - name: q + - description: Employment ID + in: path + name: id required: true - schema: - type: string - - description: Maximum number of rows to return - in: query - name: limit schema: format: int64 type: integer - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer - - description: Message type filter; repeat or comma-separate for multiple values - in: query - name: message_type - schema: - type: string - - description: Restrict to one account/source - in: query - name: account - schema: - type: string - - description: Restrict to one collection - in: query - name: collection + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EndEmploymentBody" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliSearchResponse" - description: OK - default: + $ref: "#/components/schemas/Employment" + description: OK + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Search messages for CLI output - tags: - - API - /api/v1/cli/stats: - get: - operationId: getCLIStats - parameters: - - description: Restrict to one account/source - in: query - name: account - schema: - type: string - - description: Restrict to one collection - in: query - name: collection - schema: - type: string - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/CliStatsResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8266,39 +10527,68 @@ paths: description: Error security: - apiKey: [] - summary: Get CLI-compatible archive statistics + summary: End an employment record tags: - API - /api/v1/cli/sync: + /api/v1/employments/{id}/primary: post: - operationId: syncCLI + operationId: setPrimaryEmployment parameters: - - description: Account email or display name to sync - in: query - name: email - schema: - type: string - - description: IMAP folder names to include (repeatable) - in: query - name: folder + - description: Employment ID + in: path + name: id + required: true schema: - items: - type: string - type: array - - description: IMAP folder names to exclude (repeatable) - in: query - name: skip-folder + format: int64 + type: integer + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true schema: - items: - type: string - type: array + type: string responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLISyncEvent" + $ref: "#/components/schemas/Employment" description: OK + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8307,65 +10597,45 @@ paths: description: Error security: - apiKey: [] - summary: Run CLI incremental sync + summary: Set the primary current employment tags: - API - /api/v1/cli/sync-full: + /api/v1/explore: post: - operationId: syncFullCLI - parameters: - - description: Account email or display name to sync - in: query - name: email - schema: - type: string - - description: Gmail search query - in: query - name: query - schema: - type: string - - description: Only messages on or after this YYYY-MM-DD date - in: query - name: after - schema: - type: string - - description: Only messages before this YYYY-MM-DD date - in: query - name: before - schema: - type: string - - description: Maximum messages to sync - in: query - name: limit - schema: - format: int64 - type: integer - - description: Ignore checkpoints and start fresh - in: query - name: noresume - schema: - type: boolean - - description: IMAP folder names to include (repeatable) - in: query - name: folder - schema: - items: - type: string - type: array - - description: IMAP folder names to exclude (repeatable) - in: query - name: skip-folder - schema: - items: - type: string - type: array + operationId: explore + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ExploreHTTPRequest" + required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLISyncEvent" + $ref: "#/components/schemas/ExploreHTTPResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8374,70 +10644,71 @@ paths: description: Error security: - apiKey: [] - summary: Run CLI full sync + summary: Explore canonical archive entries tags: - - API - /api/v1/cli/verify: + - Exploration + /api/v1/explore/files: post: - operationId: verifyCLI - parameters: - - description: Account email to verify - in: query - name: email - required: true - schema: - type: string - - description: Number of messages to sample for MIME verification - in: query - name: sample - schema: - format: int64 - type: integer - - description: Skip SQLite integrity check - in: query - name: skip_db_check - schema: - type: boolean - - description: Emit JSON summary output - in: query - name: json - schema: - type: boolean + operationId: listExploreFiles + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ExploreFilesHTTPRequest" + required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLIVerifyEvent" + $ref: "#/components/schemas/ExploreFilesHTTPResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Verify the CLI archive against Gmail - tags: - - API - /api/v1/communication-services: - get: - description: Lists the small open service catalog without pagination, including aliases and normalization policy. - operationId: listCommunicationServices - parameters: - - description: Include inactive catalog entries - in: query - name: include_inactive - schema: - default: false - type: boolean + security: + - apiKey: [] + summary: List bounded chronological attachment facts + tags: + - Exploration + /api/v1/explore/groups: + post: + operationId: exploreGroups + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ExploreGroupsHTTPRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CommunicationServicesResponse" + $ref: "#/components/schemas/ExploreGroupsHTTPResponse" description: OK "400": content: @@ -8445,12 +10716,20 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8459,24 +10738,24 @@ paths: description: Error security: - apiKey: [] - summary: List communication services + summary: Group canonical archive entries tags: - - API + - Exploration + /api/v1/explore/match-counts: post: - description: Registers an unknown or custom service without a schema migration. Re-registering a slug is idempotent. - operationId: createCommunicationService + operationId: countExploreMatches requestBody: content: application/json: schema: - $ref: "#/components/schemas/CreateCommunicationServiceRequest" + $ref: "#/components/schemas/ExploreMatchCountsRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CommunicationService" + $ref: "#/components/schemas/ExploreMatchCountsResponse" description: OK "400": content: @@ -8484,12 +10763,6 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error "409": content: application/json: @@ -8500,8 +10773,10 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8510,26 +10785,24 @@ paths: description: Error security: - apiKey: [] - summary: Register a communication service + summary: Count exact lexical matches in visible rows tags: - - API - /api/v1/content/remote-image: + - Exploration + /api/v1/explore/preflight: post: - operationId: getRemoteImage + operationId: preflightExploreSelection requestBody: content: application/json: schema: - $ref: "#/components/schemas/RemoteImageRequest" + $ref: "#/components/schemas/ExplorePreflightRequest" required: true responses: "200": content: - image/*: + application/json: schema: - contentMediaType: application/octet-stream - format: binary - type: string + $ref: "#/components/schemas/ExplorePreflightResponse" description: OK "400": content: @@ -8537,24 +10810,20 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "415": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "502": + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8563,83 +10832,45 @@ paths: description: Error security: - apiKey: [] - summary: Fetch a consented remote mail image through the SSRF-hardened daemon proxy + summary: Preflight a revision-pinned analytical selection tags: - - API - /api/v1/conversations/{id}: - get: - operationId: getConversation - parameters: - - description: Conversation ID - in: path - name: id - required: true - schema: - format: int64 - type: integer - - description: Selected message ID anchoring the chronological window - in: query - name: anchor - required: true - schema: - format: int64 - type: integer - - description: Messages before the anchor (default 25, max 50) - in: query - name: before - schema: - format: int64 - type: integer - - description: Messages after the anchor (default 25, max 50) - in: query - name: after - schema: - format: int64 - type: integer - - description: Lower UTC bound, inclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) - in: query - name: start - schema: - type: string - - description: Upper UTC bound, exclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) - in: query - name: end - schema: - type: string + - Exploration + /api/v1/files/groups: + post: + operationId: groupFiles + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/FileGroupsHTTPRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ConversationResponse" + $ref: "#/components/schemas/FileGroupsHTTPResponse" description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Get a bounded containing conversation - tags: - - API - /api/v1/deletions: - get: - operationId: listDeletions - parameters: - - description: Filter manifests by status (pending, in_progress, completed, failed, cancelled) - in: query - name: status - schema: - type: string - responses: - "200": + "409": content: application/json: schema: - $ref: "#/components/schemas/ListDeletionsResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8648,30 +10879,45 @@ paths: description: Error security: - apiKey: [] - summary: List staged deletion manifests + summary: Group analytical files tags: - - API + - Exploration + /api/v1/files/search: post: - operationId: stageDeletion + operationId: searchFiles requestBody: content: application/json: schema: - $ref: "#/components/schemas/StageDeletionRequest" + $ref: "#/components/schemas/FileSearchHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/StageDeletionResponse" + $ref: "#/components/schemas/FileSearchHTTPResponse" description: OK - "201": + "400": content: application/json: schema: - $ref: "#/components/schemas/StageDeletionResponse" - description: Created + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8680,25 +10926,26 @@ paths: description: Error security: - apiKey: [] - summary: Stage messages for deletion + summary: Search analytical files tags: - - API - /api/v1/deletions/{id}: - delete: - operationId: cancelDeletion + - Exploration + /api/v1/files/{id}: + get: + operationId: getFile parameters: - - description: Deletion manifest ID + - description: File attachment ID in: path name: id required: true schema: - type: string + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CancelDeletionResponse" + $ref: "#/components/schemas/FileMetadataResponse" description: OK default: content: @@ -8708,59 +10955,48 @@ paths: description: Error security: - apiKey: [] - summary: Cancel a staged deletion manifest + summary: Get authoritative file metadata tags: - API + /api/v1/files/{id}/content: get: - operationId: getDeletion + operationId: getFileContent parameters: - - description: Deletion manifest ID + - description: File attachment ID in: path name: id required: true schema: - type: string + format: int64 + type: integer responses: "200": content: - application/json: + "*/*": schema: - $ref: "#/components/schemas/DeletionManifestDetail" + contentMediaType: application/octet-stream + format: binary + type: string description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Inspect a staged deletion manifest - tags: - - API - /api/v1/domains/search: - post: - operationId: searchDomains - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/IdentitySearchHTTPRequest" - required: true - responses: - "200": + "401": content: application/json: schema: - $ref: "#/components/schemas/DomainSearchHTTPResponse" - description: OK - "400": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "500": content: application/json: schema: @@ -8770,10 +11006,8 @@ paths: content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8782,25 +11016,18 @@ paths: description: Error security: - apiKey: [] - summary: Search analytical domains + summary: Download content for one authoritative file row tags: - - Exploration - /api/v1/domains/{domain}: + - API + /api/v1/health: get: - operationId: getDomain - parameters: - - description: Exact normalized domain fact - in: path - name: domain - required: true - schema: - type: string + operationId: getHealth responses: "200": content: application/json: schema: - $ref: "#/components/schemas/DomainSummary" + $ref: "#/components/schemas/HealthResponse" description: OK default: content: @@ -8810,52 +11037,25 @@ paths: description: Error security: - apiKey: [] - summary: Get one analytical domain + summary: Get authenticated health details tags: - API - /api/v1/domains/{domain}/files/search: + /api/v1/identity/links: post: - operationId: searchDomainFiles - parameters: - - description: Exact normalized domain fact - in: path - name: domain - required: true - schema: - type: string + operationId: linkIdentityParticipants requestBody: content: application/json: schema: - $ref: "#/components/schemas/FileSearchHTTPRequest" + $ref: "#/components/schemas/IdentityLinkRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/FileSearchHTTPResponse" + $ref: "#/components/schemas/IdentityLinkResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": - content: - application/json: - schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable default: content: application/json: @@ -8864,52 +11064,58 @@ paths: description: Error security: - apiKey: [] - summary: Search one domain's analytical files + summary: Assert two participants are the same person tags: - - Exploration - /api/v1/domains/{domain}/summary: + - API + /api/v1/identity/unlinks: post: - operationId: getDomainContextSummary - parameters: - - description: Exact normalized domain fact - in: path - name: domain - required: true - schema: - type: string + operationId: unlinkIdentityParticipants requestBody: content: application/json: schema: - $ref: "#/components/schemas/ExploreHTTPRequest" + $ref: "#/components/schemas/IdentityLinkRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/DomainContextSummaryHTTPResponse" + $ref: "#/components/schemas/IdentityLinkResponse" description: OK - "400": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + security: + - apiKey: [] + summary: Remove a link edge between two participants + tags: + - API + /api/v1/import/meeting: + post: + operationId: importMeeting + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MeetingImportRequest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/MeetingImportResponse" + description: OK + "201": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/MeetingImportResponse" + description: Created default: content: application/json: @@ -8918,52 +11124,47 @@ paths: description: Error security: - apiKey: [] - summary: Get one domain's contextual analytical summary + summary: Import one meeting tags: - - Exploration - /api/v1/domains/{domain}/timeline: - post: - operationId: getDomainTimeline + - API + /api/v1/integrations/tasks/search: + get: + operationId: searchIntegrationTasks parameters: - - description: Exact normalized domain fact - in: path - name: domain + - description: Task title search within the configured project + in: query + name: q required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreHTTPRequest" - required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreHTTPResponse" + $ref: "#/components/schemas/TaskSearchResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: Search tasks in the configured project + tags: + - API + /api/v1/integrations/tasks/status: + get: + operationId: getTaskIntegrationStatus + responses: + "200": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/TaskIntegrationStatusResponse" + description: OK default: content: application/json: @@ -8972,45 +11173,53 @@ paths: description: Error security: - apiKey: [] - summary: Get one domain's canonical activity timeline + summary: Get task integration capability status tags: - - Exploration - /api/v1/explore: + - API + /api/v1/integrations/tasks/test: post: - operationId: explore - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreHTTPRequest" - required: true + operationId: testTaskIntegration responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreHTTPResponse" + $ref: "#/components/schemas/TaskIntegrationStatusResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: Test task integration discovery, authentication, capabilities, and project + tags: + - API + /api/v1/messages: + get: + operationId: listMessages + parameters: + - description: One-based page number (default 1; values below 1 are clamped to 1). Non-numeric values are rejected with 400. + in: query + name: page + schema: + format: int64 + type: integer + - description: Page size (default 20, max 100; out-of-range values are clamped). Non-numeric values are rejected with 400. + in: query + name: page_size + schema: + format: int64 + type: integer + responses: + "200": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/MessageListResponse" + description: OK default: content: application/json: @@ -9019,32 +11228,50 @@ paths: description: Error security: - apiKey: [] - summary: Explore canonical archive entries + summary: List messages tags: - - Exploration - /api/v1/explore/files: - post: - operationId: listExploreFiles - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreFilesHTTPRequest" - required: true + - API + /api/v1/messages/changes: + get: + operationId: listChangedMessages + parameters: + - description: "Opaque cursor from the next_cursor of the previous response, sent back verbatim. Do not parse, construct, compare, or order it; its contents may change without notice. Omit, or send it empty, to start from the beginning of the archive. The token is not authenticated: the server does not sign it and cannot tell one it issued from a well-formed one you built, so a fabricated cursor naming this archive is accepted and simply moves your own position. Rejected with 400 invalid_cursor, rather than read as the beginning: a token the server cannot read, one carrying a cursor format this build does not speak, and one issued against a different archive" + in: query + name: cursor + schema: + type: string + - description: Maximum number of rows to return (default 100, max 500; values below 1 fall back to the default) + in: query + name: limit + schema: + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreFilesHTTPResponse" - description: OK - "400": + $ref: "#/components/schemas/ChangesResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "429": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "500": content: application/json: schema: @@ -9054,10 +11281,8 @@ paths: content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9066,45 +11291,279 @@ paths: description: Error security: - apiKey: [] - summary: List bounded chronological attachment facts + summary: List messages whose content changed since a cursor tags: - - Exploration - /api/v1/explore/groups: - post: - operationId: exploreGroups - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreGroupsHTTPRequest" - required: true + - API + /api/v1/messages/filter: + get: + operationId: filterMessages + parameters: + - description: Sender email/address filter + in: query + name: sender + schema: + type: string + - description: Sender display-name filter + in: query + name: sender_name + schema: + type: string + - description: Recipient email/address filter + in: query + name: recipient + schema: + type: string + - description: Recipient display-name filter + in: query + name: recipient_name + schema: + type: string + - description: Domain filter + in: query + name: domain + schema: + type: string + - description: Label filter + in: query + name: label + schema: + type: string + - description: Message type filter + in: query + name: message_type + schema: + type: string + - description: Named time period + in: query + name: time_period + schema: + type: string + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Conversation ID + in: query + name: conversation_id + schema: + format: int64 + type: integer + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + - description: Comma-separated aggregate view names to match empty values + in: query + name: empty_targets + schema: + type: string + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + - description: Maximum number of rows to return (default and max 500; larger values are clamped) + in: query + name: limit + schema: + format: int64 + type: integer + - description: "Sort field: date, size, or subject" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/FilteredMessagesResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List filtered messages + tags: + - API + /api/v1/messages/gmail-ids: + get: + operationId: getGmailIDsByFilter + parameters: + - description: Sender email/address filter + in: query + name: sender + schema: + type: string + - description: Sender display-name filter + in: query + name: sender_name + schema: + type: string + - description: Recipient email/address filter + in: query + name: recipient + schema: + type: string + - description: Recipient display-name filter + in: query + name: recipient_name + schema: + type: string + - description: Domain filter + in: query + name: domain + schema: + type: string + - description: Label filter + in: query + name: label + schema: + type: string + - description: Message type filter + in: query + name: message_type + schema: + type: string + - description: Named time period + in: query + name: time_period + schema: + type: string + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Conversation ID + in: query + name: conversation_id + schema: + format: int64 + type: integer + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + - description: Comma-separated aggregate view names to match empty values + in: query + name: empty_targets + schema: + type: string + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + - description: Maximum number of rows to return (default and max 500; larger values are clamped) + in: query + name: limit + schema: + format: int64 + type: integer + - description: "Sort field: date, size, or subject" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreGroupsHTTPResponse" + $ref: "#/components/schemas/GmailIDsResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: List Gmail message IDs matching a filter + tags: + - API + /api/v1/messages/{id}: + get: + operationId: getMessage + parameters: + - description: Message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + responses: + "200": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/MessageDetail" + description: OK default: content: application/json: @@ -9113,24 +11572,34 @@ paths: description: Error security: - apiKey: [] - summary: Group canonical archive entries + summary: Get one message tags: - - Exploration - /api/v1/explore/match-counts: - post: - operationId: countExploreMatches - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreMatchCountsRequest" - required: true + - API + /api/v1/messages/{id}/inline: + get: + operationId: getMessageInlinePart + parameters: + - description: Message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Inline MIME Content-ID + in: query + name: cid + required: true + schema: + type: string responses: "200": content: - application/json: + application/octet-stream: schema: - $ref: "#/components/schemas/ExploreMatchCountsResponse" + contentMediaType: application/octet-stream + format: binary + type: string description: OK "400": content: @@ -9138,54 +11607,31 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "401": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": - content: - application/json: - schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable - default: + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Count exact lexical matches in visible rows - tags: - - Exploration - /api/v1/explore/preflight: - post: - operationId: preflightExploreSelection - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExplorePreflightRequest" - required: true - responses: - "200": + "415": content: application/json: schema: - $ref: "#/components/schemas/ExplorePreflightResponse" - description: OK - "400": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "500": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "501": content: application/json: schema: @@ -9195,10 +11641,8 @@ paths: content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9207,45 +11651,27 @@ paths: description: Error security: - apiKey: [] - summary: Preflight a revision-pinned analytical selection + summary: Get an inline MIME part tags: - - Exploration - /api/v1/files/groups: - post: - operationId: groupFiles - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/FileGroupsHTTPRequest" - required: true + - API + /api/v1/messages/{id}/tasks: + get: + operationId: listMessageTasks + parameters: + - description: Archived email message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/FileGroupsHTTPResponse" + $ref: "#/components/schemas/TaskLinkLookupResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": - content: - application/json: - schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable default: content: application/json: @@ -9254,45 +11680,38 @@ paths: description: Error security: - apiKey: [] - summary: Group analytical files + summary: List tasks linked to an archived email tags: - - Exploration - /api/v1/files/search: + - API post: - operationId: searchFiles + operationId: createOrLinkMessageTask + parameters: + - description: Archived email message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Browser-generated retry-stable request ID + in: header + name: X-Request-Id + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/FileSearchHTTPRequest" + $ref: "#/components/schemas/TaskLinkMutationRequest" required: true responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/FileSearchHTTPResponse" - description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + "201": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/TaskLinkMutationResponse" + description: Created default: content: application/json: @@ -9301,26 +11720,32 @@ paths: description: Error security: - apiKey: [] - summary: Search analytical files + summary: Create or link a task for an archived email tags: - - Exploration - /api/v1/files/{id}: - get: - operationId: getFile + - API + /api/v1/messages/{id}/tasks/{task_id}: + delete: + operationId: unlinkMessageTask parameters: - - description: File attachment ID + - description: Archived email message ID in: path name: id required: true schema: format: int64 type: integer + - description: External task ID + in: path + name: task_id + required: true + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/FileMetadataResponse" + $ref: "#/components/schemas/TaskLinkMutationResponse" description: OK default: content: @@ -9330,28 +11755,41 @@ paths: description: Error security: - apiKey: [] - summary: Get authoritative file metadata + summary: Unlink a task from an archived email tags: - API - /api/v1/files/{id}/content: + /api/v1/organizations: get: - operationId: getFileContent + operationId: listOrganizations parameters: - - description: File attachment ID - in: path - name: id - required: true + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer + - description: Include retired organizations + in: query + name: include_retired + schema: + type: boolean + - description: Normalized-name search + in: query + name: q schema: - format: int64 - type: integer + type: string responses: "200": content: - "*/*": + application/json: schema: - contentMediaType: application/octet-stream - format: binary - type: string + $ref: "#/components/schemas/OrganizationsResponse" description: OK "400": content: @@ -9359,19 +11797,48 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "503": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + security: + - apiKey: [] + summary: List organizations + tags: + - API + post: + operationId: createOrganization + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationCreateBody" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Organization" + description: Created + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + Location: + description: Canonical URL of the created organization + schema: + type: string + "400": content: application/json: schema: @@ -9391,46 +11858,49 @@ paths: description: Error security: - apiKey: [] - summary: Download content for one authoritative file row + summary: Create an organization tags: - API - /api/v1/health: + /api/v1/organizations/duplicate-suggestions: get: - operationId: getHealth + operationId: listOrganizationDuplicateSuggestions + parameters: + - description: Filter by status + in: query + name: status + schema: + type: string + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/HealthResponse" + $ref: "#/components/schemas/OrganizationDuplicateSuggestionsResponse" description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Get authenticated health details - tags: - - API - /api/v1/identity/links: - post: - operationId: linkIdentityParticipants - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/IdentityLinkRequest" - required: true - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/IdentityLinkResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9439,25 +11909,25 @@ paths: description: Error security: - apiKey: [] - summary: Assert two participants are the same person + summary: List organization duplicate suggestions tags: - API - /api/v1/identity/unlinks: + /api/v1/organizations/duplicate-suggestions/refresh: post: - operationId: unlinkIdentityParticipants - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/IdentityLinkRequest" - required: true + operationId: refreshOrganizationDuplicateSuggestions responses: "200": content: application/json: schema: - $ref: "#/components/schemas/IdentityLinkResponse" + $ref: "#/components/schemas/RefreshOrganizationDuplicateSuggestionsResponse" description: OK + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9466,31 +11936,57 @@ paths: description: Error security: - apiKey: [] - summary: Remove a link edge between two participants + summary: Refresh organization duplicate suggestions tags: - API - /api/v1/import/meeting: + /api/v1/organizations/duplicate-suggestions/{id}/resolve: post: - operationId: importMeeting + operationId: resolveOrganizationDuplicateSuggestion + parameters: + - description: Duplicate suggestion ID + in: path + name: id + required: true + schema: + format: int64 + type: integer requestBody: content: application/json: schema: - $ref: "#/components/schemas/MeetingImportRequest" + $ref: "#/components/schemas/ResolveOrganizationDuplicateSuggestionBody" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/MeetingImportResponse" + $ref: "#/components/schemas/OrganizationDuplicateSuggestion" description: OK - "201": + "400": content: application/json: schema: - $ref: "#/components/schemas/MeetingImportResponse" - description: Created + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9499,68 +11995,59 @@ paths: description: Error security: - apiKey: [] - summary: Import one meeting + summary: Resolve an organization duplicate suggestion tags: - API - /api/v1/integrations/tasks/search: - get: - operationId: searchIntegrationTasks + /api/v1/organizations/{id}: + delete: + operationId: deleteOrganization parameters: - - description: Task title search within the configured project - in: query - name: q + - description: Organization ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest organization read + in: header + name: If-Match required: true schema: type: string responses: - "200": + "204": + description: No Content + "400": content: application/json: schema: - $ref: "#/components/schemas/TaskSearchResponse" - description: OK - default: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Search tasks in the configured project - tags: - - API - /api/v1/integrations/tasks/status: - get: - operationId: getTaskIntegrationStatus - responses: - "200": + "409": content: application/json: schema: - $ref: "#/components/schemas/TaskIntegrationStatusResponse" - description: OK - default: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Get task integration capability status - tags: - - API - /api/v1/integrations/tasks/test: - post: - operationId: testTaskIntegration - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/TaskIntegrationStatusResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9569,22 +12056,16 @@ paths: description: Error security: - apiKey: [] - summary: Test task integration discovery, authentication, capabilities, and project + summary: Delete an organization without employment records tags: - API - /api/v1/messages: get: - operationId: listMessages + operationId: getOrganization parameters: - - description: One-based page number (default 1; values below 1 are clamped to 1). Non-numeric values are rejected with 400. - in: query - name: page - schema: - format: int64 - type: integer - - description: Page size (default 20, max 100; out-of-range values are clamped). Non-numeric values are rejected with 400. - in: query - name: page_size + - description: Organization ID + in: path + name: id + required: true schema: format: int64 type: integer @@ -9593,8 +12074,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/MessageListResponse" + $ref: "#/components/schemas/OrganizationProfile" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9603,50 +12107,62 @@ paths: description: Error security: - apiKey: [] - summary: List messages + summary: Get an organization tags: - API - /api/v1/messages/changes: - get: - operationId: listChangedMessages + patch: + operationId: patchOrganization parameters: - - description: "Opaque cursor from the next_cursor of the previous response, sent back verbatim. Do not parse, construct, compare, or order it; its contents may change without notice. Omit, or send it empty, to start from the beginning of the archive. The token is not authenticated: the server does not sign it and cannot tell one it issued from a well-formed one you built, so a fabricated cursor naming this archive is accepted and simply moves your own position. Rejected with 400 invalid_cursor, rather than read as the beginning: a token the server cannot read, one carrying a cursor format this build does not speak, and one issued against a different archive" - in: query - name: cursor - schema: - type: string - - description: Maximum number of rows to return (default 100, max 500; values below 1 fall back to the default) - in: query - name: limit + - description: Organization ID + in: path + name: id + required: true schema: format: int64 type: integer + - description: Strong ETag returned by the latest organization read + in: header + name: If-Match + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationBody" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ChangesResponse" + $ref: "#/components/schemas/Organization" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "429": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + "428": content: application/json: schema: @@ -9666,115 +12182,28 @@ paths: description: Error security: - apiKey: [] - summary: List messages whose content changed since a cursor + summary: Replace an organization's mutable fields tags: - API - /api/v1/messages/filter: + /api/v1/organizations/{id}/attributes: get: - operationId: filterMessages + operationId: listOrganizationAttributes parameters: - - description: Sender email/address filter - in: query - name: sender - schema: - type: string - - description: Sender display-name filter - in: query - name: sender_name - schema: - type: string - - description: Recipient email/address filter - in: query - name: recipient - schema: - type: string - - description: Recipient display-name filter - in: query - name: recipient_name - schema: - type: string - - description: Domain filter - in: query - name: domain - schema: - type: string - - description: Label filter - in: query - name: label - schema: - type: string - - description: Message type filter - in: query - name: message_type - schema: - type: string - - description: Named time period - in: query - name: time_period - schema: - type: string - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Conversation ID - in: query - name: conversation_id - schema: - format: int64 - type: integer - - description: Source ID - in: query - name: source_id + - description: Organization ID + in: path + name: id + required: true schema: format: int64 type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages + - description: Include superseded values in: query - name: hide_deleted + name: include_superseded schema: type: boolean - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - - description: Comma-separated aggregate view names to match empty values - in: query - name: empty_targets - schema: - type: string - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer - - description: Maximum number of rows to return (default and max 500; larger values are clamped) - in: query - name: limit - schema: - format: int64 - type: integer - - description: "Sort field: date, size, or subject" + - description: Restrict to one definition in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction + name: definition_slug schema: type: string responses: @@ -9782,163 +12211,26 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/FilteredMessagesResponse" + $ref: "#/components/schemas/OrganizationAttributesResponse" description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: List filtered messages - tags: - - API - /api/v1/messages/gmail-ids: - get: - operationId: getGmailIDsByFilter - parameters: - - description: Sender email/address filter - in: query - name: sender - schema: - type: string - - description: Sender display-name filter - in: query - name: sender_name - schema: - type: string - - description: Recipient email/address filter - in: query - name: recipient - schema: - type: string - - description: Recipient display-name filter - in: query - name: recipient_name - schema: - type: string - - description: Domain filter - in: query - name: domain - schema: - type: string - - description: Label filter - in: query - name: label - schema: - type: string - - description: Message type filter - in: query - name: message_type - schema: - type: string - - description: Named time period - in: query - name: time_period - schema: - type: string - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Conversation ID - in: query - name: conversation_id - schema: - format: int64 - type: integer - - description: Source ID - in: query - name: source_id - schema: - format: int64 - type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages - in: query - name: hide_deleted - schema: - type: boolean - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - - description: Comma-separated aggregate view names to match empty values - in: query - name: empty_targets - schema: - type: string - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer - - description: Maximum number of rows to return (default and max 500; larger values are clamped) - in: query - name: limit - schema: - format: int64 - type: integer - - description: "Sort field: date, size, or subject" - in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/GmailIDsResponse" - description: OK - default: + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: List Gmail message IDs matching a filter - tags: - - API - /api/v1/messages/{id}: - get: - operationId: getMessage - parameters: - - description: Message ID - in: path - name: id - required: true - schema: - format: int64 - type: integer - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/MessageDetail" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9947,66 +12239,115 @@ paths: description: Error security: - apiKey: [] - summary: Get one message + summary: List organization typed attributes tags: - API - /api/v1/messages/{id}/inline: - get: - operationId: getMessageInlinePart + post: + operationId: setOrganizationAttribute parameters: - - description: Message ID + - description: Organization ID in: path name: id required: true schema: format: int64 type: integer - - description: Inline MIME Content-ID - in: query - name: cid - required: true - schema: - type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SetOrganizationAttributeBody" + required: true responses: "200": content: - application/octet-stream: + application/json: schema: - contentMediaType: application/octet-stream - format: binary - type: string + $ref: "#/components/schemas/OrganizationAttributeWrite" description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationAttributeWrite" + description: Created "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "415": + "503": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "501": + security: + - apiKey: [] + summary: Set an organization typed attribute + tags: + - API + /api/v1/organizations/{id}/employments: + get: + operationId: listOrganizationEmployments + parameters: + - description: Organization ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Only current employments + in: query + name: current_only + schema: + type: boolean + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EmploymentsResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: @@ -10026,14 +12367,14 @@ paths: description: Error security: - apiKey: [] - summary: Get an inline MIME part + summary: List an organization's employment records tags: - API - /api/v1/messages/{id}/tasks: + /api/v1/organizations/{id}/history: get: - operationId: listMessageTasks + operationId: getOrganizationHistory parameters: - - description: Archived email message ID + - description: Organization ID in: path name: id required: true @@ -10045,8 +12386,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TaskLinkLookupResponse" + $ref: "#/components/schemas/OrganizationProfile" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -10055,22 +12419,23 @@ paths: description: Error security: - apiKey: [] - summary: List tasks linked to an archived email + summary: Get organization profile history tags: - API + /api/v1/organizations/{id}/merge: post: - operationId: createOrLinkMessageTask + operationId: mergeOrganization parameters: - - description: Archived email message ID + - description: Organization ID in: path name: id required: true schema: format: int64 type: integer - - description: Browser-generated retry-stable request ID + - description: Strong ETag returned by the latest organization read in: header - name: X-Request-Id + name: If-Match required: true schema: type: string @@ -10078,15 +12443,50 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TaskLinkMutationRequest" + $ref: "#/components/schemas/MergeOrganizationBody" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/TaskLinkMutationResponse" - description: Created + $ref: "#/components/schemas/Organization" + description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -10095,33 +12495,74 @@ paths: description: Error security: - apiKey: [] - summary: Create or link a task for an archived email + summary: Merge another organization into this organization tags: - API - /api/v1/messages/{id}/tasks/{task_id}: - delete: - operationId: unlinkMessageTask + /api/v1/organizations/{id}/profile: + put: + operationId: putOrganizationProfile parameters: - - description: Archived email message ID + - description: Organization ID in: path name: id required: true schema: format: int64 type: integer - - description: External task ID - in: path - name: task_id + - description: Strong ETag returned by the latest organization read + in: header + name: If-Match required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationProfileBody" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/TaskLinkMutationResponse" + $ref: "#/components/schemas/OrganizationProfile" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -10130,7 +12571,7 @@ paths: description: Error security: - apiKey: [] - summary: Unlink a task from an archived email + summary: Replace organization profile collections tags: - API /api/v1/people/search: @@ -10841,6 +13282,70 @@ paths: summary: Set a person's attribute value tags: - API + /api/v1/persons/{id}/employments: + get: + operationId: listPersonEmployments + parameters: + - description: Person ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Only current employments + in: query + name: current_only + schema: + type: boolean + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EmploymentsResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List a person's employment history + tags: + - API /api/v1/persons/{id}/profile: get: description: Returns only current structured values at one person revision. Superseded values and archive observations are available from the separate history endpoint. diff --git a/cmd/msgvault/cmd/employment.go b/cmd/msgvault/cmd/employment.go new file mode 100644 index 000000000..973fe636b --- /dev/null +++ b/cmd/msgvault/cmd/employment.go @@ -0,0 +1,368 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + "go.kenn.io/msgvault/internal/daemonclient" + apiclient "go.kenn.io/msgvault/pkg/client" + "go.kenn.io/msgvault/pkg/client/generated" +) + +var ( + employmentJSON bool + employmentPersonID, employmentOrganizationID int64 + employmentTitle, employmentRole, employmentDepartment, employmentLocation, employmentDescription string + employmentStartDate, employmentEndDate, employmentSource string + employmentPrimary, employmentNoPrimary, employmentNotCurrent, employmentCurrentOnly bool +) + +var employmentCmd = &cobra.Command{Use: "employment", Short: "Manage temporal employment records between people and organizations"} + +var employmentAddCmd = &cobra.Command{Use: "add", Short: "Add an employment record", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + body, err := employmentBodyFromFlags(cmd) + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + resp, err := daemonclient.APIResponseWithStatuses(client, []int{http.StatusCreated}, func(api *apiclient.Client) (*generated.CreateEmploymentResp, error) { + return api.CreateEmploymentWithResponse(cmd.Context(), &generated.CreateEmploymentRequestOptions{Body: &body}) + }) + if err != nil { + return err + } + return writeCLIEmployment(cmd, resp.JSON201) +}} + +var employmentShowCmd = &cobra.Command{Use: "show ", Short: "Show an employment record", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "employment") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + resp, err := getCLIEmployment(cmd, client, id) + if err != nil { + return err + } + return writeCLIEmployment(cmd, resp.JSON200) +}} + +var employmentSetCmd = &cobra.Command{Use: "set ", Short: "Replace an employment record's mutable fields", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "employment") + if err != nil { + return err + } + body, err := employmentBodyFromFlags(cmd) + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIEmployment(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("employment response was empty") + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.PatchEmploymentResp, error) { + return api.PatchEmploymentWithResponse(cmd.Context(), &generated.PatchEmploymentRequestOptions{PathParams: &generated.PatchEmploymentPath{ID: id}, Header: &generated.PatchEmploymentHeaders{IfMatch: employmentETag(id, current.JSON200.Revision)}, Body: &body}) + }) + if err != nil { + return err + } + return writeCLIEmployment(cmd, resp.JSON200) +}} + +var employmentEndCmd = &cobra.Command{Use: "end ", Short: "End an employment without deleting its history", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "employment") + if err != nil { + return err + } + if strings.TrimSpace(employmentEndDate) == "" { + return usageErr(cmd, errors.New("--end-date is required")) + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIEmployment(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("employment response was empty") + } + body := generated.EndEmploymentBody{EndDate: employmentEndDate} + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.EndEmploymentResp, error) { + return api.EndEmploymentWithResponse(cmd.Context(), &generated.EndEmploymentRequestOptions{PathParams: &generated.EndEmploymentPath{ID: id}, Header: &generated.EndEmploymentHeaders{IfMatch: employmentETag(id, current.JSON200.Revision)}, Body: &body}) + }) + if err != nil { + return err + } + return writeCLIEmployment(cmd, resp.JSON200) +}} + +var employmentSetPrimaryCmd = &cobra.Command{Use: "set-primary ", Short: "Set the primary current employment", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "employment") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIEmployment(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("employment response was empty") + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.SetPrimaryEmploymentResp, error) { + return api.SetPrimaryEmploymentWithResponse(cmd.Context(), &generated.SetPrimaryEmploymentRequestOptions{PathParams: &generated.SetPrimaryEmploymentPath{ID: id}, Header: &generated.SetPrimaryEmploymentHeaders{IfMatch: employmentETag(id, current.JSON200.Revision)}}) + }) + if err != nil { + return err + } + return writeCLIEmployment(cmd, resp.JSON200) +}} + +var employmentDeleteCmd = &cobra.Command{Use: "delete ", Short: "Permanently delete an employment record", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "employment") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIEmployment(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("employment response was empty") + } + _, err = daemonclient.APIResponseWithStatuses(client, []int{http.StatusNoContent}, func(api *apiclient.Client) (*generated.DeleteEmploymentResp, error) { + return api.DeleteEmploymentWithResponse(cmd.Context(), &generated.DeleteEmploymentRequestOptions{PathParams: &generated.DeleteEmploymentPath{ID: id}, Header: &generated.DeleteEmploymentHeaders{IfMatch: employmentETag(id, current.JSON200.Revision)}}) + }) + if err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Deleted employment %d\n", id) + return nil +}} + +var employmentListCmd = &cobra.Command{Use: cmdUseList, Short: "List employment records", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + personSet, organizationSet := cmd.Flags().Changed("person"), cmd.Flags().Changed("organization") + if !personSet && !organizationSet { + return usageErr(cmd, errors.New("--person or --organization is required")) + } + if personSet && organizationSet { + return usageErr(cmd, errors.New("--person and --organization are mutually exclusive")) + } + var id int64 + if personSet { + id = employmentPersonID + } else { + id = employmentOrganizationID + } + if id <= 0 { + kind := "organization" + if personSet { + kind = "person" + } + return usageErr(cmd, fmt.Errorf("%s ID must be a positive integer", kind)) + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + currentOnly := employmentCurrentOnly + if personSet { + resp, getErr := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ListPersonEmploymentsResp, error) { + return api.ListPersonEmploymentsWithResponse(cmd.Context(), &generated.ListPersonEmploymentsRequestOptions{PathParams: &generated.ListPersonEmploymentsPath{ID: id}, Query: &generated.ListPersonEmploymentsQuery{CurrentOnly: ¤tOnly}}) + }) + if getErr != nil { + return getErr + } + if resp.JSON200 == nil { + return errors.New("employment list response was empty") + } + return writeCLIEmploymentList(cmd, resp.JSON200, true, employmentJSON) + } + resp, getErr := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ListOrganizationEmploymentsResp, error) { + return api.ListOrganizationEmploymentsWithResponse(cmd.Context(), &generated.ListOrganizationEmploymentsRequestOptions{PathParams: &generated.ListOrganizationEmploymentsPath{ID: id}, Query: &generated.ListOrganizationEmploymentsQuery{CurrentOnly: ¤tOnly}}) + }) + if getErr != nil { + return getErr + } + if resp.JSON200 == nil { + return errors.New("employment list response was empty") + } + return writeCLIEmploymentList(cmd, resp.JSON200, false, employmentJSON) +}} + +func getCLIEmployment(cmd *cobra.Command, client *daemonclient.Client, id int64) (*generated.GetEmploymentResp, error) { + return daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.GetEmploymentResp, error) { + return api.GetEmploymentWithResponse(cmd.Context(), &generated.GetEmploymentRequestOptions{PathParams: &generated.GetEmploymentPath{ID: id}}) + }) +} +func employmentETag(id, revision int64) string { + return fmt.Sprintf(`"employment-%d-r%d"`, id, revision) +} + +func employmentBodyFromFlags(cmd *cobra.Command) (generated.CreateEmploymentBody, error) { + if employmentPrimary && employmentNoPrimary { + return generated.CreateEmploymentBody{}, usageErr(cmd, errors.New("--primary and --no-primary are mutually exclusive")) + } + if employmentPersonID <= 0 { + return generated.CreateEmploymentBody{}, usageErr(cmd, errors.New("--person is required and must be a positive integer")) + } + if employmentOrganizationID <= 0 { + return generated.CreateEmploymentBody{}, usageErr(cmd, errors.New("--organization is required and must be a positive integer")) + } + source := strings.TrimSpace(employmentSource) + if source == "" { + source = "user" + } + body := generated.CreateEmploymentBody{PersonID: employmentPersonID, OrganizationID: employmentOrganizationID, Source: generated.EmploymentBodySource(source)} + if cmd.Flags().Changed("title") { + body.Title = &employmentTitle + } + if cmd.Flags().Changed("role") { + body.Role = &employmentRole + } + if cmd.Flags().Changed("department") { + body.Department = &employmentDepartment + } + if cmd.Flags().Changed("location") { + body.Location = &employmentLocation + } + if cmd.Flags().Changed("description") { + body.Description = &employmentDescription + } + if cmd.Flags().Changed("start") { + body.StartDate = &employmentStartDate + } + if cmd.Flags().Changed("end") { + body.EndDate = &employmentEndDate + } + if employmentPrimary { + value := true + body.IsPrimary = &value + } + if employmentNoPrimary { + value := false + body.IsPrimary = &value + } + if employmentNotCurrent { + value := false + body.IsCurrent = &value + } + return body, nil +} + +func formatCLIPartialDate(value *generated.PartialDate) string { + if value == nil { + return "-" + } + if value.Year == nil { + return "-" + } + if value.Day != nil && value.Month != nil { + return fmt.Sprintf("%04d-%02d-%02d", *value.Year, *value.Month, *value.Day) + } + if value.Month != nil { + return fmt.Sprintf("%04d-%02d", *value.Year, *value.Month) + } + return fmt.Sprintf("%04d", *value.Year) +} +func writeCLIEmployment(cmd *cobra.Command, employment *generated.Employment) error { + if employment == nil { + return errors.New("employment response was empty") + } + if employmentJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(employment) + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Employment: %d\nPerson: %d\nOrganization: %d\nTitle: %s\nRole: %s\nDepartment: %s\nStart date: %s\nEnd date: %s\nCurrent: %t\nPrimary: %t\nSource: %s\nRevision: %d\n", employment.ID, employment.PersonID, employment.OrganizationID, cliString(employment.Title), cliString(employment.Role), cliString(employment.Department), formatCLIPartialDate(employment.StartDate), formatCLIPartialDate(employment.EndDate), employment.IsCurrent, employment.IsPrimary, employment.Source, employment.Revision) + return nil +} +func writeCLIEmploymentList( + cmd *cobra.Command, response *generated.EmploymentsResponse, + personScoped, jsonOutput bool, +) error { + if response == nil { + return errors.New("employment list response was empty") + } + if jsonOutput { + return json.NewEncoder(cmd.OutOrStdout()).Encode(response) + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tORGANIZATION\tTITLE\tSTART\tEND\tCURRENT\tPRIMARY") + for _, employment := range response.Employments { + _, _ = fmt.Fprintf(w, "%d\t%d\t%s\t%s\t%s\t%t\t%t\n", employment.ID, employment.OrganizationID, cliString(employment.Title), formatCLIPartialDate(employment.StartDate), formatCLIPartialDate(employment.EndDate), employment.IsCurrent, employment.IsPrimary) + } + if err := w.Flush(); err != nil { + return fmt.Errorf("flush employment table: %w", err) + } + if personScoped && response.Projection != nil { + projection := response.Projection + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Current company: %s\nCurrent title: %s\n", projection.OrganizationName, cliString(projection.Title)) + if projection.Role != nil && *projection.Role != "" { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Current role: %s\n", *projection.Role) + } + if len(projection.Vcard.Org) > 0 { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "vCard ORG: %s\n", strings.Join(projection.Vcard.Org, ";")) + } + } + return nil +} + +func init() { + rootCmd.AddCommand(employmentCmd) + employmentCmd.AddCommand(employmentAddCmd, employmentShowCmd, employmentSetCmd, employmentEndCmd, employmentSetPrimaryCmd, employmentDeleteCmd, employmentListCmd) + for _, command := range []*cobra.Command{employmentAddCmd, employmentShowCmd, employmentSetCmd, employmentEndCmd, employmentSetPrimaryCmd, employmentListCmd} { + command.Flags().BoolVar(&employmentJSON, flagJSON, false, "Output as JSON") + } + for _, command := range []*cobra.Command{employmentAddCmd, employmentSetCmd} { + command.Flags().Int64Var(&employmentPersonID, "person", 0, "Person ID") + command.Flags().Int64Var(&employmentOrganizationID, "organization", 0, "Organization ID") + command.Flags().StringVar(&employmentTitle, "title", "", "Employment title") + command.Flags().StringVar(&employmentRole, "role", "", "Employment role") + command.Flags().StringVar(&employmentDepartment, "department", "", "Department") + command.Flags().StringVar(&employmentLocation, "location", "", "Location") + command.Flags().StringVar(&employmentDescription, "description", "", "Description") + command.Flags().StringVar(&employmentStartDate, "start", "", "Partial start date") + command.Flags().StringVar(&employmentEndDate, "end", "", "Partial end date") + command.Flags().StringVar(&employmentSource, "source", "", "Value source") + command.Flags().BoolVar(&employmentPrimary, "primary", false, "Make primary") + command.Flags().BoolVar(&employmentNoPrimary, "no-primary", false, "Do not make primary") + command.Flags().BoolVar(&employmentNotCurrent, "not-current", false, "Mark not current") + } + employmentEndCmd.Flags().StringVar(&employmentEndDate, "end-date", "", "Partial end date") + employmentListCmd.Flags().Int64Var(&employmentPersonID, "person", 0, "Person ID") + employmentListCmd.Flags().Int64Var(&employmentOrganizationID, "organization", 0, "Organization ID") + employmentListCmd.Flags().BoolVar(&employmentCurrentOnly, "current-only", false, "Only current employments") +} diff --git a/cmd/msgvault/cmd/employment_test.go b/cmd/msgvault/cmd/employment_test.go new file mode 100644 index 000000000..988a842fe --- /dev/null +++ b/cmd/msgvault/cmd/employment_test.go @@ -0,0 +1,147 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/config" +) + +func TestEmploymentAddSendsPartialDatesAndOmitsUndecidedPrimary(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var body map[string]any + var decodeErr error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(http.MethodPost, r.Method) + assert.Equal("/api/v1/employments", r.URL.Path) + decoder := json.NewDecoder(r.Body) + decoder.UseNumber() + decodeErr = decoder.Decode(&body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, err := w.Write([]byte(`{"id":9,"person_id":3,"organization_id":4,"title":"Staff Engineer","start_date":{"year":2019,"month":4},"is_current":true,"is_primary":true,"source":"user","revision":1,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runEmploymentCommand(t, employmentAddCmd, []string{"--person", "3", "--organization", "4", "--title", "Staff Engineer", "--start", "2019-04"}) + require.NoError(decodeErr) + assert.Equal(json.Number("3"), body["person_id"]) + assert.Equal(json.Number("4"), body["organization_id"]) + assert.Equal("2019-04", body["start_date"]) + _, hasPrimary := body["is_primary"] + assert.False(hasPrimary) + assert.Contains(output, "Employment: 9") + assert.Contains(output, "Title: Staff Engineer") + assert.Contains(output, "Primary: true") +} + +func TestEmploymentAddRejectsConflictingPrimaryFlags(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + command := cloneEmploymentCommand(employmentAddCmd) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs([]string{"--person", "3", "--organization", "4", "--primary", "--no-primary"}) + err := command.Execute() + require.Error(err) + assert.Contains(err.Error(), "--primary and --no-primary are mutually exclusive") +} + +func TestEmploymentEndReadsRevisionAndSendsEndDate(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var ifMatch string + var body map[string]any + var decodeErr error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case r.Method == http.MethodGet && r.URL.Path == "/api/v1/employments/9": + _, err := w.Write([]byte(`{"id":9,"person_id":3,"organization_id":4,"title":"Staff Engineer","is_current":true,"is_primary":true,"source":"user","revision":2,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + case r.Method == http.MethodPost && r.URL.Path == "/api/v1/employments/9/end": + ifMatch = r.Header.Get("If-Match") + decodeErr = json.NewDecoder(r.Body).Decode(&body) + _, err := w.Write([]byte(`{"id":9,"person_id":3,"organization_id":4,"title":"Staff Engineer","end_date":{"year":2026,"month":6},"is_current":false,"is_primary":false,"source":"user","revision":3,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runEmploymentCommand(t, employmentEndCmd, []string{"9", "--end-date", "2026-06"}) + require.NoError(decodeErr) + assert.Equal(`"employment-9-r2"`, ifMatch) + assert.Equal("2026-06", body["end_date"]) + assert.Contains(output, "Current: false") + assert.Contains(output, "Primary: false") + assert.Contains(output, "End date: 2026-06") +} + +func TestEmploymentListPrintsHistoryAndProjection(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("/api/v1/persons/3/employments", r.URL.Path) + assert.Contains(r.URL.RawQuery, "current_only=false") + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"employments":[{"id":9,"person_id":3,"organization_id":4,"title":"Staff Engineer","is_current":true,"is_primary":true,"source":"user","revision":1,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"},{"id":8,"person_id":3,"organization_id":5,"title":"Junior Engineer","start_date":{"year":2015},"end_date":{"year":2018,"month":6},"is_current":false,"is_primary":false,"source":"user","revision":1,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}],"projection":{"employment_id":9,"organization_id":4,"organization_name":"Example Org","title":"Staff Engineer","role":"Engineering","department":"Archive Platform","vcard":{"org":["Example Org","Archive Platform"],"title":"Staff Engineer","role":"Engineering"}}}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runEmploymentCommand(t, employmentListCmd, []string{"--person", "3"}) + assert.Contains(output, "Staff Engineer") + assert.Contains(output, "Junior Engineer") + assert.Contains(output, "2018-06") + assert.Contains(output, "Current company: Example Org") + assert.Contains(output, "Current title: Staff Engineer") + assert.Contains(output, "vCard ORG: Example Org;Archive Platform") + require.NotContains(output, "") +} + +func TestEmploymentListRequiresPersonOrOrganization(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + command := cloneEmploymentCommand(employmentListCmd) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs(nil) + err := command.Execute() + require.Error(err) + assert.Contains(err.Error(), "--person or --organization is required") +} +func cloneEmploymentCommand(template *cobra.Command) *cobra.Command { + command := &cobra.Command{Use: template.Use, Args: template.Args, RunE: template.RunE} + command.Flags().AddFlagSet(template.Flags()) + command.Flags().VisitAll(func(flag *pflag.Flag) { + _ = flag.Value.Set(flag.DefValue) + flag.Changed = false + }) + return command +} +func runEmploymentCommand(t *testing.T, template *cobra.Command, args []string) string { + t.Helper() + saved := employmentJSON + employmentJSON = false + t.Cleanup(func() { employmentJSON = saved }) + command := cloneEmploymentCommand(template) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs(args) + require.NoError(t, command.Execute()) + return output.String() +} diff --git a/cmd/msgvault/cmd/organization.go b/cmd/msgvault/cmd/organization.go new file mode 100644 index 000000000..c066a5d95 --- /dev/null +++ b/cmd/msgvault/cmd/organization.go @@ -0,0 +1,611 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + "go.kenn.io/msgvault/internal/daemonclient" + apiclient "go.kenn.io/msgvault/pkg/client" + "go.kenn.io/msgvault/pkg/client/generated" +) + +var ( + organizationJSON bool + organizationLimit, organizationOffset int64 + organizationQuery, organizationName, organizationKind, organizationDomain, organizationDescription string + organizationShowHistory, organizationIncludeRetired, organizationCurrentOnly, organizationIncludeSuperseded bool + organizationTextValue, organizationSource, organizationDefinitionSlugValue string + organizationDuplicateStatus, organizationDuplicateNote string + organizationExpectedValueID int64 + organizationDryRun bool +) + +var organizationCmd = &cobra.Command{Use: "organization", Aliases: []string{"org"}, Short: "Manage curated organizations and their employment records"} + +var organizationListCmd = &cobra.Command{Use: cmdUseList, Short: "List curated organizations", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + query := &generated.ListOrganizationsQuery{} + if cmd.Flags().Changed("limit") { + query.Limit = &organizationLimit + } + if cmd.Flags().Changed("offset") { + query.Offset = &organizationOffset + } + if cmd.Flags().Changed("query") { + query.Q = &organizationQuery + } + if cmd.Flags().Changed("include-retired") { + query.IncludeRetired = &organizationIncludeRetired + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ListOrganizationsResp, error) { + return api.ListOrganizationsWithResponse(cmd.Context(), &generated.ListOrganizationsRequestOptions{Query: query}) + }) + if err != nil { + return err + } + if resp.JSON200 == nil { + return errors.New("organization list response was empty") + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(resp.JSON200) + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tKIND\tDOMAIN\tSTATUS\tREVISION") + for _, org := range resp.JSON200.Organizations { + status := "active" + if org.RetiredAt != nil { + status = "retired" + } + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%d\n", org.ID, org.Name, org.Kind, cliString(org.PrimaryDomain), status, org.Revision) + } + return w.Flush() +}} + +var organizationCreateCmd = &cobra.Command{Use: "create ", Short: "Create a curated organization", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + name := strings.TrimSpace(args[0]) + if name == "" { + return usageErr(cmd, errors.New("name must not be empty")) + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + body := generated.CreateOrganizationBody{Name: name} + if cmd.Flags().Changed("kind") { + kind := generated.OrganizationCreateBodyKind(organizationKind) + body.Kind = &kind + } + if cmd.Flags().Changed("domain") { + body.PrimaryDomain = &organizationDomain + } + if cmd.Flags().Changed("description") { + body.Description = &organizationDescription + } + resp, err := daemonclient.APIResponseWithStatuses(client, []int{http.StatusCreated}, func(api *apiclient.Client) (*generated.CreateOrganizationResp, error) { + return api.CreateOrganizationWithResponse(cmd.Context(), &generated.CreateOrganizationRequestOptions{Body: &body}) + }) + if err != nil { + return err + } + return writeCLIOrganization(cmd, resp.JSON201) +}} + +var organizationShowCmd = &cobra.Command{Use: "show ", Short: "Show an organization", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + if organizationShowHistory { + resp, getErr := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.GetOrganizationHistoryResp, error) { + return api.GetOrganizationHistoryWithResponse(cmd.Context(), &generated.GetOrganizationHistoryRequestOptions{PathParams: &generated.GetOrganizationHistoryPath{ID: id}}) + }) + if getErr != nil { + return getErr + } + if resp.JSON200 == nil { + return errors.New("organization history response was empty") + } + return writeCLIOrganizationHistory(cmd, resp.JSON200) + } + resp, err := getCLIOrganization(cmd, client, id) + if err != nil { + return err + } + if resp.JSON200 == nil { + return errors.New("organization response was empty") + } + return writeCLIOrganizationProfile(cmd, resp.JSON200) +}} + +var organizationSetCmd = &cobra.Command{Use: "set ", Short: "Replace an organization's mutable fields", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if !cmd.Flags().Changed("name") { + return usageErr(cmd, errors.New("--name is required")) + } + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + if strings.TrimSpace(organizationName) == "" { + return usageErr(cmd, errors.New("name must not be empty")) + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIOrganization(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("organization response was empty") + } + body := generated.PatchOrganizationBody{Name: organizationName} + if cmd.Flags().Changed("kind") { + kind := generated.OrganizationBodyKind(organizationKind) + body.Kind = &kind + } else { + kind := generated.OrganizationBodyKind(current.JSON200.Organization.Kind) + body.Kind = &kind + } + if cmd.Flags().Changed("domain") { + body.PrimaryDomain = &organizationDomain + } else { + body.PrimaryDomain = current.JSON200.Organization.PrimaryDomain + } + if cmd.Flags().Changed("description") { + body.Description = &organizationDescription + } else { + body.Description = current.JSON200.Organization.Description + } + retired := current.JSON200.Organization.RetiredAt != nil + body.Retired = &retired + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.PatchOrganizationResp, error) { + return api.PatchOrganizationWithResponse(cmd.Context(), &generated.PatchOrganizationRequestOptions{PathParams: &generated.PatchOrganizationPath{ID: id}, Header: &generated.PatchOrganizationHeaders{IfMatch: organizationETag(id, current.JSON200.Organization.Revision)}, Body: &body}) + }) + if err != nil { + return err + } + return writeCLIOrganization(cmd, resp.JSON200) +}} + +func runOrganizationRetired(retired bool) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIOrganization(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("organization response was empty") + } + kind := generated.OrganizationBodyKind(current.JSON200.Organization.Kind) + body := generated.PatchOrganizationBody{ + Name: current.JSON200.Organization.Name, Kind: &kind, + PrimaryDomain: current.JSON200.Organization.PrimaryDomain, + Description: current.JSON200.Organization.Description, + Retired: &retired, + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.PatchOrganizationResp, error) { + return api.PatchOrganizationWithResponse(cmd.Context(), &generated.PatchOrganizationRequestOptions{PathParams: &generated.PatchOrganizationPath{ID: id}, Header: &generated.PatchOrganizationHeaders{IfMatch: organizationETag(id, current.JSON200.Organization.Revision)}, Body: &body}) + }) + if err != nil { + return err + } + return writeCLIOrganization(cmd, resp.JSON200) + } +} + +var organizationRetireCmd = &cobra.Command{Use: "retire ", Short: "Retire an organization", Args: cobra.ExactArgs(1), RunE: runOrganizationRetired(true)} +var organizationUnretireCmd = &cobra.Command{Use: "unretire ", Short: "Unretire an organization", Args: cobra.ExactArgs(1), RunE: runOrganizationRetired(false)} + +var organizationDeleteCmd = &cobra.Command{Use: "delete ", Short: "Permanently delete an organization", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIOrganization(cmd, client, id) + if err != nil { + return err + } + if current.JSON200 == nil { + return errors.New("organization response was empty") + } + _, err = daemonclient.APIResponseWithStatuses(client, []int{http.StatusNoContent}, func(api *apiclient.Client) (*generated.DeleteOrganizationResp, error) { + return api.DeleteOrganizationWithResponse(cmd.Context(), &generated.DeleteOrganizationRequestOptions{PathParams: &generated.DeleteOrganizationPath{ID: id}, Header: &generated.DeleteOrganizationHeaders{IfMatch: organizationETag(id, current.JSON200.Organization.Revision)}}) + }) + if err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Deleted organization %d\n", id) + return nil +}} + +var organizationMergeCmd = &cobra.Command{Use: "merge ", Short: "Merge a losing organization into a survivor", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { + survivor, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + losing, err := positivePersonCLIArg(cmd, args[1], "organization") + if err != nil { + return err + } + if survivor == losing { + return usageErr(cmd, errors.New("survivor and losing organization must differ")) + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + current, err := getCLIOrganization(cmd, client, survivor) + if err != nil { + return err + } + loser, err := getCLIOrganization(cmd, client, losing) + if err != nil { + return err + } + if current.JSON200 == nil || loser.JSON200 == nil { + return errors.New("organization response was empty") + } + body := generated.MergeOrganizationBody{LosingOrganizationID: losing, LosingRevision: loser.JSON200.Organization.Revision} + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.MergeOrganizationResp, error) { + return api.MergeOrganizationWithResponse(cmd.Context(), &generated.MergeOrganizationRequestOptions{PathParams: &generated.MergeOrganizationPath{ID: survivor}, Header: &generated.MergeOrganizationHeaders{IfMatch: organizationETag(survivor, current.JSON200.Organization.Revision)}, Body: &body}) + }) + if err != nil { + return err + } + return writeCLIOrganization(cmd, resp.JSON200) +}} + +var organizationEmploymentsCmd = &cobra.Command{Use: "employments ", Short: "List an organization's employment records", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + query := &generated.ListOrganizationEmploymentsQuery{} + if cmd.Flags().Changed("current-only") { + query.CurrentOnly = &organizationCurrentOnly + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ListOrganizationEmploymentsResp, error) { + return api.ListOrganizationEmploymentsWithResponse(cmd.Context(), &generated.ListOrganizationEmploymentsRequestOptions{PathParams: &generated.ListOrganizationEmploymentsPath{ID: id}, Query: query}) + }) + if err != nil { + return err + } + if resp.JSON200 == nil { + return errors.New("organization employments response was empty") + } + return writeCLIEmploymentList(cmd, resp.JSON200, false, organizationJSON) +}} + +var organizationAttributeCmd = &cobra.Command{Use: "attribute", Short: "Inspect and set organization attributes"} +var organizationAttributeListCmd = &cobra.Command{Use: "list ", Short: "List organization attribute values", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + query := &generated.ListOrganizationAttributesQuery{} + if cmd.Flags().Changed("include-superseded") { + query.IncludeSuperseded = &organizationIncludeSuperseded + } + if cmd.Flags().Changed("definition") { + query.DefinitionSlug = &organizationDefinitionSlugValue + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ListOrganizationAttributesResp, error) { + return api.ListOrganizationAttributesWithResponse(cmd.Context(), &generated.ListOrganizationAttributesRequestOptions{PathParams: &generated.ListOrganizationAttributesPath{ID: id}, Query: query}) + }) + if err != nil { + return err + } + if resp.JSON200 == nil { + return errors.New("organization attributes response was empty") + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(resp.JSON200) + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "SLUG\tORDINAL\tVALUE\tSOURCE\tACTIVE FROM\tACTIVE UNTIL") + for _, value := range resp.JSON200.Values { + _, _ = fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\t%s\n", value.DefinitionSlug, value.Ordinal, formatCLIAttributeValue(value.Value), value.Source, value.ActiveFrom.Format("2006-01-02T15:04:05Z07:00"), formatCLIOptionalTime(value.ActiveUntil)) + } + return w.Flush() +}} +var organizationAttributeSetCmd = &cobra.Command{Use: "set ", Short: "Set a text organization attribute", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "organization") + if err != nil { + return err + } + if !cmd.Flags().Changed("definition") { + return usageErr(cmd, errors.New("--definition is required")) + } + if !cmd.Flags().Changed("text") { + return usageErr(cmd, errors.New("--text is required")) + } + source := strings.TrimSpace(organizationSource) + if source == "" { + source = "user" + } + typedSource := generated.SetOrganizationAttributeBodySource(source) + value := generated.AttributeValue{Type: "text", Text: &organizationTextValue} + body := generated.SetOrganizationAttributeBody{DefinitionSlug: organizationDefinitionSlugValue, Source: typedSource, Value: value} + if cmd.Flags().Changed("expected-value-id") { + if organizationExpectedValueID <= 0 { + return usageErr(cmd, errors.New("--expected-value-id must be a positive integer")) + } + body.ExpectedValueID = &organizationExpectedValueID + } + if organizationDryRun { + body.DryRun = &organizationDryRun + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + resp, err := daemonclient.APIResponseWithStatuses(client, []int{http.StatusOK, http.StatusCreated}, func(api *apiclient.Client) (*generated.SetOrganizationAttributeResp, error) { + return api.SetOrganizationAttributeWithResponse(cmd.Context(), &generated.SetOrganizationAttributeRequestOptions{PathParams: &generated.SetOrganizationAttributePath{ID: id}, Body: &body}) + }) + if err != nil { + return err + } + write := resp.JSON200 + if write == nil { + write = resp.JSON201 + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(write) + } + return writeCLIOrganizationAttribute(cmd, write) +}} + +var organizationDuplicatesCmd = &cobra.Command{Use: "duplicates", Short: "Review organization duplicate suggestions"} +var organizationDuplicatesListCmd = &cobra.Command{Use: cmdUseList, Short: "List duplicate suggestions", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + query := &generated.ListOrganizationDuplicateSuggestionsQuery{} + if cmd.Flags().Changed("status") { + query.Status = &organizationDuplicateStatus + } + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ListOrganizationDuplicateSuggestionsResp, error) { + return api.ListOrganizationDuplicateSuggestionsWithResponse(cmd.Context(), &generated.ListOrganizationDuplicateSuggestionsRequestOptions{Query: query}) + }) + if err != nil { + return err + } + if resp.JSON200 == nil { + return errors.New("duplicate suggestions response was empty") + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(resp.JSON200) + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tORGANIZATION A\tORGANIZATION B\tCRITERION\tSTATUS\tCONFIDENCE") + for _, row := range resp.JSON200.Suggestions { + _, _ = fmt.Fprintf(w, "%d\t%d\t%d\t%s\t%s\t%.2f\n", row.ID, row.OrganizationAId, row.OrganizationBId, row.Criterion, row.Status, row.Confidence) + } + return w.Flush() +}} +var organizationDuplicatesRefreshCmd = &cobra.Command{Use: "refresh", Short: "Refresh duplicate suggestions", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.RefreshOrganizationDuplicateSuggestionsResp, error) { + return api.RefreshOrganizationDuplicateSuggestionsWithResponse(cmd.Context()) + }) + if err != nil { + return err + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(resp.JSON200) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Duplicate suggestions refreshed") + return nil +}} +var organizationDuplicatesResolveCmd = &cobra.Command{Use: "resolve ", Short: "Resolve a duplicate suggestion", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + id, err := positivePersonCLIArg(cmd, args[0], "duplicate suggestion") + if err != nil { + return err + } + if organizationDuplicateStatus != "accepted" && organizationDuplicateStatus != "rejected" { + return usageErr(cmd, errors.New("status must be accepted or rejected")) + } + status := generated.ResolveOrganizationDuplicateSuggestionBodyStatus(organizationDuplicateStatus) + body := generated.ResolveOrganizationDuplicateSuggestionBody{Status: status} + if cmd.Flags().Changed("note") { + body.Note = &organizationDuplicateNote + } + client, _, err := OpenHTTPStore(cmd.Context()) + if err != nil { + return err + } + defer func() { _ = client.Close() }() + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.ResolveOrganizationDuplicateSuggestionResp, error) { + return api.ResolveOrganizationDuplicateSuggestionWithResponse(cmd.Context(), &generated.ResolveOrganizationDuplicateSuggestionRequestOptions{PathParams: &generated.ResolveOrganizationDuplicateSuggestionPath{ID: id}, Body: &body}) + }) + if err != nil { + return err + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(resp.JSON200) + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Resolved duplicate suggestion %d: %s\n", id, resp.JSON200.Status) + return nil +}} + +func getCLIOrganization(cmd *cobra.Command, client *daemonclient.Client, id int64) (*generated.GetOrganizationResp, error) { + resp, err := daemonclient.APIResponse(client, func(api *apiclient.Client) (*generated.GetOrganizationResp, error) { + return api.GetOrganizationWithResponse(cmd.Context(), &generated.GetOrganizationRequestOptions{PathParams: &generated.GetOrganizationPath{ID: id}}) + }) + if err != nil || resp == nil || resp.JSON200 == nil || resp.JSON200.Organization.ID != 0 { + return resp, err + } + // The generated contract returns an OrganizationProfile. Retain compatibility + // with a bare Organization response from older daemons while callers migrate. + var organization generated.Organization + if unmarshalErr := json.Unmarshal(resp.Body, &organization); unmarshalErr != nil { + return nil, fmt.Errorf("decode organization response: %w", unmarshalErr) + } + resp.JSON200.Organization = organization + return resp, nil +} +func organizationETag(id, revision int64) string { + return fmt.Sprintf(`"organization-%d-r%d"`, id, revision) +} +func cliString(v *string) string { + if v == nil || *v == "" { + return "-" + } + return *v +} +func writeCLIOrganization(cmd *cobra.Command, org *generated.Organization) error { + if org == nil { + return errors.New("organization response was empty") + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(org) + } + status := "active" + if org.RetiredAt != nil { + status = "retired" + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Organization: %d\nName: %s\nKind: %s\nPrimary domain: %s\nDescription: %s\nStatus: %s\nRevision: %d\n", org.ID, org.Name, org.Kind, cliString(org.PrimaryDomain), cliString(org.Description), status, org.Revision) + return nil +} +func writeCLIOrganizationProfile(cmd *cobra.Command, profile *generated.OrganizationProfile) error { + if profile == nil { + return errors.New("organization response was empty") + } + if organizationJSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(profile) + } + if err := writeCLIOrganization(cmd, &profile.Organization); err != nil { + return err + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Names: %d\nIdentifiers: %d\nAddresses: %d\nContact points: %d\nMedia: %d\nCategories: %d\n", len(profile.Names), len(profile.Identifiers), len(profile.Addresses), len(profile.ContactPoints), len(profile.Media), len(profile.Categories)) + return nil +} + +func writeCLIOrganizationHistory(cmd *cobra.Command, profile *generated.OrganizationProfile) error { + if err := writeCLIOrganizationProfile(cmd, profile); err != nil || organizationJSON { + return err + } + for _, name := range profile.Names { + if name.Envelope.ActiveUntil != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Name: %s (active until %s)\n", name.Name, formatCLIOptionalTime(name.Envelope.ActiveUntil)) + } + } + for _, identifier := range profile.Identifiers { + if identifier.Envelope.ActiveUntil != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Identifier: %s (active until %s)\n", identifier.IdentifierValue, formatCLIOptionalTime(identifier.Envelope.ActiveUntil)) + } + } + for _, address := range profile.Addresses { + if address.Envelope.ActiveUntil != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Address: %s (active until %s)\n", address.OriginalValue, formatCLIOptionalTime(address.Envelope.ActiveUntil)) + } + } + for _, point := range profile.ContactPoints { + if point.Envelope.ActiveUntil != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Contact point: %s (active until %s)\n", point.OriginalValue, formatCLIOptionalTime(point.Envelope.ActiveUntil)) + } + } + for _, medium := range profile.Media { + if medium.Envelope.ActiveUntil != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Media: %s (active until %s)\n", medium.OriginalValue, formatCLIOptionalTime(medium.Envelope.ActiveUntil)) + } + } + for _, category := range profile.Categories { + if category.Envelope.ActiveUntil != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Category: %s (active until %s)\n", category.Category, formatCLIOptionalTime(category.Envelope.ActiveUntil)) + } + } + return nil +} +func writeCLIOrganizationAttribute(cmd *cobra.Command, write *generated.OrganizationAttributeWrite) error { + if write == nil { + return errors.New("organization attribute response was empty") + } + if write.Value != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Set %s: %s\n", write.Value.DefinitionSlug, formatCLIAttributeValue(write.Value.Value)) + } + return nil +} + +func init() { + rootCmd.AddCommand(organizationCmd) + organizationCmd.AddCommand(organizationListCmd, organizationCreateCmd, organizationShowCmd, organizationSetCmd, organizationRetireCmd, organizationUnretireCmd, organizationDeleteCmd, organizationMergeCmd, organizationEmploymentsCmd, organizationAttributeCmd, organizationDuplicatesCmd) + organizationAttributeCmd.AddCommand(organizationAttributeListCmd, organizationAttributeSetCmd) + organizationDuplicatesCmd.AddCommand(organizationDuplicatesListCmd, organizationDuplicatesRefreshCmd, organizationDuplicatesResolveCmd) + for _, command := range []*cobra.Command{organizationListCmd, organizationCreateCmd, organizationShowCmd, organizationSetCmd, organizationRetireCmd, organizationUnretireCmd, organizationMergeCmd, organizationEmploymentsCmd, organizationAttributeListCmd, organizationAttributeSetCmd, organizationDuplicatesListCmd, organizationDuplicatesRefreshCmd, organizationDuplicatesResolveCmd} { + command.Flags().BoolVar(&organizationJSON, flagJSON, false, "Output as JSON") + } + organizationListCmd.Flags().Int64Var(&organizationLimit, "limit", 0, "Maximum results") + organizationListCmd.Flags().Int64Var(&organizationOffset, "offset", 0, "Results to skip") + organizationListCmd.Flags().StringVar(&organizationQuery, "query", "", "Normalized-name search") + organizationListCmd.Flags().BoolVar(&organizationIncludeRetired, "include-retired", false, "Include retired organizations") + for _, command := range []*cobra.Command{organizationCreateCmd, organizationSetCmd} { + command.Flags().StringVar(&organizationKind, "kind", "", "Organization kind") + command.Flags().StringVar(&organizationDomain, "domain", "", "Primary domain") + command.Flags().StringVar(&organizationDescription, "description", "", "Description") + } + organizationSetCmd.Flags().StringVar(&organizationName, "name", "", "Organization name") + organizationShowCmd.Flags().BoolVar(&organizationShowHistory, "history", false, "Include superseded profile rows") + organizationEmploymentsCmd.Flags().BoolVar(&organizationCurrentOnly, "current-only", false, "Only current employments") + organizationAttributeListCmd.Flags().BoolVar(&organizationIncludeSuperseded, "include-superseded", false, "Include superseded values") + organizationAttributeListCmd.Flags().StringVar(&organizationDefinitionSlugValue, "definition", "", "Restrict to definition slug") + organizationAttributeSetCmd.Flags().StringVar(&organizationDefinitionSlugValue, "definition", "", "Definition slug") + organizationAttributeSetCmd.Flags().StringVar(&organizationTextValue, "text", "", "Text value") + organizationAttributeSetCmd.Flags().StringVar(&organizationSource, "source", "", "Value source") + organizationAttributeSetCmd.Flags().Int64Var( + &organizationExpectedValueID, "expected-value-id", 0, + "Expected current value ID for compare-and-swap") + organizationAttributeSetCmd.Flags().BoolVar(&organizationDryRun, "dry-run", false, "Validate without writing") + organizationDuplicatesListCmd.Flags().StringVar(&organizationDuplicateStatus, "status", "", "Suggestion status") + organizationDuplicatesResolveCmd.Flags().StringVar(&organizationDuplicateStatus, "status", "", "accepted or rejected") + organizationDuplicatesResolveCmd.Flags().StringVar(&organizationDuplicateNote, "note", "", "Resolution note") +} diff --git a/cmd/msgvault/cmd/organization_test.go b/cmd/msgvault/cmd/organization_test.go new file mode 100644 index 000000000..47b70d014 --- /dev/null +++ b/cmd/msgvault/cmd/organization_test.go @@ -0,0 +1,304 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/config" +) + +func TestOrganizationCreateSendsNormalizedBodyAndPrintsResult(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var received struct { + Name string `json:"name"` + Kind string `json:"kind"` + PrimaryDomain *string `json:"primary_domain"` + } + var decodeErr error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(http.MethodPost, r.Method) + assert.Equal("/api/v1/organizations", r.URL.Path) + decodeErr = json.NewDecoder(r.Body).Decode(&received) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("ETag", `"organization-4-r1"`) + w.WriteHeader(http.StatusCreated) + _, err := w.Write([]byte(`{"id":4,"name":"Example Org","kind":"company","primary_domain":"example.com","revision":1,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runOrganizationCommand(t, organizationCreateCmd, []string{"Example Org", "--kind", "company", "--domain", "Example.com"}) + require.NoError(decodeErr) + assert.Equal("Example Org", received.Name) + assert.Equal("company", received.Kind) + require.NotNil(received.PrimaryDomain) + assert.Equal("Example.com", *received.PrimaryDomain) + assert.Contains(output, "Organization: 4") + assert.Contains(output, "Name: Example Org") + assert.Contains(output, "Primary domain: example.com") + assert.Contains(output, "Revision: 1") +} + +func TestOrganizationSetReadsCurrentRevisionAndSendsIfMatch(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var requests atomic.Int32 + var ifMatch string + var body map[string]any + var decodeErr error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests.Add(1) + assert.Equal("/api/v1/organizations/4", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + _, err := w.Write([]byte(`{"organization":{"id":4,"name":"Example Org","kind":"company","primary_domain":"example.com","description":"Synthetic description.","revision":3,"retired_at":"2026-07-29T12:00:00Z","created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}}`)) + assert.NoError(err) + case http.MethodPatch: + ifMatch = r.Header.Get("If-Match") + decodeErr = json.NewDecoder(r.Body).Decode(&body) + _, err := w.Write([]byte(`{"id":4,"name":"Example Group","kind":"company","primary_domain":"example.com","description":"Synthetic description.","revision":4,"retired_at":"2026-07-29T12:00:00Z","created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runOrganizationCommand(t, organizationSetCmd, []string{"4", "--name", "Example Group"}) + require.NoError(decodeErr) + assert.Equal(int32(2), requests.Load()) + assert.Equal(`"organization-4-r3"`, ifMatch) + assert.Equal("example.com", body["primary_domain"]) + assert.Equal("Synthetic description.", body["description"]) + assert.Equal(true, body["retired"]) + assert.Contains(output, "Name: Example Group") + assert.Contains(output, "Revision: 4") + require.NotEmpty(output) +} + +func TestOrganizationLifecycleCommandsPreserveRootFields(t *testing.T) { + for _, test := range []struct { + name string + command *cobra.Command + retiredAt string + want bool + }{ + {name: "retire", command: organizationRetireCmd, want: true}, + { + name: "unretire", command: organizationUnretireCmd, + retiredAt: `,"retired_at":"2026-07-29T12:00:00Z"`, want: false, + }, + } { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var body map[string]any + var decodeErr error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + _, err := fmt.Fprintf(w, + `{"organization":{"id":4,"name":"Example Org","kind":"company","primary_domain":"example.com","description":"Synthetic description.","revision":3%s,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}}`, + test.retiredAt) + assert.NoError(err) + case http.MethodPatch: + decodeErr = json.NewDecoder(r.Body).Decode(&body) + _, err := w.Write([]byte(`{"id":4,"name":"Example Org","kind":"company","primary_domain":"example.com","description":"Synthetic description.","revision":4,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{ + Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}, + }) + + runOrganizationCommand(t, test.command, []string{"4"}) + require.NoError(decodeErr) + assert.Equal("example.com", body["primary_domain"]) + assert.Equal("Synthetic description.", body["description"]) + assert.Equal(test.want, body["retired"]) + }) + } +} + +func TestOrganizationDeleteReportsEmploymentConflict(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + _, err := w.Write([]byte(`{"id":4,"name":"Example Org","kind":"company","revision":1,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}`)) + assert.NoError(err) + return + } + w.WriteHeader(http.StatusConflict) + _, err := w.Write([]byte(`{"error":"organization_has_employments","message":"Organization still has employment records; retire it instead of deleting it"}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + command := cloneOrganizationCommand(organizationDeleteCmd) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs([]string{"4"}) + err := command.Execute() + require.Error(err) + assert.Contains(err.Error(), "retire it instead of deleting it") +} + +func TestOrganizationListSendsQueryParametersAndRendersTable(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var rawQuery string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("/api/v1/organizations", r.URL.Path) + rawQuery = r.URL.RawQuery + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"organizations":[{"id":4,"name":"Example Org","kind":"company","primary_domain":"example.com","revision":2,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}],"total":1,"limit":25,"offset":0}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runOrganizationCommand(t, organizationListCmd, []string{"--limit", "25", "--query", "example", "--include-retired"}) + assert.Contains(rawQuery, "limit=25") + assert.Contains(rawQuery, "q=example") + assert.Contains(rawQuery, "include_retired=true") + assert.Contains(output, "ID") + assert.Contains(output, "Example Org") + assert.Contains(output, "example.com") + require.NotContains(output, "null") +} + +func TestOrganizationShowHistoryPrintsSupersededRows(t *testing.T) { + assert := assert.New(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("/api/v1/organizations/4/history", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"organization":{"id":4,"name":"Example Org","kind":"company","revision":2,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"},"names":[{"name":"Earlier Org","name_kind":"former","name_normalized":"earlier org","organization_id":4,"envelope":{"id":1,"ordinal":0,"source":"user","created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z","active_until":"2026-07-30T12:01:00Z"}}]}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}}) + output := runOrganizationCommand(t, organizationShowCmd, []string{"4", "--history"}) + assert.Contains(output, "Earlier Org") + assert.Contains(output, "active until 2026-07-30T12:01:00Z") +} + +func TestOrganizationDuplicatesResolveRequiresKnownStatus(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + command := cloneOrganizationCommand(organizationDuplicatesResolveCmd) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs([]string{"7", "--status", "maybe"}) + err := command.Execute() + require.Error(err) + assert.Contains(err.Error(), "status must be accepted or rejected") +} + +func TestOrganizationAttributeSetTransmitsOptionalExpectedValueID(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + var body map[string]any + var decodeErr error + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(http.MethodPost, r.Method) + assert.Equal("/api/v1/organizations/4/attributes", r.URL.Path) + decoder := json.NewDecoder(r.Body) + decoder.UseNumber() + decodeErr = decoder.Decode(&body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, err := w.Write([]byte(`{"value":{"id":8,"organization_id":4,"definition_id":2,"definition_slug":"industry_focus","ordinal":0,"value":{"type":"text","text":"information retrieval"},"active_from":"2026-07-30T12:00:00Z","created_at":"2026-07-30T12:00:00Z","source":"user"},"dry_run":false}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{ + Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}, + }) + + output := runOrganizationCommand(t, organizationAttributeSetCmd, []string{ + "4", "--definition", "industry_focus", "--text", "information retrieval", + "--expected-value-id", "7", + }) + require.NoError(decodeErr) + assert.Equal(json.Number("7"), body["expected_value_id"]) + assert.Contains(output, "Set industry_focus: information retrieval") +} + +func TestOrganizationEmploymentsJSONUsesOrganizationOutputFlag(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(http.MethodGet, r.Method) + assert.Equal("/api/v1/organizations/4/employments", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"employments":[{"id":9,"person_id":3,"organization_id":4,"title":"Staff Engineer","is_current":true,"is_primary":true,"source":"user","revision":1,"created_at":"2026-07-30T12:00:00Z","updated_at":"2026-07-30T12:00:00Z"}]}`)) + assert.NoError(err) + })) + t.Cleanup(server.Close) + withStoreResolverConfig(t, &config.Config{ + Remote: config.RemoteConfig{URL: server.URL, AllowInsecure: true}, + }) + + savedOrganizationJSON, savedEmploymentJSON := organizationJSON, employmentJSON + organizationJSON, employmentJSON = false, false + t.Cleanup(func() { + organizationJSON, employmentJSON = savedOrganizationJSON, savedEmploymentJSON + }) + command := cloneOrganizationCommand(organizationEmploymentsCmd) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs([]string{"4", "--json"}) + require.NoError(command.Execute()) + + require.True(json.Valid(output.Bytes()), output.String()) + var decoded struct { + Employments []struct { + ID int64 `json:"id"` + } `json:"employments"` + } + require.NoError(json.Unmarshal(output.Bytes(), &decoded)) + require.Len(decoded.Employments, 1) + assert.Equal(int64(9), decoded.Employments[0].ID) +} + +func cloneOrganizationCommand(template *cobra.Command) *cobra.Command { + command := &cobra.Command{Use: template.Use, Args: template.Args, RunE: template.RunE} + command.Flags().AddFlagSet(template.Flags()) + command.Flags().VisitAll(func(flag *pflag.Flag) { + _ = flag.Value.Set(flag.DefValue) + flag.Changed = false + }) + return command +} +func runOrganizationCommand(t *testing.T, template *cobra.Command, args []string) string { + t.Helper() + saved := organizationJSON + organizationJSON = false + t.Cleanup(func() { organizationJSON = saved }) + command := cloneOrganizationCommand(template) + var output bytes.Buffer + command.SetOut(&output) + command.SetErr(&output) + command.SetArgs(args) + require.NoError(t, command.Execute()) + return output.String() +} diff --git a/cmd/msgvault/cmd/serve.go b/cmd/msgvault/cmd/serve.go index b39115cd9..9b1a6a6e3 100644 --- a/cmd/msgvault/cmd/serve.go +++ b/cmd/msgvault/cmd/serve.go @@ -1100,6 +1100,8 @@ var _ api.PersonProfileValueStore = (*storeAPIAdapter)(nil) var _ api.CommunicationServiceStore = (*storeAPIAdapter)(nil) var _ api.AttributeDefinitionStore = (*storeAPIAdapter)(nil) var _ api.PersonAttributeStore = (*storeAPIAdapter)(nil) +var _ api.OrganizationStore = (*storeAPIAdapter)(nil) +var _ api.EmploymentStore = (*storeAPIAdapter)(nil) var _ api.IdentityCacheRefresher = (*storeAPIAdapter)(nil) var _ api.ClusterLookupStore = (*storeAPIAdapter)(nil) var _ api.ConversationWindowStore = (*storeAPIAdapter)(nil) @@ -1989,6 +1991,146 @@ func (a *storeAPIAdapter) SupersedePersonAttributeValueContext( return a.store.SupersedePersonAttributeValueContext(ctx, input) } +func (a *storeAPIAdapter) CreateOrganizationContext( + ctx context.Context, input store.OrganizationInput, +) (*store.Organization, error) { + return a.store.CreateOrganizationContext(ctx, input) +} + +func (a *storeAPIAdapter) GetOrganizationContext( + ctx context.Context, id int64, +) (*store.Organization, error) { + return a.store.GetOrganizationContext(ctx, id) +} + +func (a *storeAPIAdapter) ListOrganizationsContext( + ctx context.Context, filter store.OrganizationFilter, +) ([]store.Organization, error) { + return a.store.ListOrganizationsContext(ctx, filter) +} + +func (a *storeAPIAdapter) CountOrganizationsContext( + ctx context.Context, filter store.OrganizationFilter, +) (int64, error) { + return a.store.CountOrganizationsContext(ctx, filter) +} + +func (a *storeAPIAdapter) ReplaceOrganizationContext( + ctx context.Context, id, expectedRevision int64, input store.OrganizationInput, retired bool, +) (*store.Organization, error) { + return a.store.ReplaceOrganizationContext(ctx, id, expectedRevision, input, retired) +} + +func (a *storeAPIAdapter) DeleteOrganizationContext( + ctx context.Context, id, expectedRevision int64, +) error { + return a.store.DeleteOrganizationContext(ctx, id, expectedRevision) +} + +func (a *storeAPIAdapter) MergeOrganizationsContext( + ctx context.Context, survivorID, survivorRevision, losingID, losingRevision int64, +) (*store.Organization, error) { + return a.store.MergeOrganizationsContext( + ctx, survivorID, survivorRevision, losingID, losingRevision, + ) +} + +func (a *storeAPIAdapter) GetOrganizationProfileContext( + ctx context.Context, id int64, includeSuperseded bool, +) (*store.OrganizationProfile, error) { + return a.store.GetOrganizationProfileContext(ctx, id, includeSuperseded) +} + +func (a *storeAPIAdapter) ReplaceOrganizationProfileContext( + ctx context.Context, id, expectedRevision int64, input store.OrganizationProfileInput, +) (*store.OrganizationProfile, error) { + return a.store.ReplaceOrganizationProfileContext(ctx, id, expectedRevision, input) +} + +func (a *storeAPIAdapter) ListOrganizationAttributeValuesContext( + ctx context.Context, organizationID int64, query store.OrganizationAttributeQuery, +) ([]store.OrganizationAttributeValue, error) { + return a.store.ListOrganizationAttributeValuesContext(ctx, organizationID, query) +} + +func (a *storeAPIAdapter) SetOrganizationAttributeValueContext( + ctx context.Context, input store.OrganizationAttributeValueInput, +) (*store.OrganizationAttributeWrite, error) { + return a.store.SetOrganizationAttributeValueContext(ctx, input) +} + +func (a *storeAPIAdapter) SupersedeOrganizationAttributeValueContext( + ctx context.Context, input store.OrganizationAttributeSupersedeInput, +) (*store.OrganizationAttributeWrite, error) { + return a.store.SupersedeOrganizationAttributeValueContext(ctx, input) +} + +func (a *storeAPIAdapter) RefreshOrganizationDuplicateSuggestionsContext( + ctx context.Context, +) (int, error) { + return a.store.RefreshOrganizationDuplicateSuggestionsContext(ctx) +} + +func (a *storeAPIAdapter) ListOrganizationDuplicateSuggestionsContext( + ctx context.Context, status string, limit, offset int, +) ([]store.OrganizationDuplicateSuggestion, error) { + return a.store.ListOrganizationDuplicateSuggestionsContext(ctx, status, limit, offset) +} + +func (a *storeAPIAdapter) ResolveOrganizationDuplicateSuggestionContext( + ctx context.Context, id int64, status string, note *string, +) (*store.OrganizationDuplicateSuggestion, error) { + return a.store.ResolveOrganizationDuplicateSuggestionContext(ctx, id, status, note) +} + +func (a *storeAPIAdapter) AddEmploymentContext( + ctx context.Context, input store.EmploymentInput, +) (*store.Employment, error) { + return a.store.AddEmploymentContext(ctx, input) +} + +func (a *storeAPIAdapter) GetEmploymentContext( + ctx context.Context, id int64, +) (*store.Employment, error) { + return a.store.GetEmploymentContext(ctx, id) +} + +func (a *storeAPIAdapter) UpdateEmploymentContext( + ctx context.Context, id, expectedRevision int64, input store.EmploymentInput, +) (*store.Employment, error) { + return a.store.UpdateEmploymentContext(ctx, id, expectedRevision, input) +} + +func (a *storeAPIAdapter) EndEmploymentContext( + ctx context.Context, id, expectedRevision int64, endDate store.PartialDate, +) (*store.Employment, error) { + return a.store.EndEmploymentContext(ctx, id, expectedRevision, endDate) +} + +func (a *storeAPIAdapter) SetPrimaryEmploymentContext( + ctx context.Context, id, expectedRevision int64, +) (*store.Employment, error) { + return a.store.SetPrimaryEmploymentContext(ctx, id, expectedRevision) +} + +func (a *storeAPIAdapter) DeleteEmploymentContext( + ctx context.Context, id, expectedRevision int64, +) error { + return a.store.DeleteEmploymentContext(ctx, id, expectedRevision) +} + +func (a *storeAPIAdapter) ListEmploymentsContext( + ctx context.Context, filter store.EmploymentFilter, +) ([]store.Employment, error) { + return a.store.ListEmploymentsContext(ctx, filter) +} + +func (a *storeAPIAdapter) PrimaryCurrentEmploymentContext( + ctx context.Context, personID int64, +) (store.EmploymentProjection, bool, error) { + return a.store.PrimaryCurrentEmploymentContext(ctx, personID) +} + func (a *storeAPIAdapter) ClusterMembers(id int64) ([]int64, error) { return a.store.ClusterMembers(id) } diff --git a/cmd/msgvault/cmd/serve_api_wiring_test.go b/cmd/msgvault/cmd/serve_api_wiring_test.go index 0bf98a932..a380892e2 100644 --- a/cmd/msgvault/cmd/serve_api_wiring_test.go +++ b/cmd/msgvault/cmd/serve_api_wiring_test.go @@ -72,3 +72,37 @@ func TestStoreAPIAdapterServesProfileAndCommunicationServiceRoutes(t *testing.T) }) } } + +func TestStoreAPIAdapterServesOrganizationAndEmploymentRoutes(t *testing.T) { + requirements := require.New(t) + st := testutil.NewTestStore(t) + participantID, err := st.EnsureParticipantByIdentifier( + "email", "employment-adapter@example.test", "Employment Adapter", + ) + requirements.NoError(err) + person, _, err := st.CreatePersonFromParticipant(participantID) + requirements.NoError(err) + + srv := api.NewServerWithOptions(api.ServerOptions{ + Config: &config.Config{}, + Store: &storeAPIAdapter{store: st}, + Logger: slog.New(slog.DiscardHandler), + }) + + for _, test := range []struct { + name string + path string + }{ + {"organizations", "/api/v1/organizations"}, + {"employments", fmt.Sprintf("/api/v1/persons/%d/employments", person.ID)}, + } { + t.Run(test.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodGet, test.path, nil) + response := httptest.NewRecorder() + + srv.Router().ServeHTTP(response, request) + + require.Equal(t, http.StatusOK, response.Code, response.Body.String()) + }) + } +} diff --git a/internal/api/employments.go b/internal/api/employments.go new file mode 100644 index 000000000..ceb4ac185 --- /dev/null +++ b/internal/api/employments.go @@ -0,0 +1,544 @@ +package api + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/danielgtaylor/huma/v2" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/vcardmap" +) + +const employmentsPath = "/api/v1/employments" + +// EmploymentStore is the feature-local capability for temporal employment +// association records and the derived current-company projection. +type EmploymentStore interface { + AddEmploymentContext(ctx context.Context, input store.EmploymentInput) (*store.Employment, error) + GetEmploymentContext(ctx context.Context, id int64) (*store.Employment, error) + UpdateEmploymentContext(ctx context.Context, id, expectedRevision int64, input store.EmploymentInput) (*store.Employment, error) + EndEmploymentContext(ctx context.Context, id, expectedRevision int64, endDate store.PartialDate) (*store.Employment, error) + SetPrimaryEmploymentContext(ctx context.Context, id, expectedRevision int64) (*store.Employment, error) + DeleteEmploymentContext(ctx context.Context, id, expectedRevision int64) error + ListEmploymentsContext(ctx context.Context, filter store.EmploymentFilter) ([]store.Employment, error) + PrimaryCurrentEmploymentContext(ctx context.Context, personID int64) (store.EmploymentProjection, bool, error) +} + +// EmploymentBody is the full mutable field set. Partial dates are inbound +// truncated ISO 8601 strings while responses expose their known components. +type EmploymentBody struct { + PersonID int64 `json:"person_id"` + OrganizationID int64 `json:"organization_id"` + Title *string `json:"title,omitempty" nullable:"true"` + Role *string `json:"role,omitempty" nullable:"true"` + Department *string `json:"department,omitempty" nullable:"true"` + Location *string `json:"location,omitempty" nullable:"true"` + AddressID *int64 `json:"address_id,omitempty" nullable:"true"` + Description *string `json:"description,omitempty" nullable:"true"` + StartDate *string `json:"start_date,omitempty" nullable:"true"` + EndDate *string `json:"end_date,omitempty" nullable:"true"` + IsCurrent *bool `json:"is_current,omitempty" nullable:"true"` + IsPrimary *bool `json:"is_primary,omitempty" nullable:"true"` + Source string `json:"source" enum:"user,carddav_import,vcard_import,archive_observation,extraction,enrichment,system"` + SourceRef *string `json:"source_ref,omitempty" nullable:"true"` + Confidence *float64 `json:"confidence,omitempty" nullable:"true"` +} + +type EndEmploymentBody struct { + EndDate string `json:"end_date"` +} + +// EmploymentVCard is the vCard projection of the primary current employment. +type EmploymentVCard struct { + Org []string `json:"org,omitempty"` + Title string `json:"title,omitempty"` + Role string `json:"role,omitempty"` +} + +// EmploymentProjectionResponse is a read-time derived company and title. +type EmploymentProjectionResponse struct { + EmploymentID int64 `json:"employment_id"` + OrganizationID int64 `json:"organization_id"` + OrganizationName string `json:"organization_name"` + Title string `json:"title,omitempty"` + Role string `json:"role,omitempty"` + Department string `json:"department,omitempty"` + VCard EmploymentVCard `json:"vcard"` +} + +// EmploymentsResponse carries a listing and, only for people, their derived +// primary-current employment projection. +type EmploymentsResponse struct { + Employments []store.Employment `json:"employments"` + Projection *EmploymentProjectionResponse `json:"projection,omitempty"` +} + +func (s *Server) registerEmploymentRoutes(api huma.API) { + create := rawAPIV1Operation("createEmployment", http.MethodPost, "/employments", "Create an employment record") + create.RequestBody = jsonRequestBodyFor[EmploymentBody](api) + create.Responses = jsonResponsesFor[store.Employment](api, http.StatusCreated) + addEmploymentETagHeader(create.Responses[httpStatusKey(http.StatusCreated)]) + addEmploymentLocationHeader(create.Responses[httpStatusKey(http.StatusCreated)]) + addErrorResponses(api, create.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusServiceUnavailable) + registerRawHumaRoute(api, create, s.handleCreateEmployment) + + get := rawAPIV1Operation("getEmployment", http.MethodGet, "/employments/{id}", "Get an employment record") + addEmploymentIDParameter(&get) + get.Responses = jsonResponsesFor[store.Employment](api) + addEmploymentETagHeader(get.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, get.Responses, http.StatusBadRequest, http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, get, s.handleGetEmployment) + + patch := rawAPIV1Operation("patchEmployment", http.MethodPatch, "/employments/{id}", "Replace an employment record's mutable fields") + addEmploymentIDParameter(&patch) + addEmploymentIfMatchParameter(&patch) + patch.RequestBody = jsonRequestBodyFor[EmploymentBody](api) + patch.Responses = jsonResponsesFor[store.Employment](api) + addEmploymentETagHeader(patch.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, patch.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, patch, s.handlePatchEmployment) + + remove := rawAPIV1Operation("deleteEmployment", http.MethodDelete, "/employments/{id}", "Delete an employment record") + addEmploymentIDParameter(&remove) + addEmploymentIfMatchParameter(&remove) + remove.Responses = rawHumaResponses(http.StatusNoContent) + remove.Responses["default"] = errorResponseFor(api) + addErrorResponses(api, remove.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, remove, s.handleDeleteEmployment) + + end := rawAPIV1Operation("endEmployment", http.MethodPost, "/employments/{id}/end", "End an employment record") + addEmploymentIDParameter(&end) + addEmploymentIfMatchParameter(&end) + end.RequestBody = jsonRequestBodyFor[EndEmploymentBody](api) + end.Responses = jsonResponsesFor[store.Employment](api) + addEmploymentETagHeader(end.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, end.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, end, s.handleEndEmployment) + + primary := rawAPIV1Operation("setPrimaryEmployment", http.MethodPost, "/employments/{id}/primary", "Set the primary current employment") + addEmploymentIDParameter(&primary) + addEmploymentIfMatchParameter(&primary) + primary.Responses = jsonResponsesFor[store.Employment](api) + addEmploymentETagHeader(primary.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, primary.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, primary, s.handleSetPrimaryEmployment) + + personEmployments := rawAPIV1Operation("listPersonEmployments", http.MethodGet, "/persons/{id}/employments", "List a person's employment history") + personEmployments.Parameters = append(personEmployments.Parameters, pathIntegerParam("Person ID")) + personEmployments.Parameters = append(personEmployments.Parameters, employmentListParameters()...) + personEmployments.Responses = jsonResponsesFor[EmploymentsResponse](api) + addErrorResponses(api, personEmployments.Responses, http.StatusBadRequest, + http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, personEmployments, s.handleListPersonEmployments) + + organizationEmployments := rawAPIV1Operation("listOrganizationEmployments", http.MethodGet, "/organizations/{id}/employments", "List an organization's employment records") + addOrganizationIDParameter(&organizationEmployments) + organizationEmployments.Parameters = append(organizationEmployments.Parameters, employmentListParameters()...) + organizationEmployments.Responses = jsonResponsesFor[EmploymentsResponse](api) + addErrorResponses(api, organizationEmployments.Responses, http.StatusBadRequest, http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, organizationEmployments, s.handleListOrganizationEmployments) +} + +func employmentListParameters() []*huma.Param { + return []*huma.Param{ + queryBooleanParam("current_only", "Only current employments"), + queryIntegerParam("limit", "Maximum results"), + queryIntegerParam("offset", "Results to skip"), + } +} + +func (s *Server) handleCreateEmployment(w http.ResponseWriter, r *http.Request) { + employments, ok := s.employmentStore(w) + if !ok { + return + } + var body EmploymentBody + if _, ok := decodePersonRequestFields(w, r, &body); !ok { + return + } + input, err := employmentInputFromBody(body) + if err != nil { + s.writeEmploymentError(w, err) + return + } + employment, err := employments.AddEmploymentContext(r.Context(), input) + if err != nil { + s.writeEmploymentError(w, err) + return + } + writeEmployment(w, http.StatusCreated, employment) +} + +func (s *Server) handleGetEmployment(w http.ResponseWriter, r *http.Request) { + employments, ok := s.employmentStore(w) + if !ok { + return + } + id, ok := employmentID(w, r) + if !ok { + return + } + employment, err := employments.GetEmploymentContext(r.Context(), id) + if err != nil { + s.writeEmploymentError(w, err) + return + } + writeEmployment(w, http.StatusOK, employment) +} + +func (s *Server) handlePatchEmployment(w http.ResponseWriter, r *http.Request) { + employments, ok := s.employmentStore(w) + if !ok { + return + } + id, ok := employmentID(w, r) + if !ok { + return + } + revision, ok := employmentIfMatch(w, r, id) + if !ok { + return + } + var body EmploymentBody + if _, ok := decodePersonRequestFields(w, r, &body); !ok { + return + } + input, err := employmentInputFromBody(body) + if err != nil { + s.writeEmploymentError(w, err) + return + } + employment, err := employments.UpdateEmploymentContext(r.Context(), id, revision, input) + if err != nil { + s.writeEmploymentError(w, err) + return + } + writeEmployment(w, http.StatusOK, employment) +} + +func (s *Server) handleDeleteEmployment(w http.ResponseWriter, r *http.Request) { + employments, ok := s.employmentStore(w) + if !ok { + return + } + id, ok := employmentID(w, r) + if !ok { + return + } + revision, ok := employmentIfMatch(w, r, id) + if !ok { + return + } + if err := employments.DeleteEmploymentContext(r.Context(), id, revision); err != nil { + s.writeEmploymentError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (s *Server) handleEndEmployment(w http.ResponseWriter, r *http.Request) { + employments, ok := s.employmentStore(w) + if !ok { + return + } + id, ok := employmentID(w, r) + if !ok { + return + } + revision, ok := employmentIfMatch(w, r, id) + if !ok { + return + } + var body EndEmploymentBody + if _, ok := decodePersonRequestFields(w, r, &body); !ok { + return + } + endDate, err := store.ParsePartialDate(body.EndDate) + if err != nil { + s.writeEmploymentError(w, err) + return + } + employment, err := employments.EndEmploymentContext(r.Context(), id, revision, endDate) + if err != nil { + s.writeEmploymentError(w, err) + return + } + writeEmployment(w, http.StatusOK, employment) +} + +func (s *Server) handleSetPrimaryEmployment(w http.ResponseWriter, r *http.Request) { + employments, ok := s.employmentStore(w) + if !ok { + return + } + id, ok := employmentID(w, r) + if !ok { + return + } + revision, ok := employmentIfMatch(w, r, id) + if !ok { + return + } + employment, err := employments.SetPrimaryEmploymentContext(r.Context(), id, revision) + if err != nil { + s.writeEmploymentError(w, err) + return + } + writeEmployment(w, http.StatusOK, employment) +} + +func (s *Server) handleListPersonEmployments(w http.ResponseWriter, r *http.Request) { + profiles, ok := s.personProfileStore(w) + if !ok { + return + } + personID, ok := personProfileID(w, r) + if !ok { + return + } + if _, err := profiles.GetPersonContext(r.Context(), personID); err != nil { + s.writePersonError(w, err) + return + } + employments, ok := s.employmentStore(w) + if !ok { + return + } + filter, ok := s.employmentFilter(w, r, store.EmploymentFilter{PersonID: personID}) + if !ok { + return + } + rows, err := employments.ListEmploymentsContext(r.Context(), filter) + if err != nil { + s.writeEmploymentError(w, err) + return + } + projection, found, err := employments.PrimaryCurrentEmploymentContext(r.Context(), personID) + if err != nil { + s.writeEmploymentError(w, err) + return + } + response := EmploymentsResponse{Employments: rows} + if found { + response.Projection = employmentProjectionResponse(projection) + } + writeEmployments(w, response) +} + +func (s *Server) handleListOrganizationEmployments(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + organizationID, ok := organizationID(w, r) + if !ok { + return + } + if _, err := organizations.GetOrganizationContext(r.Context(), organizationID); err != nil { + s.writeOrganizationError(w, err) + return + } + employments, ok := s.employmentStore(w) + if !ok { + return + } + filter, ok := s.employmentFilter(w, r, store.EmploymentFilter{OrganizationID: organizationID}) + if !ok { + return + } + rows, err := employments.ListEmploymentsContext(r.Context(), filter) + if err != nil { + s.writeEmploymentError(w, err) + return + } + writeEmployments(w, EmploymentsResponse{Employments: rows}) +} + +func (s *Server) employmentFilter(w http.ResponseWriter, r *http.Request, filter store.EmploymentFilter) (store.EmploymentFilter, bool) { + filter.Limit = store.DefaultEmploymentPageSize + limit, present, err := queryInt(r, "limit") + if err != nil { + s.rejectBadParam(w, err) + return filter, false + } + if present { + if limit < 0 { + s.rejectBadParam(w, newParamError("limit", "limit must not be negative")) + return filter, false + } + if limit > 0 { + filter.Limit = min(limit, store.MaxEmploymentPageSize) + } + } + offset, present, err := queryInt(r, "offset") + if err != nil { + s.rejectBadParam(w, err) + return filter, false + } + if present { + if offset < 0 { + s.rejectBadParam(w, newParamError("offset", "offset must not be negative")) + return filter, false + } + filter.Offset = offset + } + currentOnly, _, err := queryBool(r, "current_only") + if err != nil { + s.rejectBadParam(w, err) + return filter, false + } + filter.CurrentOnly = currentOnly + return filter, true +} + +func employmentInputFromBody(body EmploymentBody) (store.EmploymentInput, error) { + source, err := store.ParseProvenance(body.Source) + if err != nil { + return store.EmploymentInput{}, err + } + input := store.EmploymentInput{ + PersonID: body.PersonID, OrganizationID: body.OrganizationID, + Title: body.Title, Role: body.Role, Department: body.Department, + Location: body.Location, AddressID: body.AddressID, Description: body.Description, + IsCurrent: body.IsCurrent, IsPrimary: body.IsPrimary, Source: source, + SourceRef: body.SourceRef, Confidence: body.Confidence, + } + if body.StartDate != nil { + startDate, err := store.ParsePartialDate(*body.StartDate) + if err != nil { + return input, err + } + input.StartDate = &startDate + } + if body.EndDate != nil { + endDate, err := store.ParsePartialDate(*body.EndDate) + if err != nil { + return input, err + } + input.EndDate = &endDate + } + return input, nil +} + +func employmentProjectionResponse(projection store.EmploymentProjection) *EmploymentProjectionResponse { + employment := vcardmap.FromProjection(projection) + return &EmploymentProjectionResponse{ + EmploymentID: projection.EmploymentID, OrganizationID: projection.OrganizationID, + OrganizationName: projection.OrganizationName, Title: projection.Title, + Role: projection.Role, Department: projection.Department, + VCard: EmploymentVCard{Org: vcardmap.OrgComponents(employment), Title: vcardmap.Title(employment), Role: vcardmap.Role(employment)}, + } +} + +func (s *Server) employmentStore(w http.ResponseWriter) (EmploymentStore, bool) { + employments, ok := s.store.(EmploymentStore) + if !ok { + writeError(w, http.StatusServiceUnavailable, "employments_unavailable", "Employments are unavailable") + } + return employments, ok +} + +func (s *Server) writeEmploymentError(w http.ResponseWriter, err error) { + if s.writeIfContextError(w, err) { + return + } + switch { + case errors.Is(err, store.ErrEmploymentNotFound): + writeError(w, http.StatusNotFound, "employment_not_found", "Employment not found") + case errors.Is(err, store.ErrEmploymentRevisionConflict): + writeError(w, http.StatusConflict, "employment_revision_conflict", "Employment changed; reload and retry") + case errors.Is(err, store.ErrEmploymentPrimaryConflict): + writeError(w, http.StatusConflict, "employment_primary_conflict", "Person already has a primary current employment") + case errors.Is(err, store.ErrEmploymentDuplicateActive): + writeError(w, http.StatusConflict, "employment_duplicate_active", "Person already has this current employment") + case errors.Is(err, store.ErrEmploymentInvalid): + writeError(w, http.StatusBadRequest, "invalid_employment", err.Error()) + case errors.Is(err, store.ErrInvalidPartialDate): + writeError(w, http.StatusBadRequest, "invalid_partial_date", err.Error()) + case errors.Is(err, store.ErrInvalidProvenance): + writeError(w, http.StatusBadRequest, "invalid_source", err.Error()) + case errors.Is(err, store.ErrPersonNotFound): + writeError(w, http.StatusBadRequest, "invalid_person_id", "Person ID is invalid") + case errors.Is(err, store.ErrOrganizationNotFound): + writeError(w, http.StatusBadRequest, "invalid_organization_id", "Organization ID is invalid") + case errors.Is(err, store.ErrOrganizationInvalid): + writeError(w, http.StatusBadRequest, "invalid_organization", err.Error()) + default: + s.logger.Error("employment operation failed", "error", err) + writeError(w, http.StatusInternalServerError, "employment_failed", "Employment operation failed") + } +} + +func writeEmployment(w http.ResponseWriter, status int, employment *store.Employment) { + w.Header().Set("ETag", employmentETag(*employment)) + w.Header().Set("Cache-Control", "no-store") + if status == http.StatusCreated { + w.Header().Set("Location", employmentsPath+"/"+strconv.FormatInt(employment.ID, 10)) + } + writeJSON(w, status, employment) +} + +func writeEmployments(w http.ResponseWriter, response EmploymentsResponse) { + if response.Employments == nil { + response.Employments = make([]store.Employment, 0) + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, response) +} + +func addEmploymentIDParameter(operation *huma.Operation) { + operation.Parameters = append(operation.Parameters, pathIntegerParam("Employment ID")) +} + +func addEmploymentIfMatchParameter(operation *huma.Operation) { + operation.Parameters = append(operation.Parameters, &huma.Param{Name: ifMatchHeaderName, In: "header", Required: true, Description: "Strong ETag returned by the latest employment read", Schema: &huma.Schema{Type: huma.TypeString}}) +} + +func addEmploymentETagHeader(response *huma.Response) { + response.Headers = map[string]*huma.Param{"ETag": {Description: "Strong employment revision tag for optimistic concurrency", Schema: &huma.Schema{Type: huma.TypeString}}} +} + +func addEmploymentLocationHeader(response *huma.Response) { + response.Headers["Location"] = &huma.Param{Description: "Canonical URL of the created employment record", Schema: &huma.Schema{Type: huma.TypeString}} +} + +func employmentID(w http.ResponseWriter, r *http.Request) (int64, bool) { + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || id <= 0 { + writeError(w, http.StatusBadRequest, "invalid_employment_id", "Employment ID must be a positive integer") + return 0, false + } + return id, true +} + +func employmentETag(employment store.Employment) string { + return fmt.Sprintf(`"employment-%d-r%d"`, employment.ID, employment.Revision) +} + +func employmentIfMatch(w http.ResponseWriter, r *http.Request, id int64) (int64, bool) { + values := r.Header.Values(ifMatchHeaderName) + if len(values) == 0 || (len(values) == 1 && strings.TrimSpace(values[0]) == "") { + writeError(w, http.StatusPreconditionRequired, "if_match_required", "If-Match is required") + return 0, false + } + if len(values) != 1 { + writeError(w, http.StatusBadRequest, "invalid_if_match", "If-Match must contain exactly one revision tag") + return 0, false + } + prefix := fmt.Sprintf(`"employment-%d-r`, id) + value := strings.TrimSpace(values[0]) + if !strings.HasPrefix(value, prefix) || !strings.HasSuffix(value, `"`) { + writeError(w, http.StatusBadRequest, "invalid_if_match", "If-Match is not an employment revision tag") + return 0, false + } + revision, err := strconv.ParseInt(strings.TrimSuffix(strings.TrimPrefix(value, prefix), `"`), 10, 64) + if err != nil || revision <= 0 { + writeError(w, http.StatusBadRequest, "invalid_if_match", "If-Match is not an employment revision tag") + return 0, false + } + return revision, true +} diff --git a/internal/api/employments_test.go b/internal/api/employments_test.go new file mode 100644 index 000000000..89eaf84ca --- /dev/null +++ b/internal/api/employments_test.go @@ -0,0 +1,296 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" +) + +func TestEmploymentHTTPLifecycleAndProjection(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "alice@example.com", "alice") + dayJob := mustAPIOrganization(t, st, "Example Org") + sideJob := mustAPIOrganization(t, st, "Another Org") + + createdResponse := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{ + "person_id":%d,"organization_id":%d, + "title":"Staff Engineer","role":"Engineering","department":"Archive Platform", + "start_date":"2019-04","source":"user" + }`, person.ID, dayJob.ID), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Employment + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + assert.True(created.IsCurrent) + assert.True(created.IsPrimary) + require.NotNil(created.StartDate) + assert.Equal("2019-04", created.StartDate.String()) + assert.Equal(fmt.Sprintf(`"employment-%d-r1"`, created.ID), + createdResponse.Header().Get("ETag")) + + personPath := fmt.Sprintf("/api/v1/persons/%d/employments", person.ID) + listResponse := organizationRequest(t, srv, http.MethodGet, personPath, nil, "") + require.Equal(http.StatusOK, listResponse.Code) + var listed EmploymentsResponse + require.NoError(json.Unmarshal(listResponse.Body.Bytes(), &listed)) + require.Len(listed.Employments, 1) + require.NotNil(listed.Projection) + assert.Equal("Example Org", listed.Projection.OrganizationName) + assert.Equal("Staff Engineer", listed.Projection.Title) + assert.Equal([]string{"Example Org", "Archive Platform"}, listed.Projection.VCard.Org) + assert.Equal("Staff Engineer", listed.Projection.VCard.Title) + assert.Equal("Engineering", listed.Projection.VCard.Role) + + sideResponse := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Advisor","source":"user"}`, + person.ID, sideJob.ID), "") + require.Equal(http.StatusCreated, sideResponse.Code) + var side store.Employment + require.NoError(json.Unmarshal(sideResponse.Body.Bytes(), &side)) + assert.False(side.IsPrimary) + + conflictResponse := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Consultant","is_primary":true,"source":"user"}`, + person.ID, sideJob.ID), "") + require.Equal(http.StatusConflict, conflictResponse.Code) + assert.Contains(conflictResponse.Body.String(), "employment_primary_conflict") + + rotateResponse := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/%d/primary", employmentsPath, side.ID), nil, + sideResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, rotateResponse.Code) + var rotated store.Employment + require.NoError(json.Unmarshal(rotateResponse.Body.Bytes(), &rotated)) + assert.True(rotated.IsPrimary) + + afterRotation := organizationRequest(t, srv, http.MethodGet, personPath, nil, "") + require.Equal(http.StatusOK, afterRotation.Code) + var rotatedList EmploymentsResponse + require.NoError(json.Unmarshal(afterRotation.Body.Bytes(), &rotatedList)) + require.NotNil(rotatedList.Projection) + assert.Equal("Another Org", rotatedList.Projection.OrganizationName) + + endResponse := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/%d/end", employmentsPath, rotated.ID), + []byte(`{"end_date":"2026-06"}`), rotateResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, endResponse.Code) + var ended store.Employment + require.NoError(json.Unmarshal(endResponse.Body.Bytes(), &ended)) + assert.False(ended.IsCurrent) + assert.False(ended.IsPrimary) + + afterEnd := organizationRequest(t, srv, http.MethodGet, personPath, nil, "") + require.Equal(http.StatusOK, afterEnd.Code) + var endedList EmploymentsResponse + require.NoError(json.Unmarshal(afterEnd.Body.Bytes(), &endedList)) + require.Len(endedList.Employments, 2, "ending an employment keeps it in history") + assert.Nil(endedList.Projection, + "the day job is still current but was demoted by the earlier rotation, "+ + "so there is no primary current employment and therefore no projection") + + promoteResponse := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/%d/primary", employmentsPath, created.ID), nil, + fmt.Sprintf(`"employment-%d-r2"`, created.ID)) + require.Equal(http.StatusOK, promoteResponse.Code) + + afterPromotion := organizationRequest(t, srv, http.MethodGet, personPath, nil, "") + require.Equal(http.StatusOK, afterPromotion.Code) + var promotedList EmploymentsResponse + require.NoError(json.Unmarshal(afterPromotion.Body.Bytes(), &promotedList)) + require.NotNil(promotedList.Projection) + assert.Equal("Example Org", promotedList.Projection.OrganizationName, + "explicit promotion restores the projection; demotion is never auto-undone") +} + +func TestPersonEmploymentsProjectionIsAbsentWithoutAPrimaryCurrentRow(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "bob@example.com", "bob") + + response := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("/api/v1/persons/%d/employments", person.ID), nil, "") + require.Equal(http.StatusOK, response.Code) + var listed EmploymentsResponse + require.NoError(json.Unmarshal(response.Body.Bytes(), &listed)) + assert.Empty(listed.Employments) + assert.Nil(listed.Projection, "no primary current employment means no projection") +} + +func TestEmploymentHTTPValidationAndConflictMapping(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "alice@example.com", "alice") + organization := mustAPIOrganization(t, st, "Example Org") + + missingPerson := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"source":"user"}`, + person.ID+9999, organization.ID), "") + require.Equal(http.StatusBadRequest, missingPerson.Code) + assert.Contains(missingPerson.Body.String(), "invalid_person_id") + + missingOrganization := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"source":"user"}`, + person.ID, organization.ID+9999), "") + require.Equal(http.StatusBadRequest, missingOrganization.Code, + "a body reference to a missing record is a bad request, not a missing resource") + assert.Contains(missingOrganization.Body.String(), "invalid_organization_id") + + missingSource := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d}`, + person.ID, organization.ID), "") + require.Equal(http.StatusBadRequest, missingSource.Code) + assert.Contains(missingSource.Body.String(), "invalid_source") + + emptySource := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"source":""}`, + person.ID, organization.ID), "") + require.Equal(http.StatusBadRequest, emptySource.Code) + assert.Contains(emptySource.Body.String(), "invalid_source") + + missingPathOrganization := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d/employments", organizationsPath, organization.ID+9999), nil, "") + require.Equal(http.StatusNotFound, missingPathOrganization.Code, + "a path-addressed organization that does not exist is a missing resource") + assert.Contains(missingPathOrganization.Body.String(), "organization_not_found") + + missingPathPerson := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("/api/v1/persons/%d/employments", person.ID+9999), nil, "") + require.Equal(http.StatusNotFound, missingPathPerson.Code, + "a path-addressed person that does not exist is a missing resource") + assert.Contains(missingPathPerson.Body.String(), "person_profile_not_found") + + badDate := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"start_date":"2019-13","source":"user"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusBadRequest, badDate.Code) + assert.Contains(badDate.Body.String(), "invalid_partial_date") + + reversedDates := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"start_date":"2020-06","end_date":"2020-05","source":"user"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusBadRequest, reversedDates.Code) + assert.Contains(reversedDates.Body.String(), "invalid_employment") + + firstResponse := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Engineer","source":"user"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusCreated, firstResponse.Code) + + duplicate := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"engineer","source":"user"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusConflict, duplicate.Code) + assert.Contains(duplicate.Body.String(), "employment_duplicate_active") + + var first store.Employment + require.NoError(json.Unmarshal(firstResponse.Body.Bytes(), &first)) + + missingIfMatch := organizationRequest(t, srv, http.MethodDelete, + fmt.Sprintf("%s/%d", employmentsPath, first.ID), nil, "") + require.Equal(http.StatusPreconditionRequired, missingIfMatch.Code) + + staleDelete := organizationRequest(t, srv, http.MethodDelete, + fmt.Sprintf("%s/%d", employmentsPath, first.ID), nil, + fmt.Sprintf(`"employment-%d-r99"`, first.ID)) + require.Equal(http.StatusConflict, staleDelete.Code) + assert.Contains(staleDelete.Body.String(), "employment_revision_conflict") + + deleted := organizationRequest(t, srv, http.MethodDelete, + fmt.Sprintf("%s/%d", employmentsPath, first.ID), nil, + firstResponse.Header().Get("ETag")) + require.Equal(http.StatusNoContent, deleted.Code) +} + +func TestOrganizationEmploymentsListing(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + alice := mustAPIPerson(t, st, "alice@example.com", "alice") + bob := mustAPIPerson(t, st, "bob@example.com", "bob") + organization := mustAPIOrganization(t, st, "Example Org") + + for _, person := range []*store.Person{alice, bob} { + response := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Engineer","source":"user"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusCreated, response.Code) + } + + response := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d/employments", organizationsPath, organization.ID), nil, "") + require.Equal(http.StatusOK, response.Code) + var listed EmploymentsResponse + require.NoError(json.Unmarshal(response.Body.Bytes(), &listed)) + assert.Len(listed.Employments, 2) + assert.Nil(listed.Projection, + "an organization listing has no single-person company projection") + + currentOnly := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d/employments?current_only=true", organizationsPath, organization.ID), + nil, "") + require.Equal(http.StatusOK, currentOnly.Code) + var current EmploymentsResponse + require.NoError(json.Unmarshal(currentOnly.Body.Bytes(), ¤t)) + assert.Len(current.Employments, 2) +} + +func TestEmploymentHTTPGetPatchAndStrictBody(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "casey@example.com", "casey") + organization := mustAPIOrganization(t, st, "Example Org") + + createdResponse := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Engineer","department":"Platform","source":"user"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Employment + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + getResponse := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d", employmentsPath, created.ID), nil, "") + require.Equal(http.StatusOK, getResponse.Code) + assert.Equal(createdResponse.Header().Get("ETag"), getResponse.Header().Get("ETag")) + + patchedResponse := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", employmentsPath, created.ID), + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Senior Engineer","source":"user"}`, + person.ID, organization.ID), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, patchedResponse.Code) + var patched store.Employment + require.NoError(json.Unmarshal(patchedResponse.Body.Bytes(), &patched)) + assert.Equal("Senior Engineer", *patched.Title) + assert.Nil(patched.Department, "PATCH replaces every mutable employment field") + + patchMissingSource := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", employmentsPath, patched.ID), + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Principal Engineer"}`, + person.ID, organization.ID), patchedResponse.Header().Get("ETag")) + require.Equal(http.StatusBadRequest, patchMissingSource.Code) + assert.Contains(patchMissingSource.Body.String(), "invalid_source") + + unknownField := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"source":"user","company":"ignored"}`, + person.ID, organization.ID), "") + require.Equal(http.StatusBadRequest, unknownField.Code) + assert.Contains(unknownField.Body.String(), "bad_request") +} + +func mustAPIOrganization(t *testing.T, st *store.Store, name string) *store.Organization { + t.Helper() + organization, err := st.CreateOrganizationContext(context.Background(), + store.OrganizationInput{Name: name, Kind: store.OrganizationKindCompany}) + require.NoError(t, err) + return organization +} diff --git a/internal/api/openapi.go b/internal/api/openapi.go index 595c87e9c..d19018d69 100644 --- a/internal/api/openapi.go +++ b/internal/api/openapi.go @@ -184,7 +184,9 @@ import ( // 1.38.0 adds authenticated raw access to inline person-profile media bytes. // Additive (minor bump): existing profile metadata and patch contracts are // unchanged, and URI-only media remains metadata-only. -const APISchemaVersion = "1.38.0" +// 1.39.0 adds organization profiles, employment history, duplicate review, +// and their typed attribute, projection, and lifecycle routes. +const APISchemaVersion = "1.39.0" // OpenAPIDocument builds the API schema from the same Huma route registration // used by the daemon. It binds no socket and needs no database. diff --git a/internal/api/openapi_test.go b/internal/api/openapi_test.go index 0daac9f0e..c1aae59aa 100644 --- a/internal/api/openapi_test.go +++ b/internal/api/openapi_test.go @@ -34,6 +34,56 @@ func TestOpenAPIDocumentUsesAPISchemaVersion(t *testing.T) { assert.NotEmpty(t, doc.Paths, "paths") } +func TestOrganizationCreateOpenAPIDocumentsLocationHeader(t *testing.T) { + require := require.New(t) + assert.Equal(t, "1.39.0", APISchemaVersion, + "organization and employment routes are an additive schema release") + for _, document := range []*huma.OpenAPI{ + OpenAPIDocument(), + openAPIClientDocument(), + } { + operation := document.Paths[organizationsPath].Post + require.NotNil(operation) + response := operation.Responses[httpStatusKey(http.StatusCreated)] + require.NotNil(response) + require.Contains(response.Headers, "Location") + require.Equal(huma.TypeString, response.Headers["Location"].Schema.Type) + } +} + +func TestOrganizationCreateSchemaOmitsPatchOnlyRetiredState(t *testing.T) { + require := require.New(t) + for _, document := range []*huma.OpenAPI{ + OpenAPIDocument(), + openAPIClientDocument(), + } { + createSchema := operationBodySchema( + t, document, document.Paths[organizationsPath].Post) + patchSchema := operationBodySchema( + t, document, document.Paths[organizationsPath+"/{id}"].Patch) + require.NotContains(createSchema.Properties, "retired") + require.Contains(patchSchema.Properties, "retired") + } +} + +func operationBodySchema( + t *testing.T, document *huma.OpenAPI, operation *huma.Operation, +) *huma.Schema { + t.Helper() + require.NotNil(t, operation) + require.NotNil(t, operation.RequestBody) + schema := operation.RequestBody.Content["application/json"].Schema + require.NotNil(t, schema) + if schema.Ref == "" { + return schema + } + const prefix = "#/components/schemas/" + require.True(t, strings.HasPrefix(schema.Ref, prefix), schema.Ref) + resolved := document.Components.Schemas.Map()[strings.TrimPrefix(schema.Ref, prefix)] + require.NotNil(t, resolved) + return resolved +} + func TestSourceStatusRunReferencesAreNullable(t *testing.T) { requirements := require.New(t) assertions := assert.New(t) @@ -265,7 +315,7 @@ func TestOpenAPIPersonAttributeContract(t *testing.T) { require := require.New(t) assert := assert.New(t) - assert.Equal("1.38.0", APISchemaVersion, + assert.Equal("1.39.0", APISchemaVersion, "structured profiles are additive to the attribute schema release") doc := OpenAPIDocument() @@ -339,7 +389,7 @@ func TestOpenAPIPersonProfileMediaContentContract(t *testing.T) { require := require.New(t) assert := assert.New(t) - assert.Equal("1.38.0", APISchemaVersion, + assert.Equal("1.39.0", APISchemaVersion, "raw profile media content is an additive schema release") doc := OpenAPIDocument() path := doc.Paths["/api/v1/persons/{id}/profile/media/{media_id}/content"] @@ -371,8 +421,9 @@ func TestOpenAPIMeetingImportContract(t *testing.T) { // confirm the meeting-import contract below still holds. Meeting import // shipped in 1.33.0; the feed added in 1.34.0, the attributes added in // 1.35.0, source-scoped identities added in 1.36.0, and structured profiles - // added in 1.37.0, and raw profile media added in 1.38.0 did not touch it. - assert.Equal("1.38.0", APISchemaVersion, "meeting import is an additive schema release") + // added in 1.37.0, raw profile media added in 1.38.0, and organizations and + // employments added in 1.39.0 did not touch it. + assert.Equal("1.39.0", APISchemaVersion, "meeting import is an additive schema release") doc := OpenAPIDocument() path := doc.Paths["/api/v1/import/meeting"] diff --git a/internal/api/organization_review_regressions_test.go b/internal/api/organization_review_regressions_test.go new file mode 100644 index 000000000..1b9f96167 --- /dev/null +++ b/internal/api/organization_review_regressions_test.go @@ -0,0 +1,66 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" +) + +func TestOrganizationHTTPMapsProfileOrdinalValidationToBadRequest(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + profilePath := fmt.Sprintf("%s/%d/profile", organizationsPath, created.ID) + + negativeOrdinal := organizationRequest(t, srv, http.MethodPut, profilePath, []byte(`{ + "names":[{"name":"Example Alias","name_kind":"alias","ordinal":-1,"source":"user"}] + }`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusBadRequest, negativeOrdinal.Code) + assert.Contains(negativeOrdinal.Body.String(), "invalid_organization") +} + +func TestOrganizationHTTPMapsIncompleteScopeValidationToBadRequest(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + profilePath := fmt.Sprintf("%s/%d/profile", organizationsPath, created.ID) + incompleteScope := organizationRequest(t, srv, http.MethodPut, profilePath, []byte(`{ + "contact_points":[{"contact_kind":"username","scope_kind":"workspace","original_value":"alice","source":"user"}] + }`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusBadRequest, incompleteScope.Code) + assert.Contains(incompleteScope.Body.String(), "invalid_organization") +} + +func TestEmploymentHTTPMapsMergedOrganizationToBadRequest(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "alice@example.com", "alice") + survivor := mustAPIOrganization(t, st, "Survivor Org") + losing := mustAPIOrganization(t, st, "Losing Org") + _, err := st.MergeOrganizationsContext(context.Background(), + survivor.ID, survivor.Revision, losing.ID, losing.Revision) + require.NoError(err) + + response := organizationRequest(t, srv, http.MethodPost, employmentsPath, + fmt.Appendf(nil, `{"person_id":%d,"organization_id":%d,"title":"Engineer","source":"user"}`, + person.ID, losing.ID), "") + require.Equal(http.StatusBadRequest, response.Code) + assert.Contains(response.Body.String(), "invalid_organization") +} diff --git a/internal/api/organizations.go b/internal/api/organizations.go new file mode 100644 index 000000000..dd2095acc --- /dev/null +++ b/internal/api/organizations.go @@ -0,0 +1,836 @@ +package api + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/danielgtaylor/huma/v2" + "go.kenn.io/msgvault/internal/store" +) + +const ( + organizationsPath = "/api/v1/organizations" +) + +// OrganizationStore is the feature-local capability for curated organizations. +type OrganizationStore interface { + CreateOrganizationContext(ctx context.Context, input store.OrganizationInput) (*store.Organization, error) + GetOrganizationContext(ctx context.Context, id int64) (*store.Organization, error) + ListOrganizationsContext(ctx context.Context, filter store.OrganizationFilter) ([]store.Organization, error) + CountOrganizationsContext(ctx context.Context, filter store.OrganizationFilter) (int64, error) + ReplaceOrganizationContext(ctx context.Context, id, expectedRevision int64, input store.OrganizationInput, retired bool) (*store.Organization, error) + DeleteOrganizationContext(ctx context.Context, id, expectedRevision int64) error + MergeOrganizationsContext(ctx context.Context, survivorID, survivorRevision, losingID, losingRevision int64) (*store.Organization, error) + GetOrganizationProfileContext(ctx context.Context, id int64, includeSuperseded bool) (*store.OrganizationProfile, error) + ReplaceOrganizationProfileContext(ctx context.Context, id, expectedRevision int64, input store.OrganizationProfileInput) (*store.OrganizationProfile, error) + ListOrganizationAttributeValuesContext(ctx context.Context, organizationID int64, query store.OrganizationAttributeQuery) ([]store.OrganizationAttributeValue, error) + SetOrganizationAttributeValueContext(ctx context.Context, input store.OrganizationAttributeValueInput) (*store.OrganizationAttributeWrite, error) + SupersedeOrganizationAttributeValueContext(ctx context.Context, input store.OrganizationAttributeSupersedeInput) (*store.OrganizationAttributeWrite, error) + RefreshOrganizationDuplicateSuggestionsContext(ctx context.Context) (int, error) + ListOrganizationDuplicateSuggestionsContext(ctx context.Context, status string, limit, offset int) ([]store.OrganizationDuplicateSuggestion, error) + ResolveOrganizationDuplicateSuggestionContext(ctx context.Context, id int64, status string, note *string) (*store.OrganizationDuplicateSuggestion, error) +} + +type OrganizationBody struct { + Name string `json:"name"` + Kind string `json:"kind,omitempty" enum:"company,nonprofit,school,government,household,other"` + PrimaryDomain *string `json:"primary_domain,omitempty" nullable:"true"` + Description *string `json:"description,omitempty" nullable:"true"` + Retired bool `json:"retired,omitempty"` +} + +type OrganizationCreateBody struct { + Name string `json:"name"` + Kind string `json:"kind,omitempty" enum:"company,nonprofit,school,government,household,other"` + PrimaryDomain *string `json:"primary_domain,omitempty" nullable:"true"` + Description *string `json:"description,omitempty" nullable:"true"` +} + +type OrganizationsResponse struct { + Organizations []store.Organization `json:"organizations"` + Total int64 `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` +} + +type MergeOrganizationBody struct { + LosingOrganizationID int64 `json:"losing_organization_id"` + LosingRevision int64 `json:"losing_revision"` +} + +// OrganizationEnvelopeBody is deliberately embedded in the child bodies so +// JSON remains flat while only client-writable envelope fields are accepted. +type OrganizationEnvelopeBody struct { + Ordinal *int `json:"ordinal,omitempty" minimum:"0"` + Pref *int `json:"pref,omitempty" nullable:"true"` + VCardProperty *string `json:"vcard_property,omitempty" nullable:"true"` + VCardGroup *string `json:"vcard_group,omitempty" nullable:"true"` + VCardPropID *string `json:"vcard_prop_id,omitempty" nullable:"true"` + VCardPID []string `json:"vcard_pid,omitempty"` + VCardAltID *string `json:"vcard_altid,omitempty" nullable:"true"` + Source string `json:"source" enum:"user,carddav_import,vcard_import,archive_observation,extraction,enrichment,system"` + SourceRef *string `json:"source_ref,omitempty" nullable:"true"` +} + +func (b OrganizationEnvelopeBody) envelope() store.ValueEnvelopeInput { + property := "" + if b.VCardProperty != nil { + property = *b.VCardProperty + } + return store.ValueEnvelopeInput{ + Ordinal: b.Ordinal, Pref: b.Pref, + VCard: store.VCardIdentity{Property: property, Group: b.VCardGroup, PropID: b.VCardPropID, PID: b.VCardPID, AltID: b.VCardAltID}, + Source: store.Provenance(b.Source), SourceRef: b.SourceRef, + } +} + +type OrganizationNameBody struct { + OrganizationEnvelopeBody + + Name string `json:"name"` + NameKind string `json:"name_kind" enum:"alias,legal,former,abbreviation,sort"` +} + +type OrganizationIdentifierBody struct { + OrganizationEnvelopeBody + + IdentifierKind string `json:"identifier_kind" enum:"domain,linkedin,duns,tax_id,registry,other"` + Value string `json:"identifier_value"` +} + +type OrganizationAddressBody struct { + OrganizationEnvelopeBody + + AddressKind string `json:"address_kind"` + PostOfficeBox *string `json:"post_office_box,omitempty" nullable:"true"` + ExtendedAddress *string `json:"extended_address,omitempty" nullable:"true"` + StreetAddress *string `json:"street_address,omitempty" nullable:"true"` + Locality *string `json:"locality,omitempty" nullable:"true"` + Region *string `json:"region,omitempty" nullable:"true"` + PostalCode *string `json:"postal_code,omitempty" nullable:"true"` + CountryName *string `json:"country_name,omitempty" nullable:"true"` + ExtendedComponents *string `json:"extended_components,omitempty" nullable:"true"` + FreeText *string `json:"free_text,omitempty" nullable:"true"` + Label *string `json:"label,omitempty" nullable:"true"` + GeoURI *string `json:"geo_uri,omitempty" nullable:"true"` + Timezone *string `json:"timezone,omitempty" nullable:"true"` + CountryCode *string `json:"country_code,omitempty" nullable:"true"` + PlaceURI *string `json:"place_uri,omitempty" nullable:"true"` + OriginalValue string `json:"original_value,omitempty"` +} + +type OrganizationContactPointBody struct { + OrganizationEnvelopeBody + + ContactKind string `json:"contact_kind" enum:"email,phone,username,impp,url,social,calendar,contact_uri,org_directory,language"` + ServiceSlug *string `json:"service_slug,omitempty" nullable:"true"` + ScopeKind *string `json:"scope_kind,omitempty" nullable:"true"` + ScopeValue *string `json:"scope_value,omitempty" nullable:"true"` + OriginalValue string `json:"original_value"` + URI *string `json:"uri,omitempty" nullable:"true"` +} + +type OrganizationMediaBody struct { + OrganizationEnvelopeBody + + MediaKind string `json:"media_kind" enum:"photo,logo,sound,key"` + MediaType *string `json:"media_type,omitempty" nullable:"true"` + URI *string `json:"uri,omitempty" nullable:"true"` + Data []byte `json:"data,omitempty"` + OriginalValue string `json:"original_value,omitempty"` +} + +type OrganizationCategoryBody struct { + OrganizationEnvelopeBody + + Category string `json:"category"` +} + +type OrganizationProfileBody struct { + Names []OrganizationNameBody `json:"names,omitempty"` + Identifiers []OrganizationIdentifierBody `json:"identifiers,omitempty"` + Addresses []OrganizationAddressBody `json:"addresses,omitempty"` + ContactPoints []OrganizationContactPointBody `json:"contact_points,omitempty"` + Media []OrganizationMediaBody `json:"media,omitempty"` + Categories []OrganizationCategoryBody `json:"categories,omitempty"` +} + +type OrganizationAttributesResponse struct { + Values []store.OrganizationAttributeValue `json:"values"` +} + +type SetOrganizationAttributeBody struct { + DefinitionSlug string `json:"definition_slug"` + Ordinal *int64 `json:"ordinal,omitempty" nullable:"true"` + Value store.AttributeValue `json:"value"` + Source string `json:"source" enum:"user,carddav_import,vcard_import,archive_observation,extraction,enrichment,system"` + SourceRef *string `json:"source_ref,omitempty" nullable:"true"` + Confidence *float64 `json:"confidence,omitempty" nullable:"true"` + Actor *string `json:"actor,omitempty" nullable:"true"` + ExpectedValueID *int64 `json:"expected_value_id,omitempty" nullable:"true"` + DryRun bool `json:"dry_run,omitempty"` +} + +type OrganizationDuplicateSuggestionsResponse struct { + Suggestions []store.OrganizationDuplicateSuggestion `json:"suggestions"` +} + +type RefreshOrganizationDuplicateSuggestionsResponse struct { + Created int `json:"created"` +} + +type ResolveOrganizationDuplicateSuggestionBody struct { + Status string `json:"status" enum:"accepted,rejected"` + Note *string `json:"note,omitempty" nullable:"true"` +} + +func (s *Server) registerOrganizationRoutes(api huma.API) { + list := rawAPIV1Operation("listOrganizations", http.MethodGet, "/organizations", "List organizations") + list.Parameters = append(list.Parameters, queryIntegerParam("limit", "Maximum results"), queryIntegerParam("offset", "Results to skip"), queryBooleanParam("include_retired", "Include retired organizations"), queryStringParam("q", "Normalized-name search", false)) + list.Responses = jsonResponsesFor[OrganizationsResponse](api) + addErrorResponses(api, list.Responses, http.StatusBadRequest, http.StatusServiceUnavailable) + registerRawHumaRoute(api, list, s.handleListOrganizations) + + create := rawAPIV1Operation("createOrganization", http.MethodPost, "/organizations", "Create an organization") + create.RequestBody = jsonRequestBodyFor[OrganizationCreateBody](api) + create.Responses = jsonResponsesFor[store.Organization](api, http.StatusCreated) + addOrganizationETagHeader(create.Responses[httpStatusKey(http.StatusCreated)]) + addOrganizationLocationHeader(create.Responses[httpStatusKey(http.StatusCreated)]) + addErrorResponses(api, create.Responses, http.StatusBadRequest, http.StatusServiceUnavailable) + registerRawHumaRoute(api, create, s.handleCreateOrganization) + + duplicates := rawAPIV1Operation("listOrganizationDuplicateSuggestions", http.MethodGet, "/organizations/duplicate-suggestions", "List organization duplicate suggestions") + duplicates.Parameters = append(duplicates.Parameters, queryStringParam("status", "Filter by status", false), queryIntegerParam("limit", "Maximum results"), queryIntegerParam("offset", "Results to skip")) + duplicates.Responses = jsonResponsesFor[OrganizationDuplicateSuggestionsResponse](api) + addErrorResponses(api, duplicates.Responses, http.StatusBadRequest, http.StatusServiceUnavailable) + registerRawHumaRoute(api, duplicates, s.handleListOrganizationDuplicateSuggestions) + + refresh := rawAPIV1Operation("refreshOrganizationDuplicateSuggestions", http.MethodPost, "/organizations/duplicate-suggestions/refresh", "Refresh organization duplicate suggestions") + refresh.Responses = jsonResponsesFor[RefreshOrganizationDuplicateSuggestionsResponse](api) + addErrorResponses(api, refresh.Responses, http.StatusServiceUnavailable) + registerRawHumaRoute(api, refresh, s.handleRefreshOrganizationDuplicateSuggestions) + + resolve := rawAPIV1Operation("resolveOrganizationDuplicateSuggestion", http.MethodPost, "/organizations/duplicate-suggestions/{id}/resolve", "Resolve an organization duplicate suggestion") + resolve.Parameters = append(resolve.Parameters, pathIntegerParam("Duplicate suggestion ID")) + resolve.RequestBody = jsonRequestBodyFor[ResolveOrganizationDuplicateSuggestionBody](api) + resolve.Responses = jsonResponsesFor[store.OrganizationDuplicateSuggestion](api) + addErrorResponses(api, resolve.Responses, http.StatusBadRequest, http.StatusConflict, + http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, resolve, s.handleResolveOrganizationDuplicateSuggestion) + + get := rawAPIV1Operation("getOrganization", http.MethodGet, "/organizations/{id}", "Get an organization") + addOrganizationIDParameter(&get) + get.Responses = jsonResponsesFor[store.OrganizationProfile](api) + addOrganizationETagHeader(get.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, get.Responses, http.StatusBadRequest, http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, get, s.handleGetOrganization) + + patch := rawAPIV1Operation("patchOrganization", http.MethodPatch, "/organizations/{id}", "Replace an organization's mutable fields") + addOrganizationIDParameter(&patch) + addOrganizationIfMatchParameter(&patch) + patch.RequestBody = jsonRequestBodyFor[OrganizationBody](api) + patch.Responses = jsonResponsesFor[store.Organization](api) + addOrganizationETagHeader(patch.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, patch.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, patch, s.handlePatchOrganization) + + remove := rawAPIV1Operation("deleteOrganization", http.MethodDelete, "/organizations/{id}", "Delete an organization without employment records") + addOrganizationIDParameter(&remove) + addOrganizationIfMatchParameter(&remove) + remove.Responses = rawHumaResponses(http.StatusNoContent) + remove.Responses["default"] = errorResponseFor(api) + addErrorResponses(api, remove.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, remove, s.handleDeleteOrganization) + + merge := rawAPIV1Operation("mergeOrganization", http.MethodPost, "/organizations/{id}/merge", "Merge another organization into this organization") + addOrganizationIDParameter(&merge) + addOrganizationIfMatchParameter(&merge) + merge.RequestBody = jsonRequestBodyFor[MergeOrganizationBody](api) + merge.Responses = jsonResponsesFor[store.Organization](api) + addOrganizationETagHeader(merge.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, merge.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, merge, s.handleMergeOrganization) + + history := rawAPIV1Operation("getOrganizationHistory", http.MethodGet, "/organizations/{id}/history", "Get organization profile history") + addOrganizationIDParameter(&history) + history.Responses = jsonResponsesFor[store.OrganizationProfile](api) + addOrganizationETagHeader(history.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, history.Responses, http.StatusBadRequest, http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, history, s.handleGetOrganizationHistory) + + profile := rawAPIV1Operation("putOrganizationProfile", http.MethodPut, "/organizations/{id}/profile", "Replace organization profile collections") + addOrganizationIDParameter(&profile) + addOrganizationIfMatchParameter(&profile) + profile.RequestBody = jsonRequestBodyFor[OrganizationProfileBody](api) + profile.Responses = jsonResponsesFor[store.OrganizationProfile](api) + addOrganizationETagHeader(profile.Responses[httpStatusKey(http.StatusOK)]) + addErrorResponses(api, profile.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusPreconditionRequired, http.StatusServiceUnavailable) + registerRawHumaRoute(api, profile, s.handlePutOrganizationProfile) + + attributes := rawAPIV1Operation("listOrganizationAttributes", http.MethodGet, "/organizations/{id}/attributes", "List organization typed attributes") + addOrganizationIDParameter(&attributes) + attributes.Parameters = append(attributes.Parameters, queryBooleanParam("include_superseded", "Include superseded values"), queryStringParam("definition_slug", "Restrict to one definition", false)) + attributes.Responses = jsonResponsesFor[OrganizationAttributesResponse](api) + addErrorResponses(api, attributes.Responses, http.StatusBadRequest, http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, attributes, s.handleListOrganizationAttributes) + + setAttribute := rawAPIV1Operation("setOrganizationAttribute", http.MethodPost, "/organizations/{id}/attributes", "Set an organization typed attribute") + addOrganizationIDParameter(&setAttribute) + setAttribute.RequestBody = jsonRequestBodyFor[SetOrganizationAttributeBody](api) + setAttribute.Responses = jsonResponsesFor[store.OrganizationAttributeWrite](api, http.StatusOK, http.StatusCreated) + addErrorResponses(api, setAttribute.Responses, http.StatusBadRequest, http.StatusConflict, http.StatusNotFound, http.StatusServiceUnavailable) + registerRawHumaRoute(api, setAttribute, s.handleSetOrganizationAttribute) +} + +func (s *Server) handleListOrganizations(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + filter, ok := s.organizationFilter(w, r) + if !ok { + return + } + total, err := organizations.CountOrganizationsContext(r.Context(), filter) + if err != nil { + s.writeOrganizationError(w, err) + return + } + rows, err := organizations.ListOrganizationsContext(r.Context(), filter) + if err != nil { + s.writeOrganizationError(w, err) + return + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, OrganizationsResponse{Organizations: rows, Total: total, Limit: filter.Limit, Offset: filter.Offset}) +} + +func (s *Server) organizationFilter(w http.ResponseWriter, r *http.Request) (store.OrganizationFilter, bool) { + filter := store.OrganizationFilter{Limit: store.DefaultOrganizationPageSize} + limit, present, err := queryInt(r, "limit") + if err != nil { + s.rejectBadParam(w, err) + return filter, false + } + if present { + if limit < 0 { + s.rejectBadParam(w, newParamError("limit", "limit must not be negative")) + return filter, false + } + if limit > 0 { + filter.Limit = min(limit, store.MaxOrganizationPageSize) + } + } + offset, present, err := queryInt(r, "offset") + if err != nil { + s.rejectBadParam(w, err) + return filter, false + } + if present { + if offset < 0 { + s.rejectBadParam(w, newParamError("offset", "offset must not be negative")) + return filter, false + } + filter.Offset = offset + } + includeRetired, _, err := queryBool(r, "include_retired") + if err != nil { + s.rejectBadParam(w, err) + return filter, false + } + filter.IncludeRetired = includeRetired + filter.Query = strings.TrimSpace(r.URL.Query().Get("q")) + return filter, true +} + +func (s *Server) handleCreateOrganization(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + var body OrganizationCreateBody + if !decodePersonRequest(w, r, &body) { + return + } + organization, err := organizations.CreateOrganizationContext( + r.Context(), organizationCreateInput(body)) + if err != nil { + s.writeOrganizationError(w, err) + return + } + writeOrganization(w, http.StatusCreated, organization) +} + +func (s *Server) handleGetOrganization(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + profile, err := organizations.GetOrganizationProfileContext(r.Context(), id, false) + if err != nil { + s.writeOrganizationError(w, err) + return + } + writeOrganizationProfile(w, profile) +} + +func (s *Server) handlePatchOrganization(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + revision, ok := organizationIfMatch(w, r, id) + if !ok { + return + } + var body OrganizationBody + if !decodePersonRequest(w, r, &body) { + return + } + organization, err := organizations.ReplaceOrganizationContext( + r.Context(), id, revision, organizationInput(body), body.Retired) + if err != nil { + s.writeOrganizationError(w, err) + return + } + writeOrganization(w, http.StatusOK, organization) +} + +func (s *Server) handleDeleteOrganization(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + revision, ok := organizationIfMatch(w, r, id) + if !ok { + return + } + if err := organizations.DeleteOrganizationContext(r.Context(), id, revision); err != nil { + s.writeOrganizationError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (s *Server) handleMergeOrganization(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + revision, ok := organizationIfMatch(w, r, id) + if !ok { + return + } + var body MergeOrganizationBody + if !decodePersonRequest(w, r, &body) { + return + } + if body.LosingOrganizationID <= 0 { + writeError(w, http.StatusBadRequest, "invalid_organization", "losing_organization_id must be a positive integer") + return + } + organization, err := organizations.MergeOrganizationsContext(r.Context(), id, revision, body.LosingOrganizationID, body.LosingRevision) + if err != nil { + s.writeOrganizationError(w, err) + return + } + writeOrganization(w, http.StatusOK, organization) +} + +func (s *Server) handleGetOrganizationHistory(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + profile, err := organizations.GetOrganizationProfileContext(r.Context(), id, true) + if err != nil { + s.writeOrganizationError(w, err) + return + } + writeOrganizationProfile(w, profile) +} + +func (s *Server) handlePutOrganizationProfile(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + revision, ok := organizationIfMatch(w, r, id) + if !ok { + return + } + var body OrganizationProfileBody + if !decodePersonRequest(w, r, &body) { + return + } + input, err := organizationProfileInput(body) + if err != nil { + s.writeOrganizationError(w, err) + return + } + profile, err := organizations.ReplaceOrganizationProfileContext(r.Context(), id, revision, input) + if err != nil { + s.writeOrganizationError(w, err) + return + } + writeOrganizationProfile(w, profile) +} + +func (s *Server) handleListOrganizationAttributes(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + includeSuperseded, _, err := queryBool(r, "include_superseded") + if err != nil { + s.rejectBadParam(w, err) + return + } + if _, err := organizations.GetOrganizationContext(r.Context(), id); err != nil { + s.writeOrganizationError(w, err) + return + } + values, err := organizations.ListOrganizationAttributeValuesContext(r.Context(), id, store.OrganizationAttributeQuery{DefinitionSlug: strings.TrimSpace(r.URL.Query().Get("definition_slug")), IncludeHistory: includeSuperseded}) + if err != nil { + s.writeOrganizationError(w, err) + return + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, OrganizationAttributesResponse{Values: values}) +} + +func (s *Server) handleSetOrganizationAttribute(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, ok := organizationID(w, r) + if !ok { + return + } + var body SetOrganizationAttributeBody + if !decodePersonRequest(w, r, &body) { + return + } + result, err := organizations.SetOrganizationAttributeValueContext(r.Context(), store.OrganizationAttributeValueInput{OrganizationID: id, DefinitionSlug: body.DefinitionSlug, Ordinal: body.Ordinal, Value: body.Value, Source: store.Provenance(body.Source), SourceRef: body.SourceRef, Confidence: body.Confidence, Actor: body.Actor, ExpectedValueID: body.ExpectedValueID, DryRun: body.DryRun}) + if err != nil { + s.writeOrganizationError(w, err) + return + } + status := http.StatusCreated + if body.DryRun { + status = http.StatusOK + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, status, result) +} + +func (s *Server) handleListOrganizationDuplicateSuggestions(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + limit, offset, ok := s.duplicateSuggestionPagination(w, r) + if !ok { + return + } + rows, err := organizations.ListOrganizationDuplicateSuggestionsContext(r.Context(), strings.TrimSpace(r.URL.Query().Get("status")), limit, offset) + if err != nil { + s.writeOrganizationError(w, err) + return + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, OrganizationDuplicateSuggestionsResponse{Suggestions: rows}) +} + +func (s *Server) handleRefreshOrganizationDuplicateSuggestions(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + created, err := organizations.RefreshOrganizationDuplicateSuggestionsContext(r.Context()) + if err != nil { + s.writeOrganizationError(w, err) + return + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, RefreshOrganizationDuplicateSuggestionsResponse{Created: created}) +} + +func (s *Server) handleResolveOrganizationDuplicateSuggestion(w http.ResponseWriter, r *http.Request) { + organizations, ok := s.organizationStore(w) + if !ok { + return + } + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || id <= 0 { + writeError(w, http.StatusBadRequest, "invalid_duplicate_suggestion", "Duplicate suggestion ID must be a positive integer") + return + } + var body ResolveOrganizationDuplicateSuggestionBody + if !decodePersonRequest(w, r, &body) { + return + } + resolved, err := organizations.ResolveOrganizationDuplicateSuggestionContext(r.Context(), id, body.Status, body.Note) + if err != nil { + s.writeOrganizationError(w, err) + return + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, resolved) +} + +func (s *Server) duplicateSuggestionPagination(w http.ResponseWriter, r *http.Request) (int, int, bool) { + limit := store.DefaultOrganizationPageSize + parsed, present, err := queryInt(r, "limit") + if err != nil { + s.rejectBadParam(w, err) + return 0, 0, false + } + if present { + if parsed < 0 { + s.rejectBadParam(w, newParamError("limit", "limit must not be negative")) + return 0, 0, false + } + if parsed > 0 { + limit = min(parsed, store.MaxOrganizationPageSize) + } + } + offset, present, err := queryInt(r, "offset") + if err != nil { + s.rejectBadParam(w, err) + return 0, 0, false + } + if present { + if offset < 0 { + s.rejectBadParam(w, newParamError("offset", "offset must not be negative")) + return 0, 0, false + } + } + return limit, offset, true +} + +func organizationInput(body OrganizationBody) store.OrganizationInput { + return store.OrganizationInput{Name: body.Name, Kind: store.OrganizationKind(body.Kind), PrimaryDomain: body.PrimaryDomain, Description: body.Description} +} + +func organizationCreateInput(body OrganizationCreateBody) store.OrganizationInput { + return store.OrganizationInput{ + Name: body.Name, Kind: store.OrganizationKind(body.Kind), + PrimaryDomain: body.PrimaryDomain, Description: body.Description, + } +} + +func organizationProfileInput(body OrganizationProfileBody) (store.OrganizationProfileInput, error) { + input := store.OrganizationProfileInput{Names: make([]store.OrganizationNameInput, 0, len(body.Names)), Identifiers: make([]store.OrganizationIdentifierInput, 0, len(body.Identifiers)), Addresses: make([]store.OrganizationAddressInput, 0, len(body.Addresses)), ContactPoints: make([]store.OrganizationContactPointInput, 0, len(body.ContactPoints)), Media: make([]store.OrganizationMediaInput, 0, len(body.Media)), Categories: make([]store.OrganizationCategoryInput, 0, len(body.Categories))} + for _, row := range body.Names { + input.Names = append(input.Names, store.OrganizationNameInput{Name: row.Name, NameKind: row.NameKind, Envelope: row.envelope()}) + } + for i, row := range body.Identifiers { + if !organizationIdentifierKindValid(row.IdentifierKind) { + return store.OrganizationProfileInput{}, fmt.Errorf( + "%w: identifiers[%d].identifier_kind %q is unknown", + store.ErrOrganizationInvalid, i, row.IdentifierKind) + } + input.Identifiers = append(input.Identifiers, store.OrganizationIdentifierInput{IdentifierKind: row.IdentifierKind, Value: row.Value, Envelope: row.envelope()}) + } + for _, row := range body.Addresses { + input.Addresses = append(input.Addresses, store.OrganizationAddressInput{AddressKind: store.PersonAddressKind(row.AddressKind), PostOfficeBox: row.PostOfficeBox, ExtendedAddress: row.ExtendedAddress, StreetAddress: row.StreetAddress, Locality: row.Locality, Region: row.Region, PostalCode: row.PostalCode, CountryName: row.CountryName, ExtendedComponents: row.ExtendedComponents, FreeText: row.FreeText, Label: row.Label, GeoURI: row.GeoURI, Timezone: row.Timezone, CountryCode: row.CountryCode, PlaceURI: row.PlaceURI, OriginalValue: row.OriginalValue, Envelope: row.envelope()}) + } + for _, row := range body.ContactPoints { + input.ContactPoints = append(input.ContactPoints, store.OrganizationContactPointInput{AddressKind: store.ContactAddressKind(row.ContactKind), ServiceSlug: row.ServiceSlug, ScopeKind: row.ScopeKind, ScopeValue: row.ScopeValue, OriginalValue: row.OriginalValue, URI: row.URI, Envelope: row.envelope()}) + } + for _, row := range body.Media { + input.Media = append(input.Media, store.OrganizationMediaInput{MediaKind: store.PersonMediaKind(row.MediaKind), MediaType: row.MediaType, URI: row.URI, Data: row.Data, OriginalValue: row.OriginalValue, Envelope: row.envelope()}) + } + for _, row := range body.Categories { + input.Categories = append(input.Categories, store.OrganizationCategoryInput{Category: row.Category, Envelope: row.envelope()}) + } + return input, nil +} + +func organizationIdentifierKindValid(kind string) bool { + switch kind { + case store.OrganizationIdentifierKindDomain, store.OrganizationIdentifierKindLinkedIn, + store.OrganizationIdentifierKindDUNS, store.OrganizationIdentifierKindTaxID, + store.OrganizationIdentifierKindRegistry, store.OrganizationIdentifierKindOther: + return true + default: + return false + } +} + +func (s *Server) organizationStore(w http.ResponseWriter) (OrganizationStore, bool) { + organizations, ok := s.store.(OrganizationStore) + if !ok { + writeError(w, http.StatusServiceUnavailable, "organizations_unavailable", "Organizations are unavailable") + } + return organizations, ok +} + +func (s *Server) writeOrganizationError(w http.ResponseWriter, err error) { + if s.writeIfContextError(w, err) { + return + } + switch { + case errors.Is(err, store.ErrOrganizationNotFound): + writeError(w, http.StatusNotFound, "organization_not_found", "Organization not found") + case errors.Is(err, store.ErrOrganizationRevisionConflict): + writeError(w, http.StatusConflict, "organization_revision_conflict", "Organization changed; reload and retry") + case errors.Is(err, store.ErrOrganizationHasEmployments): + writeError(w, http.StatusConflict, "organization_has_employments", "Organization still has employment records; retire it instead of deleting it") + case errors.Is(err, store.ErrOrganizationMergeConflict): + writeError(w, http.StatusConflict, "organization_merge_conflict", "Organization merge conflicts with existing employment records") + case errors.Is(err, store.ErrOrganizationInvalid): + writeError(w, http.StatusBadRequest, "invalid_organization", err.Error()) + case isOrganizationProfileValidationError(err): + writeError(w, http.StatusBadRequest, "invalid_organization", err.Error()) + case errors.Is(err, store.ErrInvalidProvenance): + writeError(w, http.StatusBadRequest, "invalid_source", err.Error()) + case errors.Is(err, store.ErrAttributeObjectTypeMismatch): + writeError(w, http.StatusBadRequest, "invalid_attribute_definition", err.Error()) + case errors.Is(err, store.ErrAttributeDefinitionNotFound): + writeError(w, http.StatusNotFound, "attribute_definition_not_found", "Attribute definition not found") + case errors.Is(err, store.ErrAttributeDefinitionInactive): + writeError(w, http.StatusConflict, "attribute_definition_inactive", err.Error()) + case errors.Is(err, store.ErrAttributeDefinitionNotWritable): + writeError(w, http.StatusConflict, "attribute_definition_not_writable", err.Error()) + case errors.Is(err, store.ErrAttributeValueInvalid): + writeError(w, http.StatusBadRequest, "invalid_attribute_value", err.Error()) + case errors.Is(err, store.ErrAttributeValueNotFound): + writeError(w, http.StatusNotFound, "attribute_value_not_found", "Attribute value not found") + case errors.Is(err, store.ErrAttributeValueConflict): + writeError(w, http.StatusConflict, "attribute_value_conflict", "Attribute value changed; reload and retry") + case errors.Is(err, store.ErrOrganizationDuplicateSuggestionNotFound): + writeError(w, http.StatusNotFound, "duplicate_suggestion_not_found", "Organization duplicate suggestion not found") + case errors.Is(err, store.ErrOrganizationDuplicateSuggestionAlreadyResolved): + writeError(w, http.StatusConflict, "duplicate_suggestion_already_resolved", + "Organization duplicate suggestion is already resolved") + case errors.Is(err, store.ErrOrganizationDuplicateSuggestionInvalid): + writeError(w, http.StatusBadRequest, "invalid_duplicate_suggestion", err.Error()) + case errors.Is(err, store.ErrPersonNotFound): + writeError(w, http.StatusBadRequest, "invalid_person_id", "Person ID is invalid") + default: + s.logger.Error("organization operation failed", "error", err) + writeError(w, http.StatusInternalServerError, "organization_failed", "Organization operation failed") + } +} + +func isOrganizationProfileValidationError(err error) bool { + for _, target := range []error{ + store.ErrConfidenceScope, store.ErrInvalidProfilePref, + store.ErrInvalidProfileOrdinal, store.ErrServiceScopeIncomplete, + store.ErrInvalidContactAddressKind, store.ErrContactPointValueMissing, + store.ErrInvalidPersonAddressKind, store.ErrPersonAddressValueMissing, + store.ErrInvalidPersonMediaKind, store.ErrPersonMediaEmpty, + store.ErrPersonMediaTooLarge, store.ErrServiceNotFound, + store.ErrServiceScopeRequired, store.ErrServiceScopeForbidden, + store.ErrNormalizationRejected, + } { + if errors.Is(err, target) { + return true + } + } + return false +} + +func writeOrganization(w http.ResponseWriter, status int, organization *store.Organization) { + w.Header().Set("ETag", organizationETag(*organization)) + w.Header().Set("Cache-Control", "no-store") + if status == http.StatusCreated { + w.Header().Set("Location", organizationsPath+"/"+strconv.FormatInt(organization.ID, 10)) + } + writeJSON(w, status, organization) +} + +func writeOrganizationProfile(w http.ResponseWriter, profile *store.OrganizationProfile) { + w.Header().Set("ETag", organizationETag(profile.Organization)) + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, profile) +} + +func addOrganizationIDParameter(operation *huma.Operation) { + operation.Parameters = append(operation.Parameters, &huma.Param{Name: "id", In: "path", Required: true, Description: "Organization ID", Schema: &huma.Schema{Type: huma.TypeInteger, Format: "int64"}}) +} +func addOrganizationIfMatchParameter(operation *huma.Operation) { + operation.Parameters = append(operation.Parameters, &huma.Param{Name: ifMatchHeaderName, In: "header", Required: true, Description: "Strong ETag returned by the latest organization read", Schema: &huma.Schema{Type: huma.TypeString}}) +} +func addOrganizationETagHeader(response *huma.Response) { + response.Headers = map[string]*huma.Param{"ETag": {Description: "Strong organization revision tag for optimistic concurrency", Schema: &huma.Schema{Type: huma.TypeString}}} +} + +func addOrganizationLocationHeader(response *huma.Response) { + response.Headers["Location"] = &huma.Param{ + Description: "Canonical URL of the created organization", + Schema: &huma.Schema{Type: huma.TypeString}, + } +} + +func organizationID(w http.ResponseWriter, r *http.Request) (int64, bool) { + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || id <= 0 { + writeError(w, http.StatusBadRequest, "invalid_organization_id", "Organization ID must be a positive integer") + return 0, false + } + return id, true +} + +func organizationETag(organization store.Organization) string { + return fmt.Sprintf(`"organization-%d-r%d"`, organization.ID, organization.Revision) +} + +func organizationIfMatch(w http.ResponseWriter, r *http.Request, id int64) (int64, bool) { + values := r.Header.Values(ifMatchHeaderName) + if len(values) == 0 || (len(values) == 1 && strings.TrimSpace(values[0]) == "") { + writeError(w, http.StatusPreconditionRequired, "if_match_required", "If-Match is required") + return 0, false + } + if len(values) != 1 { + writeError(w, http.StatusBadRequest, "invalid_if_match", "If-Match must contain exactly one revision tag") + return 0, false + } + prefix := fmt.Sprintf(`"organization-%d-r`, id) + value := strings.TrimSpace(values[0]) + if !strings.HasPrefix(value, prefix) || !strings.HasSuffix(value, `"`) { + writeError(w, http.StatusBadRequest, "invalid_if_match", "If-Match is not an organization revision tag") + return 0, false + } + revision, err := strconv.ParseInt(strings.TrimSuffix(strings.TrimPrefix(value, prefix), `"`), 10, 64) + if err != nil || revision <= 0 { + writeError(w, http.StatusBadRequest, "invalid_if_match", "If-Match is not an organization revision tag") + return 0, false + } + return revision, true +} diff --git a/internal/api/organizations_test.go b/internal/api/organizations_test.go new file mode 100644 index 000000000..2fa155ee1 --- /dev/null +++ b/internal/api/organizations_test.go @@ -0,0 +1,499 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/config" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestOrganizationHTTPCreateGetListPatchAndDelete(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company","primary_domain":"Example.com"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + assert.Equal("Example Org", created.Name) + require.NotNil(created.PrimaryDomain) + assert.Equal("example.com", *created.PrimaryDomain) + etag := createdResponse.Header().Get("ETag") + assert.Equal(fmt.Sprintf(`"organization-%d-r1"`, created.ID), etag) + assert.Equal(fmt.Sprintf("%s/%d", organizationsPath, created.ID), createdResponse.Header().Get("Location")) + + listResponse := organizationRequest(t, srv, http.MethodGet, organizationsPath, nil, "") + require.Equal(http.StatusOK, listResponse.Code) + var listed OrganizationsResponse + require.NoError(json.Unmarshal(listResponse.Body.Bytes(), &listed)) + require.Len(listed.Organizations, 1) + assert.Equal(int64(1), listed.Total) + assert.Equal(store.DefaultOrganizationPageSize, listed.Limit) + assert.Equal(0, listed.Offset) + + getResponse := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), nil, "") + require.Equal(http.StatusOK, getResponse.Code) + assert.Equal(etag, getResponse.Header().Get("ETag")) + + patchResponse := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), + []byte(`{"name":"Example Group","kind":"company","description":"Renamed."}`), etag) + require.Equal(http.StatusOK, patchResponse.Code) + var patched store.Organization + require.NoError(json.Unmarshal(patchResponse.Body.Bytes(), &patched)) + assert.Equal("Example Group", patched.Name) + assert.Nil(patched.PrimaryDomain, "PATCH replaces the full mutable field set") + assert.Equal(created.Revision+1, patched.Revision) + + staleResponse := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), []byte(`{"name":"Stale"}`), etag) + require.Equal(http.StatusConflict, staleResponse.Code) + assert.Contains(staleResponse.Body.String(), "organization_revision_conflict") + + missingIfMatch := organizationRequest(t, srv, http.MethodDelete, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), nil, "") + require.Equal(http.StatusPreconditionRequired, missingIfMatch.Code) + + deleteResponse := organizationRequest(t, srv, http.MethodDelete, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), nil, patchResponse.Header().Get("ETag")) + require.Equal(http.StatusNoContent, deleteResponse.Code) + + goneResponse := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), nil, "") + require.Equal(http.StatusNotFound, goneResponse.Code) + assert.Contains(goneResponse.Body.String(), "organization_not_found") +} + +func TestOrganizationHTTPRejectsInvalidRequests(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + blankName := organizationRequest(t, srv, http.MethodPost, organizationsPath, []byte(`{"name":" "}`), "") + require.Equal(http.StatusBadRequest, blankName.Code) + assert.Contains(blankName.Body.String(), "invalid_organization") + + unknownKind := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"conglomerate"}`), "") + require.Equal(http.StatusBadRequest, unknownKind.Code) + assert.Contains(unknownKind.Body.String(), "invalid_organization") + + unknownField := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","company":"nope"}`), "") + require.Equal(http.StatusBadRequest, unknownField.Code) + + retiredOnCreate := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Retired At Birth","retired":true}`), "") + require.Equal(http.StatusBadRequest, retiredOnCreate.Code) + assert.Contains(retiredOnCreate.Body.String(), `unknown field \"retired\"`) + + badID := organizationRequest(t, srv, http.MethodGet, organizationsPath+"/0", nil, "") + require.Equal(http.StatusBadRequest, badID.Code) + assert.Contains(badID.Body.String(), "invalid_organization_id") + + foreignTag := organizationRequest(t, srv, http.MethodDelete, organizationsPath+"/1", nil, `"person-1-r1"`) + require.Equal(http.StatusBadRequest, foreignTag.Code) + assert.Contains(foreignTag.Body.String(), "invalid_if_match") +} + +func TestOrganizationHTTPRejectsInvalidProfileValues(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + response := organizationRequest(t, srv, http.MethodPut, + fmt.Sprintf("%s/%d/profile", organizationsPath, created.ID), []byte(`{ + "contact_points":[{"contact_kind":"username","original_value":"example","service_slug":"missing-service","source":"user"}] + }`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusBadRequest, response.Code) + assert.Contains(response.Body.String(), "invalid_organization") +} + +func TestOrganizationHTTPRejectsUnknownProfileIdentifierKind(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + response := organizationRequest(t, srv, http.MethodPut, + fmt.Sprintf("%s/%d/profile", organizationsPath, created.ID), []byte(`{ + "identifiers":[{"identifier_kind":"bogus","identifier_value":"example","source":"user"}] + }`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusBadRequest, response.Code) + assert.Contains(response.Body.String(), "invalid_organization") +} + +func TestOrganizationHTTPDeleteWithEmploymentReturnsConflict(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "alice@example.com", "alice") + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + title := "Engineer" + _, err := st.AddEmploymentContext(context.Background(), store.EmploymentInput{ + PersonID: person.ID, OrganizationID: created.ID, Title: &title, Source: store.ProvenanceUser, + }) + require.NoError(err) + + deleteResponse := organizationRequest(t, srv, http.MethodDelete, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), nil, createdResponse.Header().Get("ETag")) + require.Equal(http.StatusConflict, deleteResponse.Code) + assert.Contains(deleteResponse.Body.String(), "organization_has_employments") + + retireResponse := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), + []byte(`{"name":"Example Org","kind":"company","retired":true}`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, retireResponse.Code) + var retired store.Organization + require.NoError(json.Unmarshal(retireResponse.Body.Bytes(), &retired)) + assert.NotNil(retired.RetiredAt) +} + +func TestOrganizationHTTPPatchReplacesFieldsAndLifecycleInOneRevision(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + patchedResponse := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", organizationsPath, created.ID), + []byte(`{"name":"Example Group","kind":"company","retired":true}`), + createdResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, patchedResponse.Code) + var patched store.Organization + require.NoError(json.Unmarshal(patchedResponse.Body.Bytes(), &patched)) + assert.Equal(created.Revision+1, patched.Revision) + assert.Equal("Example Group", patched.Name) + assert.NotNil(patched.RetiredAt) +} + +func TestOrganizationHTTPPatchRejectsMergedRowWithoutPartialFieldUpdate(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + survivor := mustAPIOrganization(t, st, "Example Org") + losing := mustAPIOrganization(t, st, "Former Org") + + _, err := st.MergeOrganizationsContext(context.Background(), + survivor.ID, survivor.Revision, losing.ID, losing.Revision) + require.NoError(err) + redirect, err := st.GetOrganizationContext(context.Background(), losing.ID) + require.NoError(err) + + response := organizationRequest(t, srv, http.MethodPatch, + fmt.Sprintf("%s/%d", organizationsPath, redirect.ID), + []byte(`{"name":"Hidden Rewrite","kind":"company","retired":false}`), + fmt.Sprintf(`"organization-%d-r%d"`, redirect.ID, redirect.Revision)) + require.Equal(http.StatusBadRequest, response.Code) + assert.Contains(response.Body.String(), "invalid_organization") + + unchanged, err := st.GetOrganizationContext(context.Background(), redirect.ID) + require.NoError(err) + assert.Equal("Former Org", unchanged.Name) + assert.Equal(redirect.Revision, unchanged.Revision) +} + +func TestConcurrentOrganizationHTTPPatchesShareOneRevisionCAS(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + + start := make(chan struct{}) + codes := make([]int, 2) + revisions := make([]int64, 2) + decodeErrors := make([]error, 2) + var wait sync.WaitGroup + for i, name := range []string{"First Group", "Second Group"} { + wait.Add(1) + go func(index int, replacement string) { + defer wait.Done() + <-start + response := organizationRequest(t, srv, http.MethodPatch, + organizationsPath+"/1", + fmt.Appendf(nil, `{"name":%q,"kind":"company","retired":true}`, replacement), + createdResponse.Header().Get("ETag")) + codes[index] = response.Code + var organization store.Organization + if response.Code == http.StatusOK { + decodeErrors[index] = json.Unmarshal(response.Body.Bytes(), &organization) + revisions[index] = organization.Revision + } + }(i, name) + } + close(start) + wait.Wait() + + assert.ElementsMatch([]int{http.StatusOK, http.StatusConflict}, codes) + for i, code := range codes { + if code == http.StatusOK { + require.NoError(decodeErrors[i]) + assert.Equal(int64(2), revisions[i]) + } + } +} + +func TestOrganizationHTTPProfileReplacementAndHistory(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + profilePath := fmt.Sprintf("%s/%d/profile", organizationsPath, created.ID) + firstResponse := organizationRequest(t, srv, http.MethodPut, profilePath, []byte(`{ + "names":[{"name":"Example Organisation","name_kind":"alias","source":"user"}], + "identifiers":[{"identifier_kind":"domain","identifier_value":"Example.com","source":"user"}], + "categories":[{"category":"Vendor","source":"user"}] + }`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, firstResponse.Code) + var first store.OrganizationProfile + require.NoError(json.Unmarshal(firstResponse.Body.Bytes(), &first)) + require.Len(first.Names, 1) + require.Len(first.Identifiers, 1) + assert.Equal("example.com", first.Identifiers[0].NormalizedValue) + + secondResponse := organizationRequest(t, srv, http.MethodPut, profilePath, + []byte(`{"identifiers":[{"identifier_kind":"domain","identifier_value":"Example.com","source":"user"}]}`), + firstResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, secondResponse.Code) + var second store.OrganizationProfile + require.NoError(json.Unmarshal(secondResponse.Body.Bytes(), &second)) + assert.Empty(second.Names) + require.Len(second.Identifiers, 1) + + historyResponse := organizationRequest(t, srv, http.MethodGet, + fmt.Sprintf("%s/%d/history", organizationsPath, created.ID), nil, "") + require.Equal(http.StatusOK, historyResponse.Code) + var history store.OrganizationProfile + require.NoError(json.Unmarshal(historyResponse.Body.Bytes(), &history)) + require.Len(history.Names, 1, "the superseded alias is visible in the explicit history view") + assert.NotNil(history.Names[0].Envelope.ActiveUntil) + + staleResponse := organizationRequest(t, srv, http.MethodPut, profilePath, []byte(`{}`), createdResponse.Header().Get("ETag")) + require.Equal(http.StatusConflict, staleResponse.Code) +} + +func TestOrganizationHTTPMergeMovesEmploymentAndBumpsBothRevisions(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + person := mustAPIPerson(t, st, "alice@example.com", "alice") + + survivorResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, survivorResponse.Code) + var survivor store.Organization + require.NoError(json.Unmarshal(survivorResponse.Body.Bytes(), &survivor)) + + losingResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org Inc","kind":"company"}`), "") + require.Equal(http.StatusCreated, losingResponse.Code) + var losing store.Organization + require.NoError(json.Unmarshal(losingResponse.Body.Bytes(), &losing)) + + title := "Engineer" + employment, err := st.AddEmploymentContext(context.Background(), store.EmploymentInput{ + PersonID: person.ID, OrganizationID: losing.ID, Title: &title, Source: store.ProvenanceUser, + }) + require.NoError(err) + + mergeResponse := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/%d/merge", organizationsPath, survivor.ID), + fmt.Appendf(nil, `{"losing_organization_id":%d,"losing_revision":%d}`, losing.ID, losing.Revision), + survivorResponse.Header().Get("ETag")) + require.Equal(http.StatusOK, mergeResponse.Code) + var merged store.Organization + require.NoError(json.Unmarshal(mergeResponse.Body.Bytes(), &merged)) + assert.Equal(survivor.ID, merged.ID) + assert.Equal(survivor.Revision+1, merged.Revision) + + moved, err := st.GetEmploymentContext(context.Background(), employment.ID) + require.NoError(err) + assert.Equal(survivor.ID, moved.OrganizationID) + + selfMerge := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/%d/merge", organizationsPath, survivor.ID), + fmt.Appendf(nil, `{"losing_organization_id":%d,"losing_revision":%d}`, survivor.ID, merged.Revision), + mergeResponse.Header().Get("ETag")) + require.Equal(http.StatusBadRequest, selfMerge.Code) + assert.Contains(selfMerge.Body.String(), "invalid_organization") +} + +func TestOrganizationHTTPDuplicateSuggestionsRefreshListAndResolve(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv := newOrganizationTestServer(t) + + for _, body := range []string{ + `{"name":"Example Org","kind":"company","primary_domain":"example.com"}`, + `{"name":"Example Org Inc","kind":"company","primary_domain":"example.com"}`, + } { + response := organizationRequest(t, srv, http.MethodPost, organizationsPath, []byte(body), "") + require.Equal(http.StatusCreated, response.Code) + } + + refreshResponse := organizationRequest(t, srv, http.MethodPost, + organizationsPath+"/duplicate-suggestions/refresh", nil, "") + require.Equal(http.StatusOK, refreshResponse.Code) + var refreshed RefreshOrganizationDuplicateSuggestionsResponse + require.NoError(json.Unmarshal(refreshResponse.Body.Bytes(), &refreshed)) + assert.Equal(1, refreshed.Created) + + listResponse := organizationRequest(t, srv, http.MethodGet, + organizationsPath+"/duplicate-suggestions?status=open", nil, "") + require.Equal(http.StatusOK, listResponse.Code) + var listed OrganizationDuplicateSuggestionsResponse + require.NoError(json.Unmarshal(listResponse.Body.Bytes(), &listed)) + require.Len(listed.Suggestions, 1) + assert.Equal("domain", listed.Suggestions[0].Criterion) + + resolveResponse := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/duplicate-suggestions/%d/resolve", organizationsPath, listed.Suggestions[0].ID), + []byte(`{"status":"rejected","note":"Separate legal entities."}`), "") + require.Equal(http.StatusOK, resolveResponse.Code) + var resolved store.OrganizationDuplicateSuggestion + require.NoError(json.Unmarshal(resolveResponse.Body.Bytes(), &resolved)) + assert.Equal("rejected", resolved.Status) + + repeatedResolution := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/duplicate-suggestions/%d/resolve", organizationsPath, listed.Suggestions[0].ID), + []byte(`{"status":"accepted"}`), "") + require.Equal(http.StatusConflict, repeatedResolution.Code) + assert.Contains(repeatedResolution.Body.String(), "duplicate_suggestion_already_resolved") + + badStatus := organizationRequest(t, srv, http.MethodPost, + fmt.Sprintf("%s/duplicate-suggestions/%d/resolve", organizationsPath, listed.Suggestions[0].ID), + []byte(`{"status":"maybe"}`), "") + require.Equal(http.StatusBadRequest, badStatus.Code) + assert.Contains(badStatus.Body.String(), "invalid_duplicate_suggestion") +} + +func TestOrganizationHTTPAttributesListAndSet(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + srv, st := newOrganizationTestServerWithStore(t) + definition := mustAPIOrganizationAttributeDefinition(t, st, "industry_focus") + + createdResponse := organizationRequest(t, srv, http.MethodPost, organizationsPath, + []byte(`{"name":"Example Org","kind":"company"}`), "") + require.Equal(http.StatusCreated, createdResponse.Code) + var created store.Organization + require.NoError(json.Unmarshal(createdResponse.Body.Bytes(), &created)) + + attributesPath := fmt.Sprintf("%s/%d/attributes", organizationsPath, created.ID) + setResponse := organizationRequest(t, srv, http.MethodPost, attributesPath, + []byte(`{"definition_slug":"industry_focus","value":{"type":"text","text":"archival software"},"source":"user"}`), "") + require.Equal(http.StatusCreated, setResponse.Code) + assert.Equal("industry_focus", definition.Slug) + + listResponse := organizationRequest(t, srv, http.MethodGet, attributesPath, nil, "") + require.Equal(http.StatusOK, listResponse.Code) + var listed OrganizationAttributesResponse + require.NoError(json.Unmarshal(listResponse.Body.Bytes(), &listed)) + require.Len(listed.Values, 1) + + historyResponse := organizationRequest(t, srv, http.MethodGet, + attributesPath+"?include_superseded=true", nil, "") + require.Equal(http.StatusOK, historyResponse.Code) + var history OrganizationAttributesResponse + require.NoError(json.Unmarshal(historyResponse.Body.Bytes(), &history)) + assert.Len(history.Values, 1) +} + +func newOrganizationTestServer(t *testing.T) *Server { + t.Helper() + srv, _ := newOrganizationTestServerWithStore(t) + return srv +} + +func newOrganizationTestServerWithStore(t *testing.T) (*Server, *store.Store) { + t.Helper() + st := testutil.NewTestStore(t) + srv := NewServerWithOptions(ServerOptions{ + Config: &config.Config{Server: config.ServerConfig{APIPort: 8080}}, + Store: st, + Logger: testLogger(), + }) + t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) }) + return srv, st +} + +func mustAPIOrganizationAttributeDefinition(t *testing.T, st *store.Store, slug string) *store.AttributeDefinition { + t.Helper() + definition, err := st.CreateAttributeDefinitionContext(context.Background(), store.AttributeDefinitionInput{ + UniversalID: "test-organization-" + slug, + ObjectType: store.AttributeObjectOrganization, + Slug: slug, Label: slug, + ValueType: store.AttributeValueText, FieldType: store.AttributeFieldText, + Cardinality: store.AttributeCardinalitySingle, + Ownership: store.AttributeOwnershipUser, + APIMutable: true, UICreatable: true, UIEditable: true, IsDeletable: true, + }) + require.NoError(t, err) + return definition +} + +func mustAPIPerson(t *testing.T, st *store.Store, email, name string) *store.Person { + t.Helper() + participantID, err := st.EnsureParticipant(email, name, "example.com") + require.NoError(t, err) + person, _, err := st.CreatePersonFromParticipant(participantID) + require.NoError(t, err) + return person +} + +func organizationRequest(t *testing.T, srv *Server, method, path string, body []byte, ifMatch string) *httptest.ResponseRecorder { + t.Helper() + req := httptest.NewRequest(method, path, bytes.NewReader(body)) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if ifMatch != "" { + req.Header.Set("If-Match", ifMatch) + } + response := httptest.NewRecorder() + srv.Router().ServeHTTP(response, req) + return response +} diff --git a/internal/api/routes.go b/internal/api/routes.go index 92531334c..aff950715 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -222,6 +222,8 @@ func (s *Server) registerHumaRoutes(api huma.API, apiV1 huma.API) { s.registerExploreRoutes(apiV1) s.registerFilesRoutes(apiV1) s.registerPersonProfileRoutes(apiV1) + s.registerOrganizationRoutes(apiV1) + s.registerEmploymentRoutes(apiV1) s.registerPersonProfileValueRoutes(apiV1) s.registerCommunicationServiceRoutes(apiV1) s.registerAttributeDefinitionRoutes(apiV1) diff --git a/internal/store/attribute_definitions.go b/internal/store/attribute_definitions.go index c9048671a..86a0abc40 100644 --- a/internal/store/attribute_definitions.go +++ b/internal/store/attribute_definitions.go @@ -763,8 +763,11 @@ func (s *Store) DeleteAttributeDefinitionContext( return ErrAttributeDefinitionNotDeletable } var values int - if err := tx.QueryRowContext(ctx, - `SELECT COUNT(*) FROM person_attribute_values WHERE definition_id = ?`, id, + if err := tx.QueryRowContext(ctx, ` + SELECT + (SELECT COUNT(*) FROM person_attribute_values WHERE definition_id = ?) + + (SELECT COUNT(*) FROM organization_attribute_values WHERE definition_id = ?) + `, id, id, ).Scan(&values); err != nil { return fmt.Errorf("count values for attribute definition %d: %w", id, err) } diff --git a/internal/store/attribute_values.go b/internal/store/attribute_values.go index 30900e2e2..8d2411e18 100644 --- a/internal/store/attribute_values.go +++ b/internal/store/attribute_values.go @@ -24,6 +24,27 @@ var errAttributeDryRun = errors.New("attribute dry run rollback") const maxAttributeWriteAttempts = 5 +// retryAttributeWrite retries read-then-write transactions that can fail +// snapshot upgrade (SQLITE_BUSY), deadlock, or race the current-value unique +// index. All attribute owners use the same bounded policy. +func retryAttributeWrite[T any]( + s *Store, operation string, attempt func() (*T, error), +) (*T, error) { + var lastErr error + for range maxAttributeWriteAttempts { + write, err := attempt() + if err == nil { + return write, nil + } + if !s.dialect.IsConflictError(err) && !s.dialect.IsBusyError(err) { + return nil, err + } + lastErr = err + } + return nil, fmt.Errorf("%s: gave up after %d attempts: %w", + operation, maxAttributeWriteAttempts, lastErr) +} + // AttributeValue is a typed value union with exactly one populated member. type AttributeValue struct { Type AttributeValueType `json:"type"` diff --git a/internal/store/employment_projection.go b/internal/store/employment_projection.go new file mode 100644 index 000000000..56fbe4cc9 --- /dev/null +++ b/internal/store/employment_projection.go @@ -0,0 +1,110 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" +) + +// EmploymentProjection is the derived current-company view of a person. It is +// computed from the primary current employment on every read and is never +// stored: the roadmap forbids mutable person.company and person.job_title +// copies precisely so these values cannot drift from employment history. +// Empty strings mean the underlying employment left that field unset. +type EmploymentProjection struct { + PersonID int64 `json:"person_id"` + EmploymentID int64 `json:"employment_id"` + OrganizationID int64 `json:"organization_id"` + OrganizationName string `json:"organization_name"` + Title string `json:"title"` + Role string `json:"role"` + Department string `json:"department"` +} + +// PrimaryCurrentEmploymentContext returns the person's derived company and +// title. found is false when the person has no primary current employment, +// which is a normal state and not an error. +func (s *Store) PrimaryCurrentEmploymentContext( + ctx context.Context, personID int64, +) (EmploymentProjection, bool, error) { + projection, err := scanEmploymentProjection(s.db.QueryRowContext(ctx, fmt.Sprintf(` + SELECT e.person_id, e.id, e.organization_id, o.name, + COALESCE(e.title, ''), COALESCE(e.role, ''), COALESCE(e.department, '') + FROM employments e + JOIN organizations o ON o.id = e.organization_id + WHERE e.person_id = ? AND %s AND %s + `, s.dialect.BoolTrueExpr("e.is_primary"), s.dialect.BoolTrueExpr("e.is_current")), personID)) + if errors.Is(err, sql.ErrNoRows) { + return EmploymentProjection{}, false, nil + } + if err != nil { + return EmploymentProjection{}, false, fmt.Errorf("project primary current employment for person %d: %w", personID, err) + } + return projection, true, nil +} + +// PrimaryCurrentEmploymentsContext resolves projections for many people in one +// query so a people listing never issues a query per row. People without a +// primary current employment are absent from the map rather than mapped to a +// zero value. +func (s *Store) PrimaryCurrentEmploymentsContext( + ctx context.Context, personIDs []int64, +) (map[int64]EmploymentProjection, error) { + projections := make(map[int64]EmploymentProjection) + ids := make([]int64, 0, len(personIDs)) + for _, personID := range personIDs { + if personID > 0 { + ids = append(ids, personID) + } + } + if len(ids) == 0 { + return projections, nil + } + + placeholders := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",") + args := make([]any, len(ids)) + for i, personID := range ids { + args[i] = personID + } + rows, err := s.db.QueryContext(ctx, fmt.Sprintf(` + SELECT e.person_id, e.id, e.organization_id, o.name, + COALESCE(e.title, ''), COALESCE(e.role, ''), COALESCE(e.department, '') + FROM employments e + JOIN organizations o ON o.id = e.organization_id + WHERE e.person_id IN (%s) AND %s AND %s + `, placeholders, s.dialect.BoolTrueExpr("e.is_primary"), s.dialect.BoolTrueExpr("e.is_current")), args...) + if err != nil { + return nil, fmt.Errorf("query primary current employment projections: %w", err) + } + defer func() { _ = rows.Close() }() + for rows.Next() { + projection, err := scanEmploymentProjection(rows) + if err != nil { + return nil, fmt.Errorf("scan primary current employment projection: %w", err) + } + projections[projection.PersonID] = projection + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate primary current employment projections: %w", err) + } + return projections, nil +} + +func scanEmploymentProjection(row scanner) (EmploymentProjection, error) { + var projection EmploymentProjection + err := row.Scan( + &projection.PersonID, + &projection.EmploymentID, + &projection.OrganizationID, + &projection.OrganizationName, + &projection.Title, + &projection.Role, + &projection.Department, + ) + if err != nil { + return EmploymentProjection{}, err + } + return projection, nil +} diff --git a/internal/store/employment_projection_test.go b/internal/store/employment_projection_test.go new file mode 100644 index 000000000..6eed9f787 --- /dev/null +++ b/internal/store/employment_projection_test.go @@ -0,0 +1,141 @@ +package store_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestPrimaryCurrentEmploymentProjectsCompanyAndTitle(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + + _, found, err := st.PrimaryCurrentEmploymentContext(ctx, person.ID) + require.NoError(err) + assert.False(found, "a person with no employment has no projection") + + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, + OrganizationID: organization.ID, + Title: new("Staff Engineer"), + Role: new("Engineering"), + Department: new("Archive Platform"), + Source: store.ProvenanceUser, + }) + require.NoError(err) + + projection, found, err := st.PrimaryCurrentEmploymentContext(ctx, person.ID) + require.NoError(err) + require.True(found) + assert.Equal(store.EmploymentProjection{ + PersonID: person.ID, + EmploymentID: employment.ID, + OrganizationID: organization.ID, + OrganizationName: "Example Org", + Title: "Staff Engineer", + Role: "Engineering", + Department: "Archive Platform", + }, projection) +} + +func TestEndingThePrimaryEmploymentClearsTheProjectionWithoutDeletingHistory(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: organization.ID, + Title: new("Staff Engineer"), Source: store.ProvenanceUser, + }) + require.NoError(err) + + endDate, err := store.ParsePartialDate("2026-06") + require.NoError(err) + _, err = st.EndEmploymentContext(ctx, employment.ID, employment.Revision, endDate) + require.NoError(err) + + _, found, err := st.PrimaryCurrentEmploymentContext(ctx, person.ID) + require.NoError(err) + assert.False(found, "the projection follows the primary current row") + + history, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{PersonID: person.ID}) + require.NoError(err) + require.Len(history, 1, "history is intact after the projection clears") + assert.Equal(employment.ID, history[0].ID) +} + +func TestPrimaryCurrentEmploymentFollowsRotation(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + dayJob := mustOrganization(t, st, "Example Org") + sideJob := mustOrganization(t, st, "Another Org") + + _, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: dayJob.ID, + Title: new("Staff Engineer"), Source: store.ProvenanceUser, + }) + require.NoError(err) + side, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: sideJob.ID, + Title: new("Advisor"), Source: store.ProvenanceUser, + }) + require.NoError(err) + + _, err = st.SetPrimaryEmploymentContext(ctx, side.ID, side.Revision) + require.NoError(err) + + projection, found, err := st.PrimaryCurrentEmploymentContext(ctx, person.ID) + require.NoError(err) + require.True(found) + assert.Equal("Another Org", projection.OrganizationName) + assert.Equal("Advisor", projection.Title) +} + +func TestPrimaryCurrentEmploymentsBatchesByPerson(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + alice := mustPromotedPerson(t, st, "alice@example.com", "alice") + bob := mustPromotedPerson(t, st, "bob@example.com", "bob") + carol := mustPromotedPerson(t, st, "carol@example.com", "carol") + organization := mustOrganization(t, st, "Example Org") + + _, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: alice.ID, OrganizationID: organization.ID, + Title: new("Engineer"), Source: store.ProvenanceUser, + }) + require.NoError(err) + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: bob.ID, OrganizationID: organization.ID, + Title: new("Designer"), Source: store.ProvenanceUser, + }) + require.NoError(err) + + projections, err := st.PrimaryCurrentEmploymentsContext(ctx, + []int64{alice.ID, bob.ID, carol.ID}) + require.NoError(err) + require.Len(projections, 2, "a person with no primary current row is absent, not zero-valued") + assert.Equal("Engineer", projections[alice.ID].Title) + assert.Equal("Designer", projections[bob.ID].Title) + _, present := projections[carol.ID] + assert.False(present) + + empty, err := st.PrimaryCurrentEmploymentsContext(ctx, nil) + require.NoError(err) + assert.Empty(empty) +} diff --git a/internal/store/employments.go b/internal/store/employments.go new file mode 100644 index 000000000..6d2292244 --- /dev/null +++ b/internal/store/employments.go @@ -0,0 +1,623 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "slices" + "strings" + "time" +) + +var ( + ErrEmploymentNotFound = errors.New("employment not found") + ErrEmploymentRevisionConflict = errors.New("employment revision conflict") + ErrEmploymentInvalid = errors.New("invalid employment") + ErrEmploymentPrimaryConflict = errors.New("person already has a primary current employment") + ErrEmploymentDuplicateActive = errors.New("person already has a current employment with this organization and title") +) + +type Employment struct { + ID int64 `json:"id"` + PersonID int64 `json:"person_id"` + OrganizationID int64 `json:"organization_id"` + Title *string `json:"title,omitempty"` + Role *string `json:"role,omitempty"` + Department *string `json:"department,omitempty"` + Location *string `json:"location,omitempty"` + AddressID *int64 `json:"address_id,omitempty"` + Description *string `json:"description,omitempty"` + StartDate *PartialDate `json:"start_date,omitempty"` + EndDate *PartialDate `json:"end_date,omitempty"` + IsCurrent bool `json:"is_current"` + IsPrimary bool `json:"is_primary"` + Source Provenance `json:"source"` + SourceRef *string `json:"source_ref,omitempty"` + Confidence *float64 `json:"confidence,omitempty"` + Revision int64 `json:"revision"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EmploymentInput struct { + PersonID, OrganizationID int64 + Title, Role, Department, Location *string + AddressID *int64 + Description *string + StartDate, EndDate *PartialDate + IsCurrent, IsPrimary *bool + Source Provenance + SourceRef *string + Confidence *float64 +} + +type EmploymentFilter struct { + PersonID, OrganizationID int64 + CurrentOnly bool + Limit, Offset int +} + +const ( + DefaultEmploymentPageSize = 200 + MaxEmploymentPageSize = 1000 +) + +const employmentColumns = ` + id, person_id, organization_id, title, role, department, location, address_id, + description, start_year, start_month, start_day, end_year, end_month, end_day, + is_current, is_primary, source, source_ref, confidence, revision, created_at, updated_at` + +func (s *Store) AddEmploymentContext(ctx context.Context, input EmploymentInput) (*Employment, error) { + input, err := validateEmploymentInput(input) + if err != nil { + return nil, err + } + var employment *Employment + err = s.withTxContext(ctx, func(tx *loggedTx) error { + if err := s.lockEmploymentPeopleTx(ctx, tx, input.PersonID); err != nil { + return err + } + if err := s.verifyEmploymentReferencesTx(ctx, tx, input); err != nil { + return err + } + current := employmentCurrent(input) + primary, err := s.resolveEmploymentPrimaryTx(ctx, tx, input, current, 0) + if err != nil { + return err + } + employment, err = s.insertEmploymentTx(ctx, tx, input, current, primary) + return err + }) + if err != nil { + return nil, err + } + return employment, nil +} + +func (s *Store) GetEmploymentContext(ctx context.Context, id int64) (*Employment, error) { + employment, err := scanEmployment(s.db.QueryRowContext(ctx, `SELECT `+employmentColumns+` FROM employments WHERE id = ?`, id)) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrEmploymentNotFound + } + if err != nil { + return nil, fmt.Errorf("get employment %d: %w", id, err) + } + return employment, nil +} + +func (s *Store) UpdateEmploymentContext(ctx context.Context, id, expectedRevision int64, input EmploymentInput) (*Employment, error) { + input, err := validateEmploymentInput(input) + if err != nil { + return nil, err + } + var employment *Employment + err = s.withTxContext(ctx, func(tx *loggedTx) error { + snapshot, err := getEmploymentTx(ctx, tx, id) + if err != nil { + return err + } + if snapshot.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + if err := s.lockEmploymentPeopleTx( + ctx, tx, snapshot.PersonID, input.PersonID); err != nil { + return err + } + if err := s.verifyEmploymentReferencesTx(ctx, tx, input); err != nil { + return err + } + currentEmployment, err := getEmploymentForUpdateTx(ctx, tx, s.dialect, id) + if err != nil { + return err + } + if currentEmployment.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + current := employmentCurrent(input) + primary, err := s.resolveEmploymentPrimaryTx(ctx, tx, input, current, id) + if err != nil { + return err + } + args := employmentWriteArgs(input, current, primary) + args = append(args, id, expectedRevision) + employment, err = s.employmentWriteWithConflictSavepointTx( + ctx, tx, input, current, id, func() (*Employment, error) { + return scanEmployment(tx.QueryRowContext(ctx, fmt.Sprintf(` + UPDATE employments SET person_id = ?, organization_id = ?, title = ?, title_normalized = ?, + role = ?, department = ?, location = ?, address_id = ?, description = ?, + start_year = ?, start_month = ?, start_day = ?, end_year = ?, end_month = ?, end_day = ?, + is_current = ?, is_primary = ?, source = ?, source_ref = ?, confidence = ?, + revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? RETURNING %s`, s.dialect.Now(), employmentColumns), args...)) + }) + if errors.Is(err, sql.ErrNoRows) { + return s.employmentCASMissTx(ctx, tx, id) + } + if err != nil { + if errors.Is(err, ErrEmploymentDuplicateActive) || errors.Is(err, ErrEmploymentPrimaryConflict) { + return err + } + return fmt.Errorf("update employment %d: %w", id, err) + } + return nil + }) + if err != nil { + return nil, err + } + return employment, nil +} + +func (s *Store) EndEmploymentContext(ctx context.Context, id, expectedRevision int64, endDate PartialDate) (*Employment, error) { + if err := validateEmploymentDate(endDate, "end"); err != nil { + return nil, err + } + var employment *Employment + err := s.withTxContext(ctx, func(tx *loggedTx) error { + snapshot, err := getEmploymentTx(ctx, tx, id) + if err != nil { + return err + } + if snapshot.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + if err := s.lockEmploymentPeopleTx(ctx, tx, snapshot.PersonID); err != nil { + return err + } + current, err := getEmploymentForUpdateTx(ctx, tx, s.dialect, id) + if err != nil { + return err + } + if current.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + if current.StartDate != nil && CompareAtSharedPrecision(endDate, *current.StartDate) < 0 { + return fmt.Errorf("%w: end date must not precede start date", ErrEmploymentInvalid) + } + args := append(PartialDateArgs(endDate), false, false, id, expectedRevision) + employment, err = scanEmployment(tx.QueryRowContext(ctx, fmt.Sprintf(` + UPDATE employments SET end_year = ?, end_month = ?, end_day = ?, is_current = ?, is_primary = ?, + revision = revision + 1, updated_at = %s WHERE id = ? AND revision = ? RETURNING %s`, s.dialect.Now(), employmentColumns), args...)) + if errors.Is(err, sql.ErrNoRows) { + return s.employmentCASMissTx(ctx, tx, id) + } + if err != nil { + return fmt.Errorf("end employment %d: %w", id, err) + } + return nil + }) + if err != nil { + return nil, err + } + return employment, nil +} + +func (s *Store) SetPrimaryEmploymentContext(ctx context.Context, id, expectedRevision int64) (*Employment, error) { + var employment *Employment + err := s.withTxContext(ctx, func(tx *loggedTx) error { + snapshot, err := getEmploymentTx(ctx, tx, id) + if err != nil { + return err + } + if snapshot.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + if err := s.lockEmploymentPeopleTx(ctx, tx, snapshot.PersonID); err != nil { + return err + } + current, err := getEmploymentForUpdateTx(ctx, tx, s.dialect, id) + if err != nil { + return err + } + if current.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + if !current.IsCurrent { + return fmt.Errorf("%w: a historical employment cannot be primary", ErrEmploymentInvalid) + } + if _, err := tx.ExecContext(ctx, fmt.Sprintf(`UPDATE employments SET is_primary = ?, revision = revision + 1, updated_at = %s WHERE person_id = ? AND id <> ? AND %s`, s.dialect.Now(), s.dialect.BoolTrueExpr("is_primary")), false, current.PersonID, id); err != nil { + return fmt.Errorf("demote primary employment: %w", err) + } + employment, err = scanEmployment(tx.QueryRowContext(ctx, fmt.Sprintf(`UPDATE employments SET is_primary = ?, revision = revision + 1, updated_at = %s WHERE id = ? AND revision = ? RETURNING %s`, s.dialect.Now(), employmentColumns), true, id, expectedRevision)) + if errors.Is(err, sql.ErrNoRows) { + return s.employmentCASMissTx(ctx, tx, id) + } + if err != nil { + return fmt.Errorf("promote employment %d: %w", id, err) + } + return nil + }) + if err != nil { + return nil, err + } + return employment, nil +} + +func (s *Store) DeleteEmploymentContext(ctx context.Context, id, expectedRevision int64) error { + return s.withTxContext(ctx, func(tx *loggedTx) error { + snapshot, err := getEmploymentTx(ctx, tx, id) + if err != nil { + return err + } + if snapshot.Revision != expectedRevision { + return ErrEmploymentRevisionConflict + } + if err := s.lockEmploymentPeopleTx(ctx, tx, snapshot.PersonID); err != nil { + return err + } + var deletedID int64 + err = tx.QueryRowContext(ctx, + `DELETE FROM employments WHERE id = ? AND revision = ? RETURNING id`, + id, expectedRevision).Scan(&deletedID) + if errors.Is(err, sql.ErrNoRows) { + return s.employmentCASMissTx(ctx, tx, id) + } + if err != nil { + return fmt.Errorf("delete employment %d: %w", id, err) + } + return nil + }) +} + +func (s *Store) ListEmploymentsContext(ctx context.Context, filter EmploymentFilter) ([]Employment, error) { + if filter.PersonID <= 0 && filter.OrganizationID <= 0 { + return nil, fmt.Errorf("%w: person id or organization id is required", ErrEmploymentInvalid) + } + conditions, args := make([]string, 0, 3), make([]any, 0, 4) + if filter.PersonID > 0 { + conditions, args = append(conditions, "person_id = ?"), append(args, filter.PersonID) + } + if filter.OrganizationID > 0 { + conditions, args = append(conditions, "organization_id = ?"), append(args, filter.OrganizationID) + } + current := s.dialect.BoolTrueExpr("is_current") + if filter.CurrentOnly { + conditions = append(conditions, current) + } + limit := filter.Limit + if limit <= 0 { + limit = DefaultEmploymentPageSize + } else if limit > MaxEmploymentPageSize { + limit = MaxEmploymentPageSize + } + args = append(args, limit, max(filter.Offset, 0)) + query := `SELECT ` + employmentColumns + ` FROM employments WHERE ` + strings.Join(conditions, " AND ") + fmt.Sprintf(` ORDER BY + CASE WHEN %s THEN 0 ELSE 1 END, CASE WHEN %s THEN 0 ELSE 1 END, + COALESCE(end_year, 9999) DESC, COALESCE(end_month, 12) DESC, COALESCE(end_day, 31) DESC, + COALESCE(start_year, 0) DESC, COALESCE(start_month, 1) DESC, COALESCE(start_day, 1) DESC, id LIMIT ? OFFSET ?`, current, s.dialect.BoolTrueExpr("is_primary")) + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("list employments: %w", err) + } + defer func() { _ = rows.Close() }() + employments := make([]Employment, 0) + for rows.Next() { + employment, err := scanEmployment(rows) + if err != nil { + return nil, fmt.Errorf("scan employment: %w", err) + } + employments = append(employments, *employment) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("list employments: %w", err) + } + return employments, nil +} + +func NormalizeEmploymentTitle(title *string) string { + if title == nil { + return "" + } + return strings.ToLower(strings.Join(strings.Fields(*title), " ")) +} + +func validateEmploymentInput(input EmploymentInput) (EmploymentInput, error) { + if input.PersonID <= 0 { + return input, fmt.Errorf("%w: person id must be positive", ErrEmploymentInvalid) + } + if input.OrganizationID <= 0 { + return input, fmt.Errorf("%w: organization id must be positive", ErrEmploymentInvalid) + } + input.Title = trimEmploymentString(input.Title) + input.Role = trimEmploymentString(input.Role) + input.Department = trimEmploymentString(input.Department) + input.Location = trimEmploymentString(input.Location) + input.Description = trimEmploymentString(input.Description) + input.SourceRef = trimEmploymentString(input.SourceRef) + if input.Source == "" { + input.Source = ProvenanceUser + } else { + source, err := ParseProvenance(string(input.Source)) + if err != nil { + return input, err + } + input.Source = source + } + if input.Confidence != nil && (*input.Confidence < 0 || *input.Confidence > 1) { + return input, fmt.Errorf("%w: confidence must be between 0 and 1", ErrEmploymentInvalid) + } + if input.Confidence != nil && input.Source.IsDeclared() { + return input, fmt.Errorf("%w: confidence is only meaningful for derived or suggested values", ErrEmploymentInvalid) + } + if input.StartDate != nil { + if err := validateEmploymentDate(*input.StartDate, "start"); err != nil { + return input, err + } + } + if input.EndDate != nil { + if err := validateEmploymentDate(*input.EndDate, "end"); err != nil { + return input, err + } + } + if input.StartDate != nil && input.EndDate != nil && CompareAtSharedPrecision(*input.EndDate, *input.StartDate) < 0 { + return input, fmt.Errorf("%w: end date must not precede start date", ErrEmploymentInvalid) + } + return input, nil +} + +func validateEmploymentDate(date PartialDate, label string) error { + if err := date.Validate(); err != nil { + return fmt.Errorf("%w: %s date is invalid: %w", ErrEmploymentInvalid, label, err) + } + if date.Year == nil { + return fmt.Errorf("%w: %s date requires a year", ErrEmploymentInvalid, label) + } + return nil +} +func trimEmploymentString(value *string) *string { + if value == nil { + return nil + } + trimmed := strings.TrimSpace(*value) + if trimmed == "" { + return nil + } + return &trimmed +} +func employmentCurrent(input EmploymentInput) bool { + return input.IsCurrent == nil && input.EndDate == nil || input.IsCurrent != nil && *input.IsCurrent +} + +func (s *Store) verifyEmploymentReferencesTx(ctx context.Context, tx *loggedTx, input EmploymentInput) error { + var exists bool + organization, err := getOrganizationForUpdateTx( + ctx, tx, s.dialect, input.OrganizationID) + if err != nil { + return err + } + if organization.MergedIntoID != nil { + return fmt.Errorf("%w: employment cannot target a merged organization", + ErrOrganizationInvalid) + } + if input.AddressID != nil { + if err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM organization_addresses WHERE id = ? AND organization_id = ?)`, *input.AddressID, input.OrganizationID).Scan(&exists); err != nil { + return fmt.Errorf("verify employment address: %w", err) + } + if !exists { + return fmt.Errorf("%w: address %d does not belong to organization %d", ErrEmploymentInvalid, *input.AddressID, input.OrganizationID) + } + } + return nil +} + +func (s *Store) lockEmploymentPeopleTx( + ctx context.Context, tx *loggedTx, personIDs ...int64, +) error { + ids := append([]int64(nil), personIDs...) + slices.Sort(ids) + var previous int64 + for index, personID := range ids { + if index > 0 && personID == previous { + continue + } + var lockedID int64 + err := tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT id FROM persons WHERE id = ?%s + `, s.dialect.SelectForUpdate()), personID).Scan(&lockedID) + if errors.Is(err, sql.ErrNoRows) { + return ErrPersonNotFound + } + if err != nil { + return fmt.Errorf("lock employment person %d: %w", personID, err) + } + previous = personID + } + return nil +} + +func (s *Store) resolveEmploymentPrimaryTx(ctx context.Context, tx *loggedTx, input EmploymentInput, current bool, excludeID int64) (bool, error) { + primaryExists, err := s.hasPrimaryCurrentTx(ctx, tx, input.PersonID, excludeID) + if err != nil { + return false, err + } + if input.IsPrimary == nil { + return current && !primaryExists, nil + } + if !*input.IsPrimary { + return false, nil + } + if !current { + return false, fmt.Errorf("%w: a historical employment cannot be primary", ErrEmploymentInvalid) + } + if primaryExists { + return false, ErrEmploymentPrimaryConflict + } + return true, nil +} + +func (s *Store) hasPrimaryCurrentTx(ctx context.Context, tx *loggedTx, personID, excludeID int64) (bool, error) { + var exists bool + err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM employments WHERE person_id = ? AND id <> ? AND `+s.dialect.BoolTrueExpr("is_primary")+` AND `+s.dialect.BoolTrueExpr("is_current")+`)`, personID, excludeID).Scan(&exists) + return exists, err +} + +func (s *Store) insertEmploymentTx(ctx context.Context, tx *loggedTx, input EmploymentInput, current, primary bool) (*Employment, error) { + employment, err := s.employmentWriteWithConflictSavepointTx(ctx, tx, input, current, 0, + func() (*Employment, error) { + return scanEmployment(tx.QueryRowContext(ctx, `INSERT INTO employments (person_id, organization_id, title, title_normalized, role, department, location, address_id, description, start_year, start_month, start_day, end_year, end_month, end_day, is_current, is_primary, source, source_ref, confidence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING `+employmentColumns, employmentWriteArgs(input, current, primary)...)) + }) + if err != nil { + if errors.Is(err, ErrEmploymentDuplicateActive) || errors.Is(err, ErrEmploymentPrimaryConflict) { + return nil, err + } + return nil, fmt.Errorf("add employment: %w", err) + } + return employment, nil +} + +func employmentWriteArgs(input EmploymentInput, current, primary bool) []any { + start, end := partialDateArgs(input.StartDate), partialDateArgs(input.EndDate) + return []any{input.PersonID, input.OrganizationID, input.Title, NormalizeEmploymentTitle(input.Title), input.Role, input.Department, input.Location, input.AddressID, input.Description, start[0], start[1], start[2], end[0], end[1], end[2], current, primary, input.Source, input.SourceRef, input.Confidence} +} +func partialDateArgs(date *PartialDate) []any { + if date == nil { + return []any{nil, nil, nil} + } + return PartialDateArgs(*date) +} + +func (s *Store) classifyEmploymentConflictTx(ctx context.Context, tx *loggedTx, input EmploymentInput, current bool, excludeID int64) error { + if current { + var duplicate bool + err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM employments WHERE person_id = ? AND organization_id = ? AND title_normalized = ? AND id <> ? AND `+s.dialect.BoolTrueExpr("is_current")+`)`, input.PersonID, input.OrganizationID, NormalizeEmploymentTitle(input.Title), excludeID).Scan(&duplicate) + if err != nil { + return fmt.Errorf("classify employment conflict: %w", err) + } + if duplicate { + return ErrEmploymentDuplicateActive + } + } + primary, err := s.hasPrimaryCurrentTx(ctx, tx, input.PersonID, excludeID) + if err != nil { + return fmt.Errorf("classify employment conflict: %w", err) + } + if primary { + return ErrEmploymentPrimaryConflict + } + return ErrEmploymentDuplicateActive +} + +// employmentWriteWithConflictSavepointTx recovers a unique violation before +// asking the database which partial index was hit. PostgreSQL aborts the whole +// transaction after SQLSTATE 23505; rolling back to the savepoint restores a +// queryable transaction while SQLite preserves the same classification path. +func (s *Store) employmentWriteWithConflictSavepointTx( + ctx context.Context, tx *loggedTx, input EmploymentInput, current bool, excludeID int64, + write func() (*Employment, error), +) (*Employment, error) { + const savepoint = "employment_write" + if _, err := tx.ExecContext(ctx, "SAVEPOINT "+savepoint); err != nil { + return nil, fmt.Errorf("create employment write savepoint: %w", err) + } + employment, err := write() + if err == nil { + if _, releaseErr := tx.ExecContext(ctx, "RELEASE SAVEPOINT "+savepoint); releaseErr != nil { + return nil, fmt.Errorf("release employment write savepoint: %w", releaseErr) + } + return employment, nil + } + if _, rollbackErr := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT "+savepoint); rollbackErr != nil { + return nil, fmt.Errorf("rollback employment write savepoint: %w", rollbackErr) + } + if _, releaseErr := tx.ExecContext(ctx, "RELEASE SAVEPOINT "+savepoint); releaseErr != nil { + return nil, fmt.Errorf("release rolled-back employment write savepoint: %w", releaseErr) + } + if s.dialect.IsConflictError(err) { + return nil, s.classifyEmploymentConflictTx(ctx, tx, input, current, excludeID) + } + return nil, err +} + +func (s *Store) employmentCASMissTx(ctx context.Context, tx *loggedTx, id int64) error { + var revision int64 + err := tx.QueryRowContext(ctx, `SELECT revision FROM employments WHERE id = ?`, id).Scan(&revision) + if errors.Is(err, sql.ErrNoRows) { + return ErrEmploymentNotFound + } + if err != nil { + return fmt.Errorf("check employment %d after revision miss: %w", id, err) + } + return ErrEmploymentRevisionConflict +} +func getEmploymentTx(ctx context.Context, tx *loggedTx, id int64) (*Employment, error) { + employment, err := scanEmployment(tx.QueryRowContext(ctx, `SELECT `+employmentColumns+` FROM employments WHERE id = ?`, id)) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrEmploymentNotFound + } + if err != nil { + return nil, fmt.Errorf("get employment %d: %w", id, err) + } + return employment, nil +} + +func getEmploymentForUpdateTx( + ctx context.Context, tx *loggedTx, dialect Dialect, id int64, +) (*Employment, error) { + employment, err := scanEmployment(tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT %s FROM employments WHERE id = ?%s + `, employmentColumns, dialect.SelectForUpdate()), id)) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrEmploymentNotFound + } + if err != nil { + return nil, fmt.Errorf("get employment %d for update: %w", id, err) + } + return employment, nil +} + +func scanEmployment(row scanner) (*Employment, error) { + var employment Employment + var title, role, department, location, description, sourceRef sql.NullString + var addressID sql.NullInt64 + var startYear, startMonth, startDay, endYear, endMonth, endDay sql.NullInt64 + var confidence sql.NullFloat64 + var createdAt, updatedAt sql.NullTime + err := row.Scan(&employment.ID, &employment.PersonID, &employment.OrganizationID, &title, &role, &department, &location, &addressID, &description, &startYear, &startMonth, &startDay, &endYear, &endMonth, &endDay, &employment.IsCurrent, &employment.IsPrimary, &employment.Source, &sourceRef, &confidence, &employment.Revision, &createdAt, &updatedAt) + if err != nil { + return nil, err + } + employment.Title, employment.Role, employment.Department, employment.Location, employment.Description, employment.SourceRef = nullStringPtr(title), nullStringPtr(role), nullStringPtr(department), nullStringPtr(location), nullStringPtr(description), nullStringPtr(sourceRef) + if addressID.Valid { + employment.AddressID = new(addressID.Int64) + } + if confidence.Valid { + employment.Confidence = new(confidence.Float64) + } + start, end := ScanPartialDate(startYear, startMonth, startDay), ScanPartialDate(endYear, endMonth, endDay) + if !start.IsZero() { + employment.StartDate = &start + } + if !end.IsZero() { + employment.EndDate = &end + } + var timeErr error + employment.CreatedAt, timeErr = requireNullTime(createdAt, "created_at") + if timeErr != nil { + return nil, timeErr + } + employment.UpdatedAt, timeErr = requireNullTime(updatedAt, "updated_at") + if timeErr != nil { + return nil, timeErr + } + return &employment, nil +} diff --git a/internal/store/employments_test.go b/internal/store/employments_test.go new file mode 100644 index 000000000..a2ead6751 --- /dev/null +++ b/internal/store/employments_test.go @@ -0,0 +1,573 @@ +package store_test + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestConcurrentAndHistoricalEmploymentsStayIndependentlyQueryable(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + dayJob := mustOrganization(t, st, "Example Org") + sideJob := mustOrganization(t, st, "Another Org") + pastJob := mustOrganization(t, st, "Former Org") + + past, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: pastJob.ID, Title: new("Junior Engineer"), StartDate: mustPartialDate(t, "2015"), EndDate: mustPartialDate(t, "2018-06"), IsCurrent: new(false), Source: store.ProvenanceUser}) + require.NoError(err) + assert.False(past.IsCurrent) + assert.False(past.IsPrimary) + primary, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: dayJob.ID, Title: new("Staff Engineer"), Role: new("Engineering"), Department: new("Archive Platform"), StartDate: mustPartialDate(t, "2018-07"), Source: store.ProvenanceUser}) + require.NoError(err) + assert.True(primary.IsCurrent) + assert.True(primary.IsPrimary) + side, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: sideJob.ID, Title: new("Advisor"), StartDate: mustPartialDate(t, "2022-01"), Source: store.ProvenanceUser}) + require.NoError(err) + assert.True(side.IsCurrent) + assert.False(side.IsPrimary) + all, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{PersonID: person.ID}) + require.NoError(err) + require.Len(all, 3) + assert.Equal([]int64{primary.ID, side.ID, past.ID}, employmentIDs(all)) + currentOnly, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{PersonID: person.ID, CurrentOnly: true}) + require.NoError(err) + assert.Equal([]int64{primary.ID, side.ID}, employmentIDs(currentOnly)) + byOrganization, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{OrganizationID: pastJob.ID}) + require.NoError(err) + require.Len(byOrganization, 1) + assert.Equal(past.ID, byOrganization[0].ID) + require.NotNil(byOrganization[0].EndDate) + assert.Equal("2018-06", byOrganization[0].EndDate.String()) +} + +func TestAtMostOnePrimaryCurrentEmploymentIsEnforced(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "bob@example.com", "bob") + first := mustOrganization(t, st, "Example Org") + second := mustOrganization(t, st, "Another Org") + primary, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: first.ID, Title: new("Engineer"), Source: store.ProvenanceUser, IsPrimary: new(true)}) + require.NoError(err) + assert.True(primary.IsPrimary) + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: second.ID, Title: new("Advisor"), Source: store.ProvenanceUser, IsPrimary: new(true)}) + require.Error(err) + require.ErrorIs(err, store.ErrEmploymentPrimaryConflict) + side, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: second.ID, Title: new("Advisor"), Source: store.ProvenanceUser}) + require.NoError(err) + rotated, err := st.SetPrimaryEmploymentContext(ctx, side.ID, side.Revision) + require.NoError(err) + assert.True(rotated.IsPrimary) + demoted, err := st.GetEmploymentContext(ctx, primary.ID) + require.NoError(err) + assert.False(demoted.IsPrimary) + assert.True(demoted.IsCurrent) + assert.Equal(primary.Revision+1, demoted.Revision) +} + +func TestConcurrentPrimaryRotationDoesNotLeakDatabaseConflicts(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + if !st.IsPostgreSQL() { + t.Skip("PostgreSQL row locks are required for this race regression") + } + person := mustPromotedPerson(t, st, "primary-race@example.com", "primary-race") + firstOrg := mustOrganization(t, st, "Primary Race A") + secondOrg := mustOrganization(t, st, "Primary Race B") + first, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: firstOrg.ID, + Title: new("Engineer"), Source: store.ProvenanceUser, + IsPrimary: new(false), + }) + require.NoError(err) + second, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: secondOrg.ID, + Title: new("Advisor"), Source: store.ProvenanceUser, + IsPrimary: new(false), + }) + require.NoError(err) + + firstLock, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + t.Cleanup(func() { _ = firstLock.Rollback() }) + var lockedID int64 + err = firstLock.QueryRowContext(ctx, + `SELECT id FROM employments WHERE id = $1 FOR UPDATE`, first.ID).Scan(&lockedID) + require.NoError(err) + assert.Equal(first.ID, lockedID) + + secondLock, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + t.Cleanup(func() { _ = secondLock.Rollback() }) + err = secondLock.QueryRowContext(ctx, + `SELECT id FROM employments WHERE id = $1 FOR UPDATE`, second.ID).Scan(&lockedID) + require.NoError(err) + assert.Equal(second.ID, lockedID) + + results := make(chan error, 2) + go func() { + _, promoteErr := st.SetPrimaryEmploymentContext(ctx, first.ID, first.Revision) + results <- promoteErr + }() + go func() { + _, promoteErr := st.SetPrimaryEmploymentContext(ctx, second.ID, second.Revision) + results <- promoteErr + }() + + select { + case early := <-results: + require.FailNow("promotion bypassed held target row", "error: %v", early) + case <-time.After(200 * time.Millisecond): + } + require.NoError(firstLock.Commit()) + require.NoError(secondLock.Commit()) + + successes := 0 + for range 2 { + promoteErr := <-results + switch { + case promoteErr == nil: + successes++ + case errors.Is(promoteErr, store.ErrEmploymentPrimaryConflict), + errors.Is(promoteErr, store.ErrEmploymentRevisionConflict): + default: + require.NoError(promoteErr, "database conflicts must retain a typed classification") + } + } + assert.Positive(successes) + + employments, err := st.ListEmploymentsContext( + ctx, store.EmploymentFilter{PersonID: person.ID, CurrentOnly: true}) + require.NoError(err) + primaries := 0 + for _, employment := range employments { + if employment.IsPrimary { + primaries++ + } + } + assert.Equal(1, primaries) +} + +func TestEndEmploymentClearsCurrentAndPrimaryWithoutDeletingHistory(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + replacement := mustOrganization(t, st, "Another Org") + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Engineer"), StartDate: mustPartialDate(t, "2019-04"), Source: store.ProvenanceUser}) + require.NoError(err) + require.True(employment.IsPrimary) + endDate, err := store.ParsePartialDate("2024-09") + require.NoError(err) + ended, err := st.EndEmploymentContext(ctx, employment.ID, employment.Revision, endDate) + require.NoError(err) + assert.False(ended.IsCurrent) + assert.False(ended.IsPrimary) + require.NotNil(ended.EndDate) + assert.Equal("2024-09", ended.EndDate.String()) + assert.Equal(employment.Revision+1, ended.Revision) + history, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{PersonID: person.ID}) + require.NoError(err) + require.Len(history, 1) + next, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: replacement.ID, Title: new("Principal Engineer"), StartDate: mustPartialDate(t, "2024-10"), Source: store.ProvenanceUser, IsPrimary: new(true)}) + require.NoError(err) + assert.True(next.IsPrimary) +} + +func TestEmploymentValidationRejectsBadInput(t *testing.T) { + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + tests := []struct { + name string + input store.EmploymentInput + wantErr error + message string + }{ + {"missing person", store.EmploymentInput{PersonID: person.ID + 9999, OrganizationID: organization.ID, Source: store.ProvenanceUser}, store.ErrPersonNotFound, ""}, + {"missing organization", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID + 9999, Source: store.ProvenanceUser}, store.ErrOrganizationNotFound, ""}, + {"end before start", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, StartDate: mustPartialDate(t, "2020-06"), EndDate: mustPartialDate(t, "2020-05"), Source: store.ProvenanceUser}, store.ErrEmploymentInvalid, "end date must not precede start date"}, + {"year-less start date", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, StartDate: mustPartialDate(t, "--04-12"), Source: store.ProvenanceUser}, store.ErrEmploymentInvalid, "start date requires a year"}, + {"unknown source", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Source: "guessed"}, store.ErrInvalidProvenance, ""}, + {"out-of-range confidence", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Source: store.ProvenanceExtraction, Confidence: new(1.5)}, store.ErrEmploymentInvalid, "confidence must be between 0 and 1"}, + {"confidence on user-declared data", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Source: store.ProvenanceUser, Confidence: new(0.9)}, store.ErrEmploymentInvalid, "confidence is only meaningful for derived or suggested values"}, + {"confidence on carddav-imported data", store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Source: store.ProvenanceCardDAVImport, Confidence: new(0.9)}, store.ErrEmploymentInvalid, "confidence is only meaningful for derived or suggested values"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + _, err := st.AddEmploymentContext(ctx, test.input) + require.Error(err) + require.ErrorIs(err, test.wantErr) + if test.message != "" { + assert.ErrorContains(err, test.message) + } + }) + } +} + +func TestConfidenceIsAcceptedOnEveryDerivedProvenance(t *testing.T) { + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + derived := []store.Provenance{store.ProvenanceArchiveObservation, store.ProvenanceExtraction, store.ProvenanceEnrichment, store.ProvenanceSystem} + for i, source := range derived { + t.Run(string(source), func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + assert.False(source.IsDeclared()) + organization := mustOrganization(t, st, fmt.Sprintf("Example Org %d", i)) + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Engineer"), Source: source, Confidence: new(0.75), IsPrimary: new(false)}) + require.NoError(err) + require.NotNil(employment.Confidence) + assert.InDelta(0.75, *employment.Confidence, 1e-9) + }) + } +} + +func TestEmploymentColumnChecksAreEnforcedBySQL(t *testing.T) { + req := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + insert := func(t *testing.T, title, columns string, args ...any) error { + t.Helper() + query := "INSERT INTO employments (person_id, organization_id, title, title_normalized, source" + if columns != "" { + query += ", " + columns + } + query += ") VALUES (?, ?, ?, ?, ?" + var querySb171 strings.Builder + for range args { + querySb171.WriteString(", ?") + } + query += querySb171.String() + query += ")" + full := append([]any{person.ID, organization.ID, title, store.NormalizeEmploymentTitle(&title), string(store.ProvenanceUser)}, args...) + _, err := st.DB().ExecContext(ctx, st.Rebind(query), full...) + return err + } + rejected := []struct { + name, columns string + args []any + }{ + {"year below range", "start_year", []any{0}}, {"year above range", "start_year", []any{10000}}, {"month below range", "start_year, start_month", []any{2019, 0}}, {"month above range", "start_year, start_month", []any{2019, 13}}, {"day below range", "start_year, start_month, start_day", []any{2019, 4, 0}}, {"day above range", "start_year, start_month, start_day", []any{2019, 4, 32}}, {"day without month", "start_year, start_day", []any{2019, 12}}, {"month without year", "start_month", []any{4}}, {"end year below range", "end_year", []any{0}}, {"end day without month", "end_year, end_day", []any{2019, 12}}, + } + for i, test := range rejected { + t.Run("rejects "+test.name, func(t *testing.T) { + require := require.New(t) + require.Error(insert(t, fmt.Sprintf("Rejected %d", i), test.columns, test.args...)) + }) + } + accepted := []struct { + name, columns string + args []any + }{ + {"no date at all", "", nil}, {"year only", "start_year", []any{2019}}, {"year and month", "start_year, start_month", []any{2019, 4}}, {"full date", "start_year, start_month, start_day", []any{2019, 4, 12}}, {"boundary values", "start_year, start_month, start_day", []any{1, 12, 31}}, {"end year only", "end_year", []any{2024}}, {"full end date", "end_year, end_month, end_day", []any{2024, 9, 30}}, {"both bounds", "start_year, start_month, end_year, end_month", []any{2019, 4, 2024, 9}}, + } + for i, test := range accepted { + t.Run("accepts "+test.name, func(t *testing.T) { + require := require.New(t) + require.NoError(insert(t, fmt.Sprintf("Accepted %d", i), test.columns, test.args...)) + }) + } + t.Run("rejects an unknown provenance", func(t *testing.T) { + title := "Unknown Source" + _, err := st.DB().ExecContext(ctx, st.Rebind(`INSERT INTO employments (person_id, organization_id, title, title_normalized, source) VALUES (?, ?, ?, ?, ?)`), person.ID, organization.ID, title, store.NormalizeEmploymentTitle(&title), "guessed") + req.Error(err) + }) + t.Run("rejects a confidence on declared data", func(t *testing.T) { + title := "Declared Confidence" + _, err := st.DB().ExecContext(ctx, st.Rebind(`INSERT INTO employments (person_id, organization_id, title, title_normalized, source, confidence) VALUES (?, ?, ?, ?, ?, ?)`), person.ID, organization.ID, title, store.NormalizeEmploymentTitle(&title), string(store.ProvenanceUser), 0.9) + req.Error(err) + }) + t.Run("accepts a confidence on derived data", func(t *testing.T) { + title := "Derived Confidence" + _, err := st.DB().ExecContext(ctx, st.Rebind(`INSERT INTO employments (person_id, organization_id, title, title_normalized, source, confidence) VALUES (?, ?, ?, ?, ?, ?)`), person.ID, organization.ID, title, store.NormalizeEmploymentTitle(&title), string(store.ProvenanceSystem), 0.9) + req.NoError(err) + }) +} + +func TestDuplicateCurrentEmploymentAtTheSameOrganizationAndTitleIsRejected(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + _, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Engineer"), Source: store.ProvenanceUser}) + require.NoError(err) + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new(" engineer "), Source: store.ProvenanceUser}) + require.Error(err) + require.ErrorIs(err, store.ErrEmploymentDuplicateActive) + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Architect"), Source: store.ProvenanceUser}) + require.NoError(err) +} + +func TestOrganizationDeletionCannotEraseEmploymentHistory(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Engineer"), Source: store.ProvenanceUser}) + require.NoError(err) + err = st.DeleteOrganizationContext(ctx, organization.ID, organization.Revision) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationHasEmployments) + stillThere, err := st.GetEmploymentContext(ctx, employment.ID) + require.NoError(err) + assert.Equal(employment.ID, stillThere.ID) + retired, err := st.RetireOrganizationContext(ctx, organization.ID, organization.Revision) + require.NoError(err) + require.NotNil(retired.RetiredAt) + survived, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{PersonID: person.ID}) + require.NoError(err) + require.Len(survived, 1) + assert.Equal(employment.ID, survived[0].ID) +} + +func TestEmploymentCannotTargetAMergedOrganizationRedirect(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + survivor := mustOrganization(t, st, "Example Org") + losing := mustOrganization(t, st, "Former Org") + + _, err := st.MergeOrganizationsContext(ctx, + survivor.ID, survivor.Revision, losing.ID, losing.Revision) + require.NoError(err) + + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: losing.ID, + Title: new("Engineer"), Source: store.ProvenanceUser, + }) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationInvalid) + require.ErrorContains(err, "merged organization") +} + +func TestEmploymentWaitingBehindMergeCannotTargetRedirect(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + if !st.IsPostgreSQL() { + t.Skip("PostgreSQL row locks are required for this race regression") + } + person := mustPromotedPerson(t, st, "merge-race@example.com", "merge-race") + survivor := mustOrganization(t, st, "Merge Race Survivor") + losing := mustOrganization(t, st, "Merge Race Losing") + + blocker, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + t.Cleanup(func() { _ = blocker.Rollback() }) + var lockedID int64 + err = blocker.QueryRowContext(ctx, + `SELECT id FROM organizations WHERE id = $1 FOR UPDATE`, + losing.ID).Scan(&lockedID) + require.NoError(err) + require.Equal(losing.ID, lockedID) + + mergeDone := make(chan error, 1) + go func() { + _, mergeErr := st.MergeOrganizationsContext( + ctx, survivor.ID, survivor.Revision, losing.ID, losing.Revision) + mergeDone <- mergeErr + }() + + require.Eventually(func() bool { + probe, beginErr := st.DB().BeginTx(ctx, nil) + if beginErr != nil { + return false + } + defer func() { _ = probe.Rollback() }() + var id int64 + probeErr := probe.QueryRowContext(ctx, + `SELECT id FROM organizations WHERE id = $1 FOR UPDATE NOWAIT`, + survivor.ID).Scan(&id) + return probeErr != nil + }, 5*time.Second, 10*time.Millisecond, + "merge never acquired the survivor lock before waiting on the losing row") + + writeDone := make(chan error, 1) + go func() { + _, writeErr := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: losing.ID, + Title: new("Engineer"), Source: store.ProvenanceUser, + }) + writeDone <- writeErr + }() + time.Sleep(100 * time.Millisecond) + require.NoError(blocker.Commit()) + + select { + case mergeErr := <-mergeDone: + require.NoError(mergeErr) + case <-time.After(5 * time.Second): + require.FailNow("merge did not finish after releasing the losing row") + } + select { + case writeErr := <-writeDone: + require.ErrorIs(writeErr, store.ErrOrganizationInvalid) + case <-time.After(5 * time.Second): + require.FailNow("employment write did not finish after merge") + } + + employments, err := st.ListEmploymentsContext( + ctx, store.EmploymentFilter{PersonID: person.ID}) + require.NoError(err) + require.Empty(employments) +} + +func TestDeletingAPersonRemovesTheirEmploymentsButNotTheOrganization(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + _, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Engineer"), Source: store.ProvenanceUser}) + require.NoError(err) + require.NoError(st.DeletePerson(person.ID, person.Revision)) + remaining, err := st.ListEmploymentsContext(ctx, store.EmploymentFilter{OrganizationID: organization.ID}) + require.NoError(err) + assert.Empty(remaining) + _, err = st.GetOrganizationContext(ctx, organization.ID) + require.NoError(err) +} + +func TestUpdateAndDeleteEmploymentEnforceRevisions(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Engineer"), Source: store.ProvenanceUser}) + require.NoError(err) + updated, err := st.UpdateEmploymentContext(ctx, employment.ID, employment.Revision, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Title: new("Senior Engineer"), Department: new("Archive Platform"), Source: store.ProvenanceUser, IsPrimary: new(true)}) + require.NoError(err) + require.NotNil(updated.Title) + assert.Equal("Senior Engineer", *updated.Title) + assert.Equal(employment.Revision+1, updated.Revision) + _, err = st.UpdateEmploymentContext(ctx, employment.ID, employment.Revision, store.EmploymentInput{PersonID: person.ID, OrganizationID: organization.ID, Source: store.ProvenanceUser}) + require.Error(err) + require.ErrorIs(err, store.ErrEmploymentRevisionConflict) + err = st.DeleteEmploymentContext(ctx, employment.ID, employment.Revision) + require.Error(err) + require.ErrorIs(err, store.ErrEmploymentRevisionConflict) + require.NoError(st.DeleteEmploymentContext(ctx, employment.ID, updated.Revision)) + _, err = st.GetEmploymentContext(ctx, employment.ID) + require.Error(err) + require.ErrorIs(err, store.ErrEmploymentNotFound) +} + +func TestUpdateEmploymentClassifiesAnActiveTitleConflict(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + organization := mustOrganization(t, st, "Example Org") + + _, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: organization.ID, + Title: new("Engineer"), Source: store.ProvenanceUser, + }) + require.NoError(err) + second, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: organization.ID, + Title: new("Advisor"), Source: store.ProvenanceUser, + }) + require.NoError(err) + + _, err = st.UpdateEmploymentContext(ctx, second.ID, second.Revision, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: organization.ID, + Title: new("Engineer"), Source: store.ProvenanceUser, + IsPrimary: new(false), + }) + require.Error(err) + require.ErrorIs(err, store.ErrEmploymentDuplicateActive) +} + +func TestMergeOrganizationsClearsRepointedEmploymentAddressAndBumpsRevision(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + person := mustPromotedPerson(t, st, "alice@example.com", "alice") + survivor := mustOrganization(t, st, "Example Org") + losing := mustOrganization(t, st, "Another Org") + + var addressID int64 + err := st.DB().QueryRowContext(ctx, st.Rebind(` + INSERT INTO organization_addresses (organization_id, original_value, source) + VALUES (?, ?, ?) RETURNING id + `), losing.ID, "Synthetic address", string(store.ProvenanceUser)).Scan(&addressID) + require.NoError(err) + + employment, err := st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: losing.ID, + Title: new("Engineer"), AddressID: &addressID, + Source: store.ProvenanceUser, + }) + require.NoError(err) + require.NotNil(employment.AddressID) + + _, err = st.MergeOrganizationsContext(ctx, + survivor.ID, survivor.Revision, losing.ID, losing.Revision) + require.NoError(err) + + got, err := st.GetEmploymentContext(ctx, employment.ID) + require.NoError(err) + assert.Equal(survivor.ID, got.OrganizationID) + assert.Nil(got.AddressID) + assert.Equal(employment.Revision+1, got.Revision) +} + +func mustOrganization(t *testing.T, st *store.Store, name string) *store.Organization { + t.Helper() + organization, err := st.CreateOrganizationContext(context.Background(), store.OrganizationInput{Name: name, Kind: store.OrganizationKindCompany}) + require.NoError(t, err) + return organization +} +func mustPartialDate(t *testing.T, raw string) *store.PartialDate { + t.Helper() + date, err := store.ParsePartialDate(raw) + require.NoError(t, err) + return &date +} +func employmentIDs(employments []store.Employment) []int64 { + ids := make([]int64, 0, len(employments)) + for _, employment := range employments { + ids = append(ids, employment.ID) + } + return ids +} +func mustPromotedPerson(t *testing.T, st *store.Store, email, name string) *store.Person { + t.Helper() + participant, err := st.EnsureParticipant(email, name, "example.com") + require.NoError(t, err) + person, _, err := st.CreatePersonFromParticipant(participant) + require.NoError(t, err) + return person +} diff --git a/internal/store/organization_attributes.go b/internal/store/organization_attributes.go new file mode 100644 index 000000000..021bee298 --- /dev/null +++ b/internal/store/organization_attributes.go @@ -0,0 +1,552 @@ +package store + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "strings" + "time" +) + +// ErrAttributeObjectTypeMismatch reports a definition scoped to another owner type. +var ErrAttributeObjectTypeMismatch = errors.New("attribute definition object type mismatch") + +// OrganizationAttributeValue is one typed value and its history metadata. +type OrganizationAttributeValue struct { + ID int64 `json:"id"` + OrganizationID int64 `json:"organization_id"` + DefinitionID int64 `json:"definition_id"` + DefinitionSlug string `json:"definition_slug"` + Ordinal int64 `json:"ordinal"` + Value AttributeValue `json:"value"` + ActiveFrom time.Time `json:"active_from"` + ActiveUntil *time.Time `json:"active_until,omitempty"` + CreatedAt time.Time `json:"created_at"` + SupersededAt *time.Time `json:"superseded_at,omitempty"` + Source Provenance `json:"source"` + SourceRef *string `json:"source_ref,omitempty"` + Confidence *float64 `json:"confidence,omitempty"` + Actor *string `json:"actor,omitempty"` +} + +// OrganizationAttributeValueInput sets one typed organization attribute value. +type OrganizationAttributeValueInput struct { + OrganizationID int64 + DefinitionSlug string + Ordinal *int64 + Value AttributeValue + ActiveFrom *time.Time + ActiveUntil *time.Time + Source Provenance + SourceRef *string + Confidence *float64 + Actor *string + ExpectedValueID *int64 + DryRun bool +} + +// OrganizationAttributeSupersedeInput closes one current value without replacement. +type OrganizationAttributeSupersedeInput struct { + OrganizationID int64 + DefinitionSlug string + Ordinal *int64 + At *time.Time + Actor *string + ExpectedValueID *int64 + DryRun bool +} + +// OrganizationAttributeWrite describes a set or supersede result. +type OrganizationAttributeWrite struct { + Value *OrganizationAttributeValue `json:"value,omitempty"` + Superseded *OrganizationAttributeValue `json:"superseded,omitempty"` + DryRun bool `json:"dry_run"` +} + +// OrganizationAttributeQuery filters an organization's attribute values. +type OrganizationAttributeQuery struct { + DefinitionSlug string + IncludeHistory bool +} + +const organizationAttributeValueColumns = ` + v.id, v.organization_id, v.definition_id, d.slug, v.ordinal, + d.value_type, v.value_text, v.value_integer, v.value_real, v.value_boolean, + v.value_date, v.value_timestamp, v.value_json, v.value_record_type, + v.value_record_id, v.active_from, v.active_until, v.created_at, + v.superseded_at, v.source, v.source_ref, v.confidence, v.actor +` + +// ListOrganizationAttributeValuesContext lists current or historical values. +func (s *Store) ListOrganizationAttributeValuesContext( + ctx context.Context, organizationID int64, query OrganizationAttributeQuery, +) ([]OrganizationAttributeValue, error) { + conditions := []string{"v.organization_id = ?"} + args := []any{organizationID} + if query.DefinitionSlug != "" { + conditions = append(conditions, "d.slug = ?", "d.object_type = ?") + args = append(args, query.DefinitionSlug, string(AttributeObjectOrganization)) + } + if !query.IncludeHistory { + conditions = append(conditions, + "v.active_until IS NULL", "v.superseded_at IS NULL") + } + order := "d.display_order, d.slug, v.ordinal, v.id" + if query.IncludeHistory { + order = "d.display_order, d.slug, v.ordinal, " + + "CASE WHEN v.active_until IS NULL AND v.superseded_at IS NULL " + + "THEN 0 ELSE 1 END, v.active_from DESC, v.id DESC" + } + rows, err := s.db.QueryContext(ctx, fmt.Sprintf(` + SELECT %s + FROM organization_attribute_values v + JOIN attribute_definitions d ON d.id = v.definition_id + WHERE %s + ORDER BY %s + `, organizationAttributeValueColumns, strings.Join(conditions, " AND "), order), args...) + if err != nil { + return nil, fmt.Errorf("list organization %d attribute values: %w", organizationID, err) + } + defer func() { _ = rows.Close() }() + + values := make([]OrganizationAttributeValue, 0) + for rows.Next() { + value, scanErr := scanOrganizationAttributeValue(rows) + if scanErr != nil { + return nil, fmt.Errorf("scan organization attribute value: %w", scanErr) + } + values = append(values, *value) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate organization attribute values: %w", err) + } + return values, nil +} + +// SetOrganizationAttributeValueContext supersedes the current value and inserts a replacement. +func (s *Store) SetOrganizationAttributeValueContext( + ctx context.Context, input OrganizationAttributeValueInput, +) (*OrganizationAttributeWrite, error) { + if err := validateProvenance(input.Source, input.Confidence); err != nil { + return nil, err + } + if input.Ordinal != nil && *input.Ordinal < 0 { + return nil, fmt.Errorf("%w: ordinal must not be negative", ErrAttributeValueInvalid) + } + + return retryAttributeWrite(s, "set organization attribute value", + func() (*OrganizationAttributeWrite, error) { + return s.setOrganizationAttributeValueOnce(ctx, input) + }) +} + +func (s *Store) setOrganizationAttributeValueOnce( + ctx context.Context, input OrganizationAttributeValueInput, +) (*OrganizationAttributeWrite, error) { + writeTime := time.Now().UTC() + activeFrom := writeTime + if input.ActiveFrom != nil { + activeFrom = input.ActiveFrom.UTC() + } + if input.ActiveUntil != nil && input.ActiveUntil.Before(activeFrom) { + return nil, fmt.Errorf("%w: active_until must not precede active_from", + ErrAttributeValueInvalid) + } + + write := &OrganizationAttributeWrite{DryRun: input.DryRun} + err := s.withTxContext(ctx, func(tx *loggedTx) error { + definition, err := s.getOrganizationAttributeDefinitionTx( + ctx, tx, input.DefinitionSlug) + if err != nil { + return err + } + if err := writableAttributeDefinition(*definition); err != nil { + return err + } + value, err := normalizeAttributeValue(*definition, input.Value) + if err != nil { + return err + } + input.Value = value + if definition.Cardinality == AttributeCardinalitySingle && + input.Ordinal != nil && *input.Ordinal != 0 { + return fmt.Errorf( + "%w: ordinal %d is not allowed on %s, which declares cardinality single", + ErrAttributeValueInvalid, *input.Ordinal, definition.Slug) + } + if input.Value.Type == AttributeValueRecordReference { + if err := s.lockIdentityMutationTxContext(ctx, tx); err != nil { + return err + } + } + organization, err := getOrganizationForUpdateTx( + ctx, tx, s.dialect, input.OrganizationID) + if err != nil { + return err + } + if organization.MergedIntoID != nil { + return fmt.Errorf("%w: merged organization redirects are immutable", + ErrOrganizationInvalid) + } + if err := s.verifyAttributeRecordTargetTx(ctx, tx, input.Value); err != nil { + return err + } + ordinal, err := s.resolveOrganizationAttributeOrdinalTx(ctx, tx, *definition, input) + if err != nil { + return err + } + current, hasCurrent, err := s.currentOrganizationAttributeValueTx( + ctx, tx, input.OrganizationID, definition.ID, ordinal) + if err != nil { + return err + } + if input.ExpectedValueID != nil && + (!hasCurrent || current.ID != *input.ExpectedValueID) { + return ErrAttributeValueConflict + } + if hasCurrent { + if activeFrom.Before(current.ActiveFrom) { + return fmt.Errorf("%w: active_from precedes the current value", + ErrAttributeValueInvalid) + } + closed, err := s.closeOrganizationAttributeValueTx( + ctx, tx, current.ID, activeFrom, writeTime) + if err != nil { + return err + } + write.Superseded = closed + } + inserted, err := s.insertOrganizationAttributeValueTx( + ctx, tx, *definition, input, ordinal, activeFrom) + if err != nil { + return err + } + write.Value = inserted + if input.DryRun { + write.Value.ID = 0 + if write.Superseded != nil { + write.Superseded.ID = current.ID + } + return errAttributeDryRun + } + return nil + }) + if err != nil && !errors.Is(err, errAttributeDryRun) { + return nil, err + } + return write, nil +} + +func (s *Store) resolveOrganizationAttributeOrdinalTx( + ctx context.Context, tx *loggedTx, + definition AttributeDefinition, input OrganizationAttributeValueInput, +) (int64, error) { + if definition.Cardinality == AttributeCardinalitySingle { + return 0, nil + } + if input.Ordinal != nil { + return *input.Ordinal, nil + } + // Scan historical rows too: reusing a superseded slot's ordinal would + // splice an unrelated value into that slot's supersession lineage. + var next int64 + if err := tx.QueryRowContext(ctx, ` + SELECT COALESCE(MAX(ordinal) + 1, 0) + FROM organization_attribute_values + WHERE organization_id = ? AND definition_id = ? + `, input.OrganizationID, definition.ID).Scan(&next); err != nil { + return 0, fmt.Errorf("resolve next ordinal for %s: %w", definition.Slug, err) + } + return next, nil +} + +func (s *Store) currentOrganizationAttributeValueTx( + ctx context.Context, tx *loggedTx, organizationID, definitionID, ordinal int64, +) (*OrganizationAttributeValue, bool, error) { + value, err := scanOrganizationAttributeValue(tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT %s + FROM organization_attribute_values v + JOIN attribute_definitions d ON d.id = v.definition_id + WHERE v.organization_id = ? AND v.definition_id = ? AND v.ordinal = ? + AND v.active_until IS NULL AND v.superseded_at IS NULL%s + `, organizationAttributeValueColumns, s.dialect.SelectForUpdate()), + organizationID, definitionID, ordinal)) + if errors.Is(err, sql.ErrNoRows) { + return nil, false, nil + } + if err != nil { + return nil, false, fmt.Errorf("load current attribute value: %w", err) + } + return value, true, nil +} + +func (s *Store) closeOrganizationAttributeValueTx( + ctx context.Context, tx *loggedTx, valueID int64, + activeUntil, supersededAt time.Time, +) (*OrganizationAttributeValue, error) { + result, err := tx.ExecContext(ctx, ` + UPDATE organization_attribute_values + SET active_until = ?, superseded_at = ? + WHERE id = ? AND active_until IS NULL AND superseded_at IS NULL + `, activeUntil, supersededAt, valueID) + if err != nil { + return nil, fmt.Errorf("close attribute value %d: %w", valueID, err) + } + affected, err := result.RowsAffected() + if err != nil { + return nil, fmt.Errorf("check close of attribute value %d: %w", valueID, err) + } + if affected != 1 { + return nil, ErrAttributeValueConflict + } + closed, err := scanOrganizationAttributeValue(tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT %s + FROM organization_attribute_values v + JOIN attribute_definitions d ON d.id = v.definition_id + WHERE v.id = ? + `, organizationAttributeValueColumns), valueID)) + if err != nil { + return nil, fmt.Errorf("re-read closed attribute value %d: %w", valueID, err) + } + return closed, nil +} + +func (s *Store) insertOrganizationAttributeValueTx( + ctx context.Context, tx *loggedTx, definition AttributeDefinition, + input OrganizationAttributeValueInput, ordinal int64, activeFrom time.Time, +) (*OrganizationAttributeValue, error) { + var jsonValue any + if len(input.Value.JSON) > 0 { + jsonValue = string(input.Value.JSON) + } + var activeUntil any + if input.ActiveUntil != nil { + activeUntil = input.ActiveUntil.UTC() + } + var insertedID int64 + if err := tx.QueryRowContext(ctx, fmt.Sprintf(` + INSERT INTO organization_attribute_values ( + organization_id, definition_id, ordinal, + value_text, value_integer, value_real, value_boolean, + value_date, value_timestamp, value_json, + value_record_type, value_record_id, + active_from, active_until, source, source_ref, confidence, actor + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, %s, ?, ?, ?, ?, ?, ?, ?, ?) + RETURNING id + `, s.dialect.JSONBindExpr()), + input.OrganizationID, definition.ID, ordinal, + input.Value.Text, input.Value.Integer, input.Value.Real, input.Value.Boolean, + input.Value.Date, input.Value.Timestamp, jsonValue, + input.Value.RecordType, input.Value.RecordID, + activeFrom, activeUntil, string(input.Source), input.SourceRef, + input.Confidence, input.Actor, + ).Scan(&insertedID); err != nil { + return nil, fmt.Errorf("insert attribute value for %s: %w", definition.Slug, err) + } + inserted, err := scanOrganizationAttributeValue(tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT %s + FROM organization_attribute_values v + JOIN attribute_definitions d ON d.id = v.definition_id + WHERE v.id = ? + `, organizationAttributeValueColumns), insertedID)) + if err != nil { + return nil, fmt.Errorf("re-read inserted attribute value: %w", err) + } + return inserted, nil +} + +// SupersedeOrganizationAttributeValueContext closes a current value without replacement. +func (s *Store) SupersedeOrganizationAttributeValueContext( + ctx context.Context, input OrganizationAttributeSupersedeInput, +) (*OrganizationAttributeWrite, error) { + if input.Ordinal != nil && *input.Ordinal < 0 { + return nil, fmt.Errorf("%w: ordinal must not be negative", ErrAttributeValueInvalid) + } + at := time.Now().UTC() + if input.At != nil { + at = input.At.UTC() + } + + return retryAttributeWrite(s, "supersede organization attribute value", + func() (*OrganizationAttributeWrite, error) { + return s.supersedeOrganizationAttributeValueOnce(ctx, at, input) + }) +} + +func (s *Store) supersedeOrganizationAttributeValueOnce( + ctx context.Context, at time.Time, input OrganizationAttributeSupersedeInput, +) (*OrganizationAttributeWrite, error) { + writeTime := time.Now().UTC() + write := &OrganizationAttributeWrite{DryRun: input.DryRun} + err := s.withTxContext(ctx, func(tx *loggedTx) error { + definition, err := s.getOrganizationAttributeDefinitionTx( + ctx, tx, input.DefinitionSlug) + if err != nil { + return err + } + if err := retractableAttributeDefinition(*definition); err != nil { + return err + } + ordinal := int64(0) + if input.Ordinal != nil { + if definition.Cardinality == AttributeCardinalitySingle && *input.Ordinal != 0 { + return fmt.Errorf( + "%w: ordinal %d is not allowed on %s, which declares cardinality single", + ErrAttributeValueInvalid, *input.Ordinal, definition.Slug) + } + ordinal = *input.Ordinal + } + organization, err := getOrganizationForUpdateTx( + ctx, tx, s.dialect, input.OrganizationID) + if err != nil { + return err + } + if organization.MergedIntoID != nil { + return fmt.Errorf("%w: merged organization redirects are immutable", + ErrOrganizationInvalid) + } + current, hasCurrent, err := s.currentOrganizationAttributeValueTx( + ctx, tx, input.OrganizationID, definition.ID, ordinal) + if err != nil { + return err + } + if !hasCurrent { + return ErrAttributeValueNotFound + } + if input.ExpectedValueID != nil && current.ID != *input.ExpectedValueID { + return ErrAttributeValueConflict + } + if at.Before(current.ActiveFrom) { + return fmt.Errorf("%w: supersede time precedes active_from", + ErrAttributeValueInvalid) + } + closed, err := s.closeOrganizationAttributeValueTx( + ctx, tx, current.ID, at, writeTime) + if err != nil { + return err + } + write.Superseded = closed + if input.DryRun { + return errAttributeDryRun + } + return nil + }) + if err != nil && !errors.Is(err, errAttributeDryRun) { + return nil, err + } + return write, nil +} + +func (s *Store) getOrganizationAttributeDefinitionTx( + ctx context.Context, tx *loggedTx, slug string, +) (*AttributeDefinition, error) { + definition, err := scanAttributeDefinition(tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT %s + FROM attribute_definitions + WHERE object_type = ? AND slug = ?%s + `, attributeDefinitionColumns, s.dialect.SelectForUpdate()), + string(AttributeObjectOrganization), slug)) + if err == nil { + return definition, nil + } + if !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("get organization attribute definition %q: %w", slug, err) + } + var objectType AttributeObjectType + scopeErr := tx.QueryRowContext(ctx, fmt.Sprintf(` + SELECT object_type FROM attribute_definitions + WHERE slug = ? ORDER BY id LIMIT 1%s + `, s.dialect.SelectForUpdate()), slug).Scan(&objectType) + if errors.Is(scopeErr, sql.ErrNoRows) { + return nil, ErrAttributeDefinitionNotFound + } + if scopeErr != nil { + return nil, fmt.Errorf("check attribute definition scope: %w", scopeErr) + } + return nil, fmt.Errorf( + "%w: definition %q is scoped to %s, not organization", + ErrAttributeObjectTypeMismatch, slug, objectType) +} + +func scanOrganizationAttributeValue(row scanner) (*OrganizationAttributeValue, error) { + var ( + value OrganizationAttributeValue + valueType string + text sql.NullString + integer sql.NullInt64 + realValue sql.NullFloat64 + boolean sql.NullBool + date sql.NullString + timestamp sql.NullTime + rawJSON []byte + recordType sql.NullString + recordID sql.NullInt64 + activeUntil sql.NullTime + supersededAt sql.NullTime + source string + sourceRef sql.NullString + confidence sql.NullFloat64 + actor sql.NullString + ) + if err := row.Scan( + &value.ID, &value.OrganizationID, &value.DefinitionID, &value.DefinitionSlug, + &value.Ordinal, &valueType, &text, &integer, &realValue, &boolean, &date, + ×tamp, &rawJSON, &recordType, &recordID, &value.ActiveFrom, + &activeUntil, &value.CreatedAt, &supersededAt, &source, &sourceRef, + &confidence, &actor, + ); err != nil { + return nil, err + } + value.Value.Type = AttributeValueType(valueType) + if text.Valid { + value.Value.Text = &text.String + } + if integer.Valid { + value.Value.Integer = &integer.Int64 + } + if realValue.Valid { + value.Value.Real = &realValue.Float64 + } + if boolean.Valid { + value.Value.Boolean = &boolean.Bool + } + if date.Valid { + value.Value.Date = &date.String + } + if timestamp.Valid { + utc := timestamp.Time.UTC() + value.Value.Timestamp = &utc + } + if len(rawJSON) > 0 { + value.Value.JSON = json.RawMessage(append([]byte(nil), rawJSON...)) + } + if recordType.Valid { + value.Value.RecordType = &recordType.String + } + if recordID.Valid { + value.Value.RecordID = &recordID.Int64 + } + if activeUntil.Valid { + utc := activeUntil.Time.UTC() + value.ActiveUntil = &utc + } + if supersededAt.Valid { + utc := supersededAt.Time.UTC() + value.SupersededAt = &utc + } + value.Source = Provenance(source) + if sourceRef.Valid { + value.SourceRef = &sourceRef.String + } + if confidence.Valid { + value.Confidence = &confidence.Float64 + } + if actor.Valid { + value.Actor = &actor.String + } + value.ActiveFrom = value.ActiveFrom.UTC() + value.CreatedAt = value.CreatedAt.UTC() + return &value, nil +} diff --git a/internal/store/organization_attributes_test.go b/internal/store/organization_attributes_test.go new file mode 100644 index 000000000..97fa9174b --- /dev/null +++ b/internal/store/organization_attributes_test.go @@ -0,0 +1,516 @@ +package store_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func organizationTextDefinition(slug string) store.AttributeDefinitionInput { + input := personTextDefinition(slug) + input.UniversalID = "test-org-" + slug + input.ObjectType = store.AttributeObjectOrganization + return input +} + +func textAttributeValue(value string) store.AttributeValue { + return store.AttributeValue{Type: store.AttributeValueText, Text: &value} +} + +func mustOrganizationAttributeDefinition( + t *testing.T, st *store.Store, slug string, +) *store.AttributeDefinition { + t.Helper() + definition, err := st.CreateAttributeDefinitionContext( + context.Background(), organizationTextDefinition(slug)) + require.NoError(t, err) + return definition +} + +func mustAttributeOrganization(t *testing.T, st *store.Store) *store.Organization { + t.Helper() + organization, err := st.CreateOrganizationContext( + context.Background(), store.OrganizationInput{Name: "Example Org"}) + require.NoError(t, err) + return organization +} + +func organizationAttributeTestBind(st *store.Store) string { + if st.IsPostgreSQL() { + return "$1" + } + return "?" +} + +func TestOrganizationAttributeSetSupersedeAndHistory(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + mustOrganizationAttributeDefinition(t, st, "industry_focus") + firstAt := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC) + secondAt := firstAt.Add(24 * time.Hour) + + first, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("archival software"), + ActiveFrom: &firstAt, + Source: store.ProvenanceUser, + }) + require.NoError(err) + + second, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("information retrieval"), + ActiveFrom: &secondAt, + Source: store.ProvenanceUser, + ExpectedValueID: &first.Value.ID, + }) + require.NoError(err) + require.NotNil(second.Superseded) + require.NotNil(second.Superseded.ActiveUntil) + require.NotNil(second.Superseded.SupersededAt) + assert.Equal(secondAt, *second.Superseded.ActiveUntil) + assert.True( + second.Superseded.SupersededAt.After(*second.Superseded.ActiveUntil), + "transaction time must not be replaced with a backdated validity boundary", + ) + + history, err := st.ListOrganizationAttributeValuesContext( + ctx, organization.ID, + store.OrganizationAttributeQuery{ + DefinitionSlug: "industry_focus", + IncludeHistory: true, + }) + require.NoError(err) + require.Len(history, 2) + assert.Equal(second.Value.ID, history[0].ID) + assert.Equal(first.Value.ID, history[1].ID) + + closed, err := st.SupersedeOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeSupersedeInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + ExpectedValueID: &second.Value.ID, + }) + require.NoError(err) + require.NotNil(closed.Superseded) + + current, err := st.ListOrganizationAttributeValuesContext( + ctx, organization.ID, store.OrganizationAttributeQuery{}) + require.NoError(err) + assert.Empty(current) +} + +func TestOrganizationAttributeRejectsWrongScopeAndInvalidConfidence(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + + _, err := st.CreateAttributeDefinitionContext( + ctx, personTextDefinition("person_only")) + require.NoError(err) + _, err = st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "person_only", + Value: textAttributeValue("not allowed"), + Source: store.ProvenanceUser, + }) + require.ErrorIs(err, store.ErrAttributeObjectTypeMismatch) + + mustOrganizationAttributeDefinition(t, st, "declared_fact") + confidence := 0.8 + _, err = st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "declared_fact", + Value: textAttributeValue("not allowed"), + Source: store.ProvenanceUser, + Confidence: &confidence, + }) + require.ErrorIs(err, store.ErrAttributeValueInvalid) +} + +func TestOrganizationAttributeRequiresAnExistingOrganization(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + mustOrganizationAttributeDefinition(t, st, "industry_focus") + + _, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: 999_999, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("archival software"), + Source: store.ProvenanceUser, + }) + require.ErrorIs(err, store.ErrOrganizationNotFound) +} + +func TestOrganizationAttributeDryRunCASAndMultiOrdinal(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + input := organizationTextDefinition("specialties") + input.Cardinality = store.AttributeCardinalityMulti + _, err := st.CreateAttributeDefinitionContext(ctx, input) + require.NoError(err) + confidence := 0.7 + + first, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "specialties", + Value: textAttributeValue("archival software"), + Source: store.ProvenanceExtraction, + Confidence: &confidence, + }) + require.NoError(err) + second, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "specialties", + Value: textAttributeValue("information retrieval"), + Source: store.ProvenanceSystem, + }) + require.NoError(err) + assert.Equal(int64(0), first.Value.Ordinal) + assert.Equal(int64(1), second.Value.Ordinal) + + staleID := first.Value.ID + second.Value.ID + 1 + _, err = st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "specialties", + Ordinal: new(int64(0)), + Value: textAttributeValue("stale"), + Source: store.ProvenanceUser, + ExpectedValueID: &staleID, + }) + require.ErrorIs(err, store.ErrAttributeValueConflict) + + preview, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "specialties", + Ordinal: new(int64(0)), + Value: textAttributeValue("preview"), + Source: store.ProvenanceUser, + DryRun: true, + }) + require.NoError(err) + assert.True(preview.DryRun) + assert.Zero(preview.Value.ID) + + current, err := st.ListOrganizationAttributeValuesContext( + ctx, organization.ID, store.OrganizationAttributeQuery{}) + require.NoError(err) + require.Len(current, 2) + assert.Equal(first.Value.ID, current[0].ID) + assert.Equal(second.Value.ID, current[1].ID) +} + +func TestOrganizationMultiAttributeAppendsAfterSupersede(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + definition := organizationTextDefinition("append_only_specialties") + definition.Cardinality = store.AttributeCardinalityMulti + _, err := st.CreateAttributeDefinitionContext(ctx, definition) + require.NoError(err) + + first, err := st.SetOrganizationAttributeValueContext(ctx, + store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, DefinitionSlug: definition.Slug, + Value: textAttributeValue("archival software"), Source: store.ProvenanceUser, + }) + require.NoError(err) + second, err := st.SetOrganizationAttributeValueContext(ctx, + store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, DefinitionSlug: definition.Slug, + Value: textAttributeValue("information retrieval"), Source: store.ProvenanceUser, + }) + require.NoError(err) + assert.Equal(int64(0), first.Value.Ordinal) + assert.Equal(int64(1), second.Value.Ordinal) + + _, err = st.SupersedeOrganizationAttributeValueContext(ctx, + store.OrganizationAttributeSupersedeInput{ + OrganizationID: organization.ID, DefinitionSlug: definition.Slug, + Ordinal: &second.Value.Ordinal, + }) + require.NoError(err) + third, err := st.SetOrganizationAttributeValueContext(ctx, + store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, DefinitionSlug: definition.Slug, + Value: textAttributeValue("poetry"), Source: store.ProvenanceUser, + }) + require.NoError(err) + assert.Equal(int64(2), third.Value.Ordinal, + "appending must not reuse a superseded slot's ordinal and adopt its history") +} + +func TestOrganizationAttributeRetractionIsNotCurrentAndCanBeReplaced(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + mustOrganizationAttributeDefinition(t, st, "industry_focus") + + retracted, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("withdrawn"), + Source: store.ProvenanceExtraction, + }) + require.NoError(err) + _, err = st.DB().ExecContext(ctx, fmt.Sprintf(` + UPDATE organization_attribute_values + SET superseded_at = CURRENT_TIMESTAMP + WHERE id = %s + `, organizationAttributeTestBind(st)), retracted.Value.ID) + require.NoError(err) + + current, err := st.ListOrganizationAttributeValuesContext( + ctx, organization.ID, store.OrganizationAttributeQuery{}) + require.NoError(err) + assert.Empty(current) + + replacement, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("current"), + Source: store.ProvenanceUser, + }) + require.NoError(err) + assert.Equal(int64(0), replacement.Value.Ordinal) + + history, err := st.ListOrganizationAttributeValuesContext( + ctx, organization.ID, + store.OrganizationAttributeQuery{ + DefinitionSlug: "industry_focus", + IncludeHistory: true, + }) + require.NoError(err) + require.Len(history, 2) + assert.Equal(replacement.Value.ID, history[0].ID) + assert.Equal(retracted.Value.ID, history[1].ID) + assert.Nil(history[1].ActiveUntil) + require.NotNil(history[1].SupersededAt) +} + +func TestOrganizationDeleteCascadesAttributeValues(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + mustOrganizationAttributeDefinition(t, st, "industry_focus") + + _, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("archival software"), + Source: store.ProvenanceUser, + }) + require.NoError(err) + require.NoError(st.DeleteOrganizationContext( + ctx, organization.ID, organization.Revision)) + + var count int + require.NoError(st.DB().QueryRowContext(ctx, fmt.Sprintf(` + SELECT COUNT(*) FROM organization_attribute_values + WHERE organization_id = %s + `, organizationAttributeTestBind(st)), organization.ID).Scan(&count)) + assert.Zero(count) +} + +func TestDeleteAttributeDefinitionRejectsDefinitionsWithOnlyOrganizationValues( + t *testing.T, +) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + definition := mustOrganizationAttributeDefinition(t, st, "industry_focus") + _, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: "industry_focus", + Value: textAttributeValue("archival software"), + Source: store.ProvenanceUser, + }) + require.NoError(err) + + err = st.DeleteAttributeDefinitionContext( + ctx, definition.ID, definition.Revision) + require.ErrorIs(err, store.ErrAttributeDefinitionHasValues) +} + +func TestOrganizationAttributeWriteUsesTransactionalDefinitionState(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + if !st.IsPostgreSQL() { + t.Skip("PostgreSQL row locks are required for this race regression") + } + organization := mustAttributeOrganization(t, st) + definition := mustOrganizationAttributeDefinition(t, st, "industry_focus") + + definitionUpdate, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + t.Cleanup(func() { _ = definitionUpdate.Rollback() }) + _, err = definitionUpdate.ExecContext(ctx, ` + UPDATE attribute_definitions + SET is_active = FALSE, revision = revision + 1 + WHERE id = $1 + `, definition.ID) + require.NoError(err) + + writeDone := make(chan error, 1) + go func() { + _, err := st.SetOrganizationAttributeValueContext( + ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: definition.Slug, + Value: textAttributeValue("archival software"), + Source: store.ProvenanceUser, + }) + writeDone <- err + }() + + select { + case err := <-writeDone: + require.ErrorIs(err, store.ErrAttributeDefinitionInactive, + "an attribute write must not use stale definition state") + require.NoError(definitionUpdate.Commit()) + return + case <-time.After(200 * time.Millisecond): + } + require.NoError(definitionUpdate.Commit()) + + select { + case err := <-writeDone: + require.ErrorIs(err, store.ErrAttributeDefinitionInactive) + case <-time.After(5 * time.Second): + require.FailNow("attribute write did not finish after definition update committed") + } +} + +func TestInactiveOrganizationAttributeDefinitionAllowsSupersede(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustAttributeOrganization(t, st) + definition := mustOrganizationAttributeDefinition(t, st, "retired_field") + write, err := st.SetOrganizationAttributeValueContext(ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: definition.Slug, + Value: textAttributeValue("stale"), + Source: store.ProvenanceUser, + }) + require.NoError(err) + + inactive := false + _, err = st.UpdateAttributeDefinitionContext(ctx, definition.ID, definition.Revision, + store.AttributeDefinitionUpdate{IsActive: &inactive}) + require.NoError(err) + + cleared, err := st.SupersedeOrganizationAttributeValueContext(ctx, + store.OrganizationAttributeSupersedeInput{ + OrganizationID: organization.ID, + DefinitionSlug: definition.Slug, + ExpectedValueID: &write.Value.ID, + }) + require.NoError(err, "retracting a value from an inactive organization definition must stay possible") + require.NotNil(cleared.Superseded) +} + +func TestOrganizationAttributeSupersedeRetriesDeadlock(t *testing.T) { + require := require.New(t) + st := testutil.NewTestStore(t) + if !st.IsPostgreSQL() { + t.Skip("PostgreSQL row locks are required for attribute retry regression") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + organization := mustAttributeOrganization(t, st) + definition := mustOrganizationAttributeDefinition(t, st, "retry_field") + write, err := st.SetOrganizationAttributeValueContext(ctx, store.OrganizationAttributeValueInput{ + OrganizationID: organization.ID, + DefinitionSlug: definition.Slug, + Value: textAttributeValue("stale"), + Source: store.ProvenanceUser, + }) + require.NoError(err) + + blocker, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + t.Cleanup(func() { _ = blocker.Rollback() }) + var lockedID int64 + require.NoError(blocker.QueryRowContext(ctx, + `SELECT id FROM organizations WHERE id = $1 FOR UPDATE`, organization.ID).Scan(&lockedID)) + require.Equal(organization.ID, lockedID) + + writeDone := make(chan error, 1) + go func() { + _, supersedeErr := st.SupersedeOrganizationAttributeValueContext(ctx, + store.OrganizationAttributeSupersedeInput{ + OrganizationID: organization.ID, + DefinitionSlug: definition.Slug, + ExpectedValueID: &write.Value.ID, + }) + writeDone <- supersedeErr + }() + require.Eventually(func() bool { + return postgreSQLWaitingLockCount(t, st) >= 1 + }, 5*time.Second, 10*time.Millisecond, "supersede did not reach the organization lock") + + blockerDefinitionDone := make(chan error, 1) + go func() { + var id int64 + definitionErr := blocker.QueryRowContext(ctx, + `SELECT id FROM attribute_definitions WHERE id = $1 FOR UPDATE`, definition.ID).Scan(&id) + if definitionErr == nil { + definitionErr = blocker.Commit() + } else { + _ = blocker.Rollback() + } + blockerDefinitionDone <- definitionErr + }() + + var writeErr, blockerErr error + select { + case writeErr = <-writeDone: + case <-ctx.Done(): + require.FailNow("supersede did not finish after the deadlock detector", ctx.Err()) + } + select { + case blockerErr = <-blockerDefinitionDone: + case <-ctx.Done(): + require.FailNow("blocker did not finish after the deadlock detector", ctx.Err()) + } + require.NoError(blockerErr, "blocker must release the organization lock") + require.NoError(writeErr, "organization supersede must retry a transient PostgreSQL deadlock") +} diff --git a/internal/store/organization_duplicates.go b/internal/store/organization_duplicates.go new file mode 100644 index 000000000..978d64954 --- /dev/null +++ b/internal/store/organization_duplicates.go @@ -0,0 +1,315 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" + "time" +) + +var ( + ErrOrganizationDuplicateSuggestionNotFound = errors.New("organization duplicate suggestion not found") + ErrOrganizationDuplicateSuggestionInvalid = errors.New("invalid organization duplicate suggestion") + ErrOrganizationDuplicateSuggestionAlreadyResolved = errors.New( + "organization duplicate suggestion already resolved") +) + +// Duplicate-suggestion criteria. Both are review signals derived from +// normalized values; neither is identity proof. +const ( + OrganizationDuplicateCriterionDomain = "domain" + OrganizationDuplicateCriterionName = "name" +) + +// Duplicate-suggestion statuses. A rejected row is retained so the same +// inference is not raised again. +const ( + OrganizationDuplicateStatusOpen = "open" + OrganizationDuplicateStatusAccepted = "accepted" + OrganizationDuplicateStatusRejected = "rejected" +) + +// Confidence values for the two criteria. A shared registrable domain is +// stronger evidence than a shared normalized name, and neither reaches 1 +// because only a user may confirm a duplicate. +const ( + organizationDuplicateDomainConfidence = 0.7 + organizationDuplicateNameConfidence = 0.4 +) + +// OrganizationDuplicateSuggestion is one reviewable possible-duplicate pair. +// Accepting it records the user's decision; merging is the separate explicit +// MergeOrganizationsContext call, so no scan can ever combine two +// organizations on its own. +type OrganizationDuplicateSuggestion struct { + ID int64 `json:"id"` + OrganizationAID int64 `json:"organization_a_id"` + OrganizationBID int64 `json:"organization_b_id"` + Criterion string `json:"criterion"` + Evidence string `json:"evidence"` + Confidence float64 `json:"confidence"` + Status string `json:"status"` + ResolvedAt *time.Time `json:"resolved_at,omitempty"` + ResolvedBy *string `json:"resolved_by,omitempty"` + ResolutionNote *string `json:"resolution_note,omitempty"` + Source Provenance `json:"source"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +const organizationDuplicateSuggestionColumns = ` + id, organization_a_id, organization_b_id, criterion, evidence, confidence, + status, resolved_at, resolved_by, resolution_note, source, created_at, updated_at +` + +const organizationDuplicateDomainPairsQuery = ` + WITH live AS ( + SELECT id FROM organizations + WHERE retired_at IS NULL AND merged_into_id IS NULL + ), + domains AS ( + SELECT o.id AS organization_id, o.primary_domain AS value + FROM organizations o + JOIN live ON live.id = o.id + WHERE o.primary_domain IS NOT NULL AND LENGTH(o.primary_domain) > 0 + UNION + SELECT i.organization_id, i.normalized_value + FROM organization_identifiers i + JOIN live ON live.id = i.organization_id + WHERE i.identifier_kind = 'domain' + AND i.active_until IS NULL AND i.superseded_at IS NULL + ) + SELECT a.organization_id, b.organization_id, a.value + FROM domains a + JOIN domains b + ON b.value = a.value AND b.organization_id > a.organization_id + ORDER BY a.organization_id, b.organization_id, a.value +` + +const organizationDuplicateNamePairsQuery = ` + WITH live AS ( + SELECT id FROM organizations + WHERE retired_at IS NULL AND merged_into_id IS NULL + ), + names AS ( + SELECT o.id AS organization_id, o.name_normalized AS value + FROM organizations o + JOIN live ON live.id = o.id + WHERE LENGTH(o.name_normalized) > 0 + UNION + SELECT n.organization_id, n.name_normalized + FROM organization_names n + JOIN live ON live.id = n.organization_id + WHERE n.active_until IS NULL AND n.superseded_at IS NULL + ) + SELECT a.organization_id, b.organization_id, a.value + FROM names a + JOIN names b + ON b.value = a.value AND b.organization_id > a.organization_id + ORDER BY a.organization_id, b.organization_id, a.value +` + +// RefreshOrganizationDuplicateSuggestionsContext scans live organizations for +// shared normalized domains and shared normalized names and records any pair +// it has not recorded before. It returns the number of new suggestion rows. It +// never merges, retires, or revises an organization. +func (s *Store) RefreshOrganizationDuplicateSuggestionsContext(ctx context.Context) (int, error) { + created := 0 + err := s.withTxContext(ctx, func(tx *loggedTx) error { + var err error + created, err = refreshOrganizationDuplicateSuggestionsTx(ctx, tx, s.dialect, + organizationDuplicateDomainPairsQuery, OrganizationDuplicateCriterionDomain, + organizationDuplicateDomainConfidence) + if err != nil { + return err + } + nameCreated, err := refreshOrganizationDuplicateSuggestionsTx(ctx, tx, s.dialect, + organizationDuplicateNamePairsQuery, OrganizationDuplicateCriterionName, + organizationDuplicateNameConfidence) + if err != nil { + return err + } + created += nameCreated + return nil + }) + if err != nil { + return 0, fmt.Errorf("refresh organization duplicate suggestions: %w", err) + } + return created, nil +} + +func refreshOrganizationDuplicateSuggestionsTx( + ctx context.Context, tx *loggedTx, dialect Dialect, query, criterion string, confidence float64, +) (int, error) { + type pair struct { + aID, bID int64 + evidence string + } + rows, err := tx.QueryContext(ctx, query) + if err != nil { + return 0, fmt.Errorf("find %s duplicate pairs: %w", criterion, err) + } + pairs := make([]pair, 0) + for rows.Next() { + var candidate pair + if err := rows.Scan(&candidate.aID, &candidate.bID, &candidate.evidence); err != nil { + _ = rows.Close() + return 0, fmt.Errorf("scan %s duplicate pair: %w", criterion, err) + } + pairs = append(pairs, candidate) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return 0, fmt.Errorf("iterate %s duplicate pairs: %w", criterion, err) + } + if err := rows.Close(); err != nil { + return 0, fmt.Errorf("close %s duplicate pairs: %w", criterion, err) + } + + insert := dialect.InsertOrIgnore(fmt.Sprintf(` + INSERT OR IGNORE INTO organization_duplicate_suggestions ( + organization_a_id, organization_b_id, criterion, evidence, confidence, + status, source, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, %s, %s) + `, dialect.Now(), dialect.Now())) + created := 0 + for _, candidate := range pairs { + result, err := tx.ExecContext(ctx, insert, + candidate.aID, candidate.bID, criterion, candidate.evidence, confidence, + OrganizationDuplicateStatusOpen, ProvenanceSystem) + if err != nil { + return 0, fmt.Errorf("insert %s duplicate suggestion: %w", criterion, err) + } + count, err := result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("count inserted %s duplicate suggestions: %w", criterion, err) + } + created += int(count) + } + return created, nil +} + +// ListOrganizationDuplicateSuggestionsContext returns suggestions filtered by +// status. An empty status returns every status. Limit is clamped to +// MaxOrganizationPageSize. +func (s *Store) ListOrganizationDuplicateSuggestionsContext( + ctx context.Context, status string, limit, offset int, +) ([]OrganizationDuplicateSuggestion, error) { + if err := validateOrganizationDuplicateSuggestionStatus(status, true); err != nil { + return nil, err + } + if limit <= 0 { + limit = DefaultOrganizationPageSize + } else if limit > MaxOrganizationPageSize { + limit = MaxOrganizationPageSize + } + offset = max(offset, 0) + args := []any{limit, offset} + where := "" + if status != "" { + where = " WHERE status = ?" + args = append([]any{status}, args...) + } + rows, err := s.db.QueryContext(ctx, `SELECT `+organizationDuplicateSuggestionColumns+ + ` FROM organization_duplicate_suggestions`+where+` ORDER BY id LIMIT ? OFFSET ?`, args...) + if err != nil { + return nil, fmt.Errorf("list organization duplicate suggestions: %w", err) + } + defer func() { _ = rows.Close() }() + + suggestions := make([]OrganizationDuplicateSuggestion, 0) + for rows.Next() { + suggestion, err := scanOrganizationDuplicateSuggestion(rows) + if err != nil { + return nil, fmt.Errorf("scan organization duplicate suggestion: %w", err) + } + suggestions = append(suggestions, *suggestion) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate organization duplicate suggestions: %w", err) + } + return suggestions, nil +} + +// ResolveOrganizationDuplicateSuggestionContext records the user's decision. +// status must be accepted or rejected; resolving back to open is rejected so +// an audit trail cannot be erased. +func (s *Store) ResolveOrganizationDuplicateSuggestionContext( + ctx context.Context, id int64, status string, note *string, +) (*OrganizationDuplicateSuggestion, error) { + if status == OrganizationDuplicateStatusOpen { + return nil, fmt.Errorf("%w: cannot resolve a suggestion back to open", + ErrOrganizationDuplicateSuggestionInvalid) + } + if err := validateOrganizationDuplicateSuggestionStatus(status, false); err != nil { + return nil, err + } + if note != nil { + trimmed := strings.TrimSpace(*note) + if trimmed == "" { + note = nil + } else { + note = &trimmed + } + } + suggestion, err := scanOrganizationDuplicateSuggestion(s.db.QueryRowContext(ctx, fmt.Sprintf(` + UPDATE organization_duplicate_suggestions + SET status = ?, resolution_note = ?, resolved_at = %s, updated_at = %s + WHERE id = ? AND status = ? + RETURNING %s + `, s.dialect.Now(), s.dialect.Now(), organizationDuplicateSuggestionColumns), + status, note, id, OrganizationDuplicateStatusOpen)) + if errors.Is(err, sql.ErrNoRows) { + var currentStatus string + statusErr := s.db.QueryRowContext(ctx, ` + SELECT status FROM organization_duplicate_suggestions WHERE id = ? + `, id).Scan(¤tStatus) + if errors.Is(statusErr, sql.ErrNoRows) { + return nil, ErrOrganizationDuplicateSuggestionNotFound + } + if statusErr != nil { + return nil, fmt.Errorf( + "check organization duplicate suggestion %d resolution: %w", id, statusErr) + } + return nil, fmt.Errorf("%w: current status is %s", + ErrOrganizationDuplicateSuggestionAlreadyResolved, currentStatus) + } + if err != nil { + return nil, fmt.Errorf("resolve organization duplicate suggestion %d: %w", id, err) + } + return suggestion, nil +} + +func validateOrganizationDuplicateSuggestionStatus(status string, allowEmpty bool) error { + if status == "" && allowEmpty { + return nil + } + if status != OrganizationDuplicateStatusOpen && + status != OrganizationDuplicateStatusAccepted && + status != OrganizationDuplicateStatusRejected { + return fmt.Errorf("%w: unknown status %q", ErrOrganizationDuplicateSuggestionInvalid, status) + } + return nil +} + +func scanOrganizationDuplicateSuggestion(row scanner) (*OrganizationDuplicateSuggestion, error) { + var ( + suggestion OrganizationDuplicateSuggestion + resolvedAt sql.NullTime + resolvedBy, resolutionNote sql.NullString + ) + if err := row.Scan( + &suggestion.ID, &suggestion.OrganizationAID, &suggestion.OrganizationBID, + &suggestion.Criterion, &suggestion.Evidence, &suggestion.Confidence, + &suggestion.Status, &resolvedAt, &resolvedBy, &resolutionNote, + &suggestion.Source, &suggestion.CreatedAt, &suggestion.UpdatedAt, + ); err != nil { + return nil, err + } + suggestion.ResolvedAt = nullTimePtr(resolvedAt) + suggestion.ResolvedBy = nullStringPtr(resolvedBy) + suggestion.ResolutionNote = nullStringPtr(resolutionNote) + return &suggestion, nil +} diff --git a/internal/store/organization_duplicates_test.go b/internal/store/organization_duplicates_test.go new file mode 100644 index 000000000..7f46ae293 --- /dev/null +++ b/internal/store/organization_duplicates_test.go @@ -0,0 +1,400 @@ +package store_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestRefreshOrganizationDuplicateSuggestionsFindsSharedDomainsAndNames(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + domain := "example.com" + first, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", PrimaryDomain: &domain, + }) + require.NoError(err) + second, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org Inc", PrimaryDomain: &domain, + }) + require.NoError(err) + third, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: " example org ", + }) + require.NoError(err) + unrelated, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Unrelated Org", + }) + require.NoError(err) + + created, err := st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + assert.Equal(2, created) + + suggestions, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, + store.OrganizationDuplicateStatusOpen, 0, 0) + require.NoError(err) + require.Len(suggestions, 2) + + byCriterion := map[string]store.OrganizationDuplicateSuggestion{} + for _, suggestion := range suggestions { + byCriterion[suggestion.Criterion] = suggestion + } + + domainSuggestion, ok := byCriterion[store.OrganizationDuplicateCriterionDomain] + require.True(ok) + assert.Equal(first.ID, domainSuggestion.OrganizationAID) + assert.Equal(second.ID, domainSuggestion.OrganizationBID) + assert.Equal("example.com", domainSuggestion.Evidence) + assert.Equal(store.OrganizationDuplicateStatusOpen, domainSuggestion.Status) + + nameSuggestion, ok := byCriterion[store.OrganizationDuplicateCriterionName] + require.True(ok) + assert.Equal(first.ID, nameSuggestion.OrganizationAID) + assert.Equal(third.ID, nameSuggestion.OrganizationBID) + assert.Equal("example org", nameSuggestion.Evidence) + + for _, suggestion := range suggestions { + assert.NotEqual(unrelated.ID, suggestion.OrganizationAID) + assert.NotEqual(unrelated.ID, suggestion.OrganizationBID) + } +} + +func TestRefreshOrganizationDuplicateSuggestionsIsIdempotentAndNeverMerges(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + domain := "example.com" + first, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", PrimaryDomain: &domain, + }) + require.NoError(err) + second, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org Inc", PrimaryDomain: &domain, + }) + require.NoError(err) + + firstRun, err := st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + assert.Equal(1, firstRun) + + secondRun, err := st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + assert.Equal(0, secondRun, "a repeated scan creates no duplicate suggestion rows") + + stillFirst, err := st.GetOrganizationContext(ctx, first.ID) + require.NoError(err) + assert.Nil(stillFirst.MergedIntoID, "a suggestion never merges anything") + assert.Nil(stillFirst.RetiredAt) + stillSecond, err := st.GetOrganizationContext(ctx, second.ID) + require.NoError(err) + assert.Nil(stillSecond.MergedIntoID) + assert.Nil(stillSecond.RetiredAt) + assert.Equal(first.Revision, stillFirst.Revision, + "a suggestion does not touch the organization revision") +} + +func TestRejectedOrganizationDuplicateSuggestionsAreRetainedAndNotReopened(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + domain := "example.com" + _, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org Inc", PrimaryDomain: &domain, + }) + require.NoError(err) + + _, err = st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + open, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, + store.OrganizationDuplicateStatusOpen, 0, 0) + require.NoError(err) + require.Len(open, 1) + + note := "Separate legal entities that share a marketing domain." + rejected, err := st.ResolveOrganizationDuplicateSuggestionContext(ctx, + open[0].ID, store.OrganizationDuplicateStatusRejected, ¬e) + require.NoError(err) + assert.Equal(store.OrganizationDuplicateStatusRejected, rejected.Status) + require.NotNil(rejected.ResolvedAt) + require.NotNil(rejected.ResolutionNote) + assert.Equal(note, *rejected.ResolutionNote) + + rerun, err := st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + assert.Equal(0, rerun, "the same low-quality inference is not repeated") + + stillRejected, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, + store.OrganizationDuplicateStatusRejected, 0, 0) + require.NoError(err) + require.Len(stillRejected, 1) + assert.Equal(open[0].ID, stillRejected[0].ID) + + remainingOpen, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, + store.OrganizationDuplicateStatusOpen, 0, 0) + require.NoError(err) + assert.Empty(remainingOpen) +} + +func TestRefreshOrganizationDuplicateSuggestionsSkipsRetiredAndMergedOrganizations(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + domain := "example.com" + first, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", PrimaryDomain: &domain, + }) + require.NoError(err) + second, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org Inc", PrimaryDomain: &domain, + }) + require.NoError(err) + + _, err = st.RetireOrganizationContext(ctx, second.ID, second.Revision) + require.NoError(err) + merged, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Merged Example Org", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.MergeOrganizationsContext(ctx, first.ID, first.Revision, merged.ID, merged.Revision) + require.NoError(err) + merged, err = st.GetOrganizationContext(ctx, merged.ID) + require.NoError(err) + require.NotNil(merged.MergedIntoID) + _, err = st.DB().ExecContext(ctx, + st.Rebind(`UPDATE organizations SET retired_at = NULL WHERE id = ?`), merged.ID) + require.NoError(err) + + created, err := st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + assert.Equal(0, created, "retired organizations are not duplicate candidates") + + _, err = st.GetOrganizationContext(ctx, first.ID) + require.NoError(err) +} + +func TestRefreshOrganizationDuplicateSuggestionsUsesAliasesAndIdentifiers(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + first, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{Name: "Example Org"}) + require.NoError(err) + second, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{Name: "Second Org"}) + require.NoError(err) + + _, err = st.ReplaceOrganizationProfileContext(ctx, first.ID, first.Revision, + store.OrganizationProfileInput{ + Identifiers: []store.OrganizationIdentifierInput{{ + IdentifierKind: store.OrganizationIdentifierKindDomain, + Value: "shared.example", + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + }) + require.NoError(err) + refreshedSecond, err := st.GetOrganizationContext(ctx, second.ID) + require.NoError(err) + _, err = st.ReplaceOrganizationProfileContext(ctx, second.ID, refreshedSecond.Revision, + store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{{ + Name: "Example Org", + NameKind: store.OrganizationNameKindAlias, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + Identifiers: []store.OrganizationIdentifierInput{{ + IdentifierKind: store.OrganizationIdentifierKindDomain, + Value: "shared.example", + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + }) + require.NoError(err) + + created, err := st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + assert.Equal(2, created, + "an alias name and a child-table domain both produce suggestions") +} + +func TestResolveOrganizationDuplicateSuggestionValidatesInput(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + _, err := st.ResolveOrganizationDuplicateSuggestionContext(ctx, 9999, + store.OrganizationDuplicateStatusAccepted, nil) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationDuplicateSuggestionNotFound) + + domain := "example.com" + _, err = st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org Inc", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + open, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, + store.OrganizationDuplicateStatusOpen, 0, 0) + require.NoError(err) + require.Len(open, 1) + + _, err = st.ResolveOrganizationDuplicateSuggestionContext(ctx, open[0].ID, "maybe", nil) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationDuplicateSuggestionInvalid) + require.ErrorContains(err, `unknown status "maybe"`) + + _, err = st.ResolveOrganizationDuplicateSuggestionContext(ctx, open[0].ID, + store.OrganizationDuplicateStatusOpen, nil) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationDuplicateSuggestionInvalid) + require.ErrorContains(err, "cannot resolve a suggestion back to open") +} + +func TestResolveOrganizationDuplicateSuggestionIsOneWay(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + domain := "one-way.example" + _, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "One Way A", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "One Way B", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + open, err := st.ListOrganizationDuplicateSuggestionsContext( + ctx, store.OrganizationDuplicateStatusOpen, 0, 0) + require.NoError(err) + require.Len(open, 1) + + firstNote := "first decision" + first, err := st.ResolveOrganizationDuplicateSuggestionContext( + ctx, open[0].ID, store.OrganizationDuplicateStatusAccepted, &firstNote) + require.NoError(err) + assert.Equal(store.OrganizationDuplicateStatusAccepted, first.Status) + assert.Nil(first.ResolvedBy) + + secondNote := "later overwrite" + _, err = st.ResolveOrganizationDuplicateSuggestionContext( + ctx, open[0].ID, store.OrganizationDuplicateStatusRejected, &secondNote) + require.ErrorIs(err, store.ErrOrganizationDuplicateSuggestionAlreadyResolved) + + all, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, "", 0, 0) + require.NoError(err) + require.Len(all, 1) + assert.Equal(store.OrganizationDuplicateStatusAccepted, all[0].Status) + require.NotNil(all[0].ResolutionNote) + assert.Equal(firstNote, *all[0].ResolutionNote) + assert.Nil(all[0].ResolvedBy) +} + +func TestConcurrentOrganizationDuplicateSuggestionResolutionHasOneWinner(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + domain := "concurrent-resolution.example" + _, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Concurrent Resolution A", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Concurrent Resolution B", PrimaryDomain: &domain, + }) + require.NoError(err) + _, err = st.RefreshOrganizationDuplicateSuggestionsContext(ctx) + require.NoError(err) + open, err := st.ListOrganizationDuplicateSuggestionsContext( + ctx, store.OrganizationDuplicateStatusOpen, 0, 0) + require.NoError(err) + require.Len(open, 1) + + type result struct { + status string + err error + } + start := make(chan struct{}) + results := make(chan result, 2) + for _, status := range []string{ + store.OrganizationDuplicateStatusAccepted, + store.OrganizationDuplicateStatusRejected, + } { + go func(status string) { + <-start + _, resolveErr := st.ResolveOrganizationDuplicateSuggestionContext( + ctx, open[0].ID, status, nil) + results <- result{status: status, err: resolveErr} + }(status) + } + close(start) + + var winner string + conflicts := 0 + for range 2 { + got := <-results + switch { + case got.err == nil: + require.Empty(winner, "only one resolution may succeed") + winner = got.status + case errors.Is(got.err, store.ErrOrganizationDuplicateSuggestionAlreadyResolved): + conflicts++ + default: + require.NoError(got.err) + } + } + require.NotEmpty(winner) + assert.Equal(1, conflicts) + + all, err := st.ListOrganizationDuplicateSuggestionsContext(ctx, "", 0, 0) + require.NoError(err) + require.Len(all, 1) + assert.Equal(winner, all[0].Status) +} + +func TestPostgreSQLOrganizationDuplicateConfidenceUsesDoublePrecision(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + if !st.IsPostgreSQL() { + t.Skip("PostgreSQL runtime metadata is required") + } + + var dataType string + err := st.DB().QueryRowContext(ctx, ` + SELECT data_type + FROM information_schema.columns + WHERE table_schema = current_schema() + AND table_name = 'organization_duplicate_suggestions' + AND column_name = 'confidence' + `).Scan(&dataType) + require.NoError(err) + require.Equal("double precision", dataType) +} diff --git a/internal/store/organization_profile.go b/internal/store/organization_profile.go new file mode 100644 index 000000000..1a8cb26c0 --- /dev/null +++ b/internal/store/organization_profile.go @@ -0,0 +1,1007 @@ +package store + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/hex" + "fmt" + "strings" + "time" +) + +const ( + OrganizationNameKindAlias = "alias" + OrganizationNameKindLegal = "legal" + OrganizationNameKindFormer = "former" + OrganizationNameKindAbbreviation = "abbreviation" + OrganizationNameKindSort = "sort" + + OrganizationIdentifierKindDomain = "domain" + OrganizationIdentifierKindLinkedIn = "linkedin" + OrganizationIdentifierKindDUNS = "duns" + OrganizationIdentifierKindTaxID = "tax_id" + OrganizationIdentifierKindRegistry = "registry" + OrganizationIdentifierKindOther = "other" +) + +type OrganizationName struct { + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` + Name string `json:"name"` + NameNormalized string `json:"name_normalized"` + NameKind string `json:"name_kind"` +} + +type OrganizationNameInput struct { + Name string `json:"name"` + NameKind string `json:"name_kind"` + Envelope ValueEnvelopeInput `json:"envelope"` +} + +type OrganizationIdentifier struct { + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` + IdentifierKind string `json:"identifier_kind"` + Value string `json:"identifier_value"` + NormalizedValue string `json:"normalized_value"` +} + +type OrganizationIdentifierInput struct { + IdentifierKind string `json:"identifier_kind"` + Value string `json:"identifier_value"` + Envelope ValueEnvelopeInput `json:"envelope"` +} + +type OrganizationAddress struct { + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` + AddressKind PersonAddressKind `json:"address_kind"` + PostOfficeBox *string `json:"post_office_box,omitempty"` + ExtendedAddress *string `json:"extended_address,omitempty"` + StreetAddress *string `json:"street_address,omitempty"` + Locality *string `json:"locality,omitempty"` + Region *string `json:"region,omitempty"` + PostalCode *string `json:"postal_code,omitempty"` + CountryName *string `json:"country_name,omitempty"` + ExtendedComponents *string `json:"extended_components,omitempty"` + FreeText *string `json:"free_text,omitempty"` + Label *string `json:"label,omitempty"` + GeoURI *string `json:"geo_uri,omitempty"` + Timezone *string `json:"timezone,omitempty"` + CountryCode *string `json:"country_code,omitempty"` + PlaceURI *string `json:"place_uri,omitempty"` + OriginalValue string `json:"original_value"` +} + +type OrganizationAddressInput = PersonAddressInput + +type OrganizationContactPoint struct { + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` + AddressKind ContactAddressKind `json:"address_kind"` + ServiceSlug *string `json:"service_slug,omitempty"` + ServiceID *int64 `json:"-"` + ScopeKind *string `json:"scope_kind,omitempty"` + ScopeValue *string `json:"scope_value,omitempty"` + OriginalValue string `json:"original_value"` + NormalizedValue string `json:"normalized_value"` + Normalization string `json:"normalization"` + NormalizationVersion int `json:"normalization_version"` + URI *string `json:"uri,omitempty"` +} + +type OrganizationContactPointInput = PersonContactPointInput + +type OrganizationMedia struct { + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` + MediaKind PersonMediaKind `json:"media_kind"` + MediaType *string `json:"media_type,omitempty"` + URI *string `json:"uri,omitempty"` + ByteSize *int64 `json:"byte_size,omitempty"` + ContentHash *string `json:"content_hash,omitempty"` + HasData bool `json:"has_data"` + OriginalValue string `json:"original_value"` +} + +type OrganizationMediaInput = PersonMediaInput + +type OrganizationCategory struct { + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` + Category string `json:"category"` + CategoryNormalized string `json:"category_normalized"` +} + +type OrganizationCategoryInput struct { + Category string `json:"category"` + Envelope ValueEnvelopeInput `json:"envelope"` +} + +type OrganizationProfile struct { + Organization Organization `json:"organization"` + Names []OrganizationName `json:"names"` + Identifiers []OrganizationIdentifier `json:"identifiers"` + Addresses []OrganizationAddress `json:"addresses"` + ContactPoints []OrganizationContactPoint `json:"contact_points"` + Media []OrganizationMedia `json:"media"` + Categories []OrganizationCategory `json:"categories"` +} + +type OrganizationProfileInput struct { + Names []OrganizationNameInput + Identifiers []OrganizationIdentifierInput + Addresses []OrganizationAddressInput + ContactPoints []OrganizationContactPointInput + Media []OrganizationMediaInput + Categories []OrganizationCategoryInput +} + +type preparedOrganizationContact struct { + input OrganizationContactPointInput + serviceID any + serviceSlug *string + normalized string + normalization string + normalizationVersion int +} + +type preparedOrganizationProfile struct { + input OrganizationProfileInput + nameKeys []string + identifierKeys []string + addressKeys []string + contacts []preparedOrganizationContact + contactKeys []string + mediaKeys []string + categoryKeys []string +} + +func (s *Store) GetOrganizationProfileContext( + ctx context.Context, id int64, includeSuperseded bool, +) (*OrganizationProfile, error) { + var profile *OrganizationProfile + err := s.withTxContext(ctx, func(tx *loggedTx) error { + organization, err := getOrganizationForUpdateTx(ctx, tx, s.dialect, id) + if err != nil { + return err + } + profile, err = s.loadOrganizationProfileContext( + ctx, tx, organization, includeSuperseded) + return err + }) + if err != nil { + return nil, err + } + return profile, nil +} + +func (s *Store) ReplaceOrganizationProfileContext( + ctx context.Context, id, expectedRevision int64, input OrganizationProfileInput, +) (*OrganizationProfile, error) { + prepared, err := s.prepareOrganizationProfileContext(ctx, input) + if err != nil { + return nil, err + } + now := time.Now().UTC() + var profile *OrganizationProfile + err = s.withTxContext(ctx, func(tx *loggedTx) error { + result, err := tx.ExecContext(ctx, fmt.Sprintf(` + UPDATE organizations + SET revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? AND merged_into_id IS NULL + `, s.dialect.Now()), id, expectedRevision) + if err != nil { + return fmt.Errorf("bump organization profile revision: %w", err) + } + changed, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("check organization profile revision: %w", err) + } + if changed == 0 { + return organizationMutableCASMissTx(ctx, tx, id, expectedRevision) + } + organization, err := getOrganizationTx(ctx, tx, id) + if err != nil { + return err + } + current, err := s.loadOrganizationProfileContext(ctx, tx, organization, false) + if err != nil { + return err + } + if err := s.reconcileOrganizationProfileTx(ctx, tx, id, current, prepared, now); err != nil { + return err + } + profile, err = s.loadOrganizationProfileContext(ctx, tx, organization, false) + return err + }) + return profile, err +} + +func (s *Store) prepareOrganizationProfileContext( + ctx context.Context, input OrganizationProfileInput, +) (*preparedOrganizationProfile, error) { + prepared := &preparedOrganizationProfile{input: input} + nameSeen := map[string]int{} + for i := range prepared.input.Names { + row := &prepared.input.Names[i] + row.Name = strings.TrimSpace(row.Name) + if row.Name == "" { + return nil, fmt.Errorf("%w: names[%d].name is required", ErrOrganizationInvalid, i) + } + if !organizationNameKindValid(row.NameKind) { + return nil, fmt.Errorf("%w: names[%d].name_kind %q is unknown", + ErrOrganizationInvalid, i, row.NameKind) + } + if err := row.Envelope.Validate(); err != nil { + return nil, err + } + key := row.NameKind + "\x1f" + NormalizeOrganizationName(row.Name) + if previous, exists := nameSeen[key]; exists { + return nil, fmt.Errorf("%w: names[%d] duplicates names[%d]", + ErrOrganizationInvalid, i, previous) + } + nameSeen[key] = i + prepared.nameKeys = append(prepared.nameKeys, key) + } + identifierSeen := map[string]int{} + for i := range prepared.input.Identifiers { + row := &prepared.input.Identifiers[i] + row.IdentifierKind = strings.TrimSpace(row.IdentifierKind) + row.Value = strings.TrimSpace(row.Value) + if row.IdentifierKind == "" || row.Value == "" { + return nil, fmt.Errorf("%w: identifiers[%d].identifier_value is required", + ErrOrganizationInvalid, i) + } + if err := row.Envelope.Validate(); err != nil { + return nil, err + } + normalized := normalizeOrganizationIdentifier(row.IdentifierKind, row.Value) + if normalized == "" { + return nil, fmt.Errorf("%w: identifiers[%d].identifier_value is invalid", + ErrOrganizationInvalid, i) + } + key := row.IdentifierKind + "\x1f" + normalized + if previous, exists := identifierSeen[key]; exists { + return nil, fmt.Errorf("%w: identifiers[%d] duplicates identifiers[%d]", + ErrOrganizationInvalid, i, previous) + } + identifierSeen[key] = i + prepared.identifierKeys = append(prepared.identifierKeys, key) + } + addressSeen := map[string]int{} + for i := range prepared.input.Addresses { + row := &prepared.input.Addresses[i] + *row = canonicalOrganizationAddressInput(*row) + if !row.AddressKind.Valid() || !personAddressHasValue(*row) { + return nil, fmt.Errorf("%w: addresses[%d] is invalid", ErrOrganizationInvalid, i) + } + if err := row.Envelope.Validate(); err != nil { + return nil, err + } + key := organizationAddressFingerprint(*row) + if previous, exists := addressSeen[key]; exists { + return nil, fmt.Errorf("%w: addresses[%d] duplicates addresses[%d]", + ErrOrganizationInvalid, i, previous) + } + addressSeen[key] = i + prepared.addressKeys = append(prepared.addressKeys, key) + } + contactSeen := map[string]int{} + for i := range prepared.input.ContactPoints { + row := &prepared.input.ContactPoints[i] + row.ScopeKind = trimmedOrNil(row.ScopeKind) + row.ScopeValue = trimmedOrNil(row.ScopeValue) + if !row.AddressKind.Valid() || strings.TrimSpace(row.OriginalValue) == "" { + return nil, fmt.Errorf("%w: contact_points[%d].value_original is required", + ErrOrganizationInvalid, i) + } + if err := row.Envelope.Validate(); err != nil { + return nil, err + } + service, _, err := s.resolveOptionalCommunicationServiceContext(ctx, row.ServiceSlug) + if err != nil { + return nil, err + } + if err := ValidateServiceScope(service, row.ScopeKind, row.ScopeValue); err != nil { + return nil, err + } + normalized, err := NormalizeServiceValue(service, row.AddressKind, row.OriginalValue) + if err != nil { + return nil, err + } + contact := preparedOrganizationContact{ + input: *row, normalized: normalized, + normalization: fallbackContactNormalization(row.AddressKind), + normalizationVersion: 1, + } + if service != nil { + contact.serviceID = service.ID + contact.serviceSlug = &service.Slug + contact.normalization = service.Normalization + contact.normalizationVersion = service.NormalizationVersion + } + key := organizationContactKey( + row.AddressKind, contact.serviceID, row.ScopeKind, row.ScopeValue, normalized) + if previous, exists := contactSeen[key]; exists { + return nil, fmt.Errorf("%w: contact_points[%d] duplicates contact_points[%d]", + ErrOrganizationInvalid, i, previous) + } + contactSeen[key] = i + prepared.contacts = append(prepared.contacts, contact) + prepared.contactKeys = append(prepared.contactKeys, key) + } + mediaSeen := map[string]int{} + for i := range prepared.input.Media { + row := &prepared.input.Media[i] + *row = canonicalOrganizationMediaInput(*row) + if !row.MediaKind.Valid() || + (len(row.Data) == 0 && (row.URI == nil || strings.TrimSpace(*row.URI) == "")) { + return nil, fmt.Errorf("%w: media[%d] requires uri or blob_hash", + ErrOrganizationInvalid, i) + } + if len(row.Data) > MaxPersonMediaBytes { + return nil, ErrPersonMediaTooLarge + } + if err := row.Envelope.Validate(); err != nil { + return nil, err + } + key := organizationMediaFingerprint(*row) + if previous, exists := mediaSeen[key]; exists { + return nil, fmt.Errorf("%w: media[%d] duplicates media[%d]", + ErrOrganizationInvalid, i, previous) + } + mediaSeen[key] = i + prepared.mediaKeys = append(prepared.mediaKeys, key) + } + categorySeen := map[string]int{} + for i := range prepared.input.Categories { + row := &prepared.input.Categories[i] + row.Category = strings.TrimSpace(row.Category) + if row.Category == "" { + return nil, fmt.Errorf("%w: categories[%d].category is required", + ErrOrganizationInvalid, i) + } + if err := row.Envelope.Validate(); err != nil { + return nil, err + } + key := NormalizeOrganizationName(row.Category) + if previous, exists := categorySeen[key]; exists { + return nil, fmt.Errorf("%w: categories[%d] duplicates categories[%d]", + ErrOrganizationInvalid, i, previous) + } + categorySeen[key] = i + prepared.categoryKeys = append(prepared.categoryKeys, key) + } + return prepared, nil +} + +func (s *Store) reconcileOrganizationProfileTx( + ctx context.Context, tx *loggedTx, organizationID int64, + current *OrganizationProfile, prepared *preparedOrganizationProfile, now time.Time, +) error { + if err := reconcileOrganizationCollection(ctx, tx, s, organizationID, + "organization_names", current.Names, prepared.input.Names, + func(row OrganizationName) int64 { return row.Envelope.ID }, + func(row OrganizationName) string { + return row.NameKind + "\x1f" + row.NameNormalized + }, + func(_ OrganizationNameInput, i int) string { return prepared.nameKeys[i] }, + func(row OrganizationNameInput) (int64, error) { + return s.insertOrganizationNameTx(ctx, tx, organizationID, row, now) + }, now); err != nil { + return err + } + if err := reconcileOrganizationCollection(ctx, tx, s, organizationID, + "organization_identifiers", current.Identifiers, prepared.input.Identifiers, + func(row OrganizationIdentifier) int64 { return row.Envelope.ID }, + func(row OrganizationIdentifier) string { + return row.IdentifierKind + "\x1f" + row.NormalizedValue + }, + func(_ OrganizationIdentifierInput, i int) string { return prepared.identifierKeys[i] }, + func(row OrganizationIdentifierInput) (int64, error) { + return s.insertOrganizationIdentifierTx(ctx, tx, organizationID, row, now) + }, now); err != nil { + return err + } + if err := reconcileOrganizationCollection(ctx, tx, s, organizationID, + "organization_addresses", current.Addresses, prepared.input.Addresses, + func(row OrganizationAddress) int64 { return row.Envelope.ID }, + organizationAddressRowFingerprint, + func(_ OrganizationAddressInput, i int) string { return prepared.addressKeys[i] }, + func(row OrganizationAddressInput) (int64, error) { + return s.insertOrganizationAddressTx(ctx, tx, organizationID, row, now) + }, now); err != nil { + return err + } + if err := reconcileOrganizationCollection(ctx, tx, s, organizationID, + "organization_contact_points", current.ContactPoints, prepared.contacts, + func(row OrganizationContactPoint) int64 { return row.Envelope.ID }, + func(row OrganizationContactPoint) string { + var serviceID any + if row.ServiceID != nil { + serviceID = *row.ServiceID + } + return organizationContactKey( + row.AddressKind, serviceID, row.ScopeKind, row.ScopeValue, row.NormalizedValue) + }, + func(_ preparedOrganizationContact, i int) string { return prepared.contactKeys[i] }, + func(row preparedOrganizationContact) (int64, error) { + return s.insertOrganizationContactTx(ctx, tx, organizationID, row, now) + }, now); err != nil { + return err + } + if err := reconcileOrganizationCollection(ctx, tx, s, organizationID, + "organization_media", current.Media, prepared.input.Media, + func(row OrganizationMedia) int64 { return row.Envelope.ID }, + organizationMediaRowFingerprint, + func(_ OrganizationMediaInput, i int) string { return prepared.mediaKeys[i] }, + func(row OrganizationMediaInput) (int64, error) { + return s.insertOrganizationMediaTx(ctx, tx, organizationID, row, now) + }, now); err != nil { + return err + } + return reconcileOrganizationCollection(ctx, tx, s, organizationID, + "organization_categories", current.Categories, prepared.input.Categories, + func(row OrganizationCategory) int64 { return row.Envelope.ID }, + func(row OrganizationCategory) string { return row.CategoryNormalized }, + func(_ OrganizationCategoryInput, i int) string { return prepared.categoryKeys[i] }, + func(row OrganizationCategoryInput) (int64, error) { + return s.insertOrganizationCategoryTx(ctx, tx, organizationID, row, now) + }, now) +} + +func reconcileOrganizationCollection[C any, D any]( + ctx context.Context, tx *loggedTx, s *Store, organizationID int64, table string, + current []C, desired []D, currentID func(C) int64, currentKey func(C) string, + desiredKey func(D, int) string, insert func(D) (int64, error), now time.Time, +) error { + existing := make(map[string]int64, len(current)) + for _, row := range current { + existing[currentKey(row)] = currentID(row) + } + kept := make([]int64, 0, len(desired)) + for i, row := range desired { + if id, ok := existing[desiredKey(row, i)]; ok { + kept = append(kept, id) + continue + } + id, err := insert(row) + if err != nil { + return err + } + kept = append(kept, id) + } + return s.supersedeOrganizationRowsTx(ctx, tx, table, organizationID, kept, now) +} + +func (s *Store) supersedeOrganizationRowsTx( + ctx context.Context, tx *loggedTx, table string, organizationID int64, + kept []int64, now time.Time, +) error { + where := ` WHERE organization_id = ? + AND active_until IS NULL AND superseded_at IS NULL` + args := []any{organizationID} + if len(kept) > 0 { + where += ` AND id NOT IN (` + placeholders(len(kept)) + `)` + for _, id := range kept { + args = append(args, id) + } + } + futureArgs := append([]any{now}, args...) + futureArgs = append(futureArgs, now) + if _, err := tx.ExecContext(ctx, `UPDATE `+table+` + SET superseded_at = ?, updated_at = `+s.dialect.Now()+where+` + AND active_from IS NOT NULL AND active_from > ?`, futureArgs...); err != nil { + return fmt.Errorf("retract future %s: %w", table, err) + } + effectiveArgs := append([]any{now, now}, args...) + effectiveArgs = append(effectiveArgs, now) + if _, err := tx.ExecContext(ctx, `UPDATE `+table+` + SET active_until = ?, superseded_at = ?, updated_at = `+s.dialect.Now()+where+` + AND (active_from IS NULL OR active_from <= ?)`, effectiveArgs...); err != nil { + return fmt.Errorf("supersede %s: %w", table, err) + } + return nil +} + +func (s *Store) insertOrganizationNameTx( + ctx context.Context, tx *loggedTx, organizationID int64, + input OrganizationNameInput, now time.Time, +) (int64, error) { + env, err := resolveOrganizationEnvelopeTx( + ctx, tx, "organization_names", "name_kind", + organizationID, input.NameKind, input.Envelope, now, + ) + if err != nil { + return 0, err + } + args := []any{ + organizationID, input.NameKind, input.Name, input.Name, + NormalizeOrganizationName(input.Name), + } + return s.insertOrganizationProfileRowTx(ctx, tx, "organization_names", + "organization_id, name_kind, formatted, original_value, name_normalized", + args, env) +} + +func (s *Store) insertOrganizationIdentifierTx( + ctx context.Context, tx *loggedTx, organizationID int64, + input OrganizationIdentifierInput, now time.Time, +) (int64, error) { + env, err := resolveOrganizationEnvelopeTx( + ctx, tx, "organization_identifiers", "identifier_kind", + organizationID, input.IdentifierKind, input.Envelope, now, + ) + if err != nil { + return 0, err + } + args := []any{ + organizationID, input.IdentifierKind, input.Value, + normalizeOrganizationIdentifier(input.IdentifierKind, input.Value), + } + return s.insertOrganizationProfileRowTx(ctx, tx, "organization_identifiers", + "organization_id, identifier_kind, identifier_value, normalized_value", args, env) +} + +func (s *Store) insertOrganizationAddressTx( + ctx context.Context, tx *loggedTx, organizationID int64, + input OrganizationAddressInput, now time.Time, +) (int64, error) { + input = canonicalOrganizationAddressInput(input) + env, err := resolveOrganizationEnvelopeTx( + ctx, tx, "organization_addresses", "address_kind", + organizationID, input.AddressKind, input.Envelope, now, + ) + if err != nil { + return 0, err + } + args := []any{ + organizationID, input.AddressKind, stringValue(input.PostOfficeBox), + stringValue(input.ExtendedAddress), stringValue(input.StreetAddress), + stringValue(input.Locality), stringValue(input.Region), + stringValue(input.PostalCode), stringValue(input.CountryName), + stringValue(input.ExtendedComponents), stringValue(input.FreeText), + stringValue(input.Label), stringValue(input.GeoURI), stringValue(input.Timezone), + stringValue(input.CountryCode), stringValue(input.PlaceURI), input.OriginalValue, + } + return s.insertOrganizationProfileRowTx(ctx, tx, "organization_addresses", + `organization_id, address_kind, post_office_box, extended_address, + street_address, locality, region, postal_code, country_name, + extended_components, free_text, label, geo_uri, timezone, country_code, + place_uri, original_value`, args, env) +} + +func (s *Store) insertOrganizationContactTx( + ctx context.Context, tx *loggedTx, organizationID int64, + row preparedOrganizationContact, now time.Time, +) (int64, error) { + env, err := resolveOrganizationEnvelopeTx( + ctx, tx, "organization_contact_points", "address_kind", + organizationID, row.input.AddressKind, row.input.Envelope, now, + ) + if err != nil { + return 0, err + } + args := []any{ + organizationID, row.input.AddressKind, row.serviceID, + stringValue(row.input.ScopeKind), stringValue(row.input.ScopeValue), + row.input.OriginalValue, row.normalized, row.normalization, + row.normalizationVersion, stringValue(row.input.URI), + } + return s.insertOrganizationProfileRowTx(ctx, tx, "organization_contact_points", + `organization_id, address_kind, service_id, scope_kind, scope_value, + original_value, normalized_value, normalization, normalization_version, uri`, + args, env) +} + +func (s *Store) insertOrganizationMediaTx( + ctx context.Context, tx *loggedTx, organizationID int64, + input OrganizationMediaInput, now time.Time, +) (int64, error) { + input = canonicalOrganizationMediaInput(input) + env, err := resolveOrganizationEnvelopeTx( + ctx, tx, "organization_media", "media_kind", + organizationID, input.MediaKind, input.Envelope, now, + ) + if err != nil { + return 0, err + } + var data, byteSize, contentHash any + if len(input.Data) > 0 { + digest := sha256.Sum256(input.Data) + data, byteSize, contentHash = input.Data, int64(len(input.Data)), hex.EncodeToString(digest[:]) + } + args := []any{ + organizationID, input.MediaKind, stringValue(input.MediaType), + stringValue(input.URI), data, byteSize, contentHash, input.OriginalValue, + } + return s.insertOrganizationProfileRowTx(ctx, tx, "organization_media", + `organization_id, media_kind, media_type, uri, data, byte_size, + content_hash, original_value`, args, env) +} + +func (s *Store) insertOrganizationCategoryTx( + ctx context.Context, tx *loggedTx, organizationID int64, + input OrganizationCategoryInput, now time.Time, +) (int64, error) { + env, err := resolveOrganizationEnvelopeTx( + ctx, tx, "organization_categories", "", + organizationID, nil, input.Envelope, now, + ) + if err != nil { + return 0, err + } + args := []any{ + organizationID, input.Category, NormalizeOrganizationName(input.Category), + } + return s.insertOrganizationProfileRowTx(ctx, tx, "organization_categories", + "organization_id, original_value, normalized_value", args, env) +} + +func (s *Store) insertOrganizationProfileRowTx( + ctx context.Context, tx *loggedTx, table, columns string, + args []any, env ValueEnvelope, +) (int64, error) { + args = append(args, profileEnvelopeArgs(env)...) + query := `INSERT INTO ` + table + ` (` + columns + `, ` + + profileEnvelopeWriteColumns + `, created_at, updated_at) VALUES (` + + placeholders(len(args)) + `, ` + s.dialect.Now() + `, ` + s.dialect.Now() + + `) RETURNING id` + var id int64 + if err := tx.QueryRowContext(ctx, query, args...).Scan(&id); err != nil { + return 0, fmt.Errorf("insert %s: %w", table, err) + } + return id, nil +} + +func resolveOrganizationEnvelopeTx( + ctx context.Context, + tx *loggedTx, + table, kindColumn string, + organizationID int64, + kind any, + input ValueEnvelopeInput, + now time.Time, +) (ValueEnvelope, error) { + env, err := resolveProfileEnvelopeForOwnerTx( + ctx, tx, table, "organization_id", kindColumn, + organizationID, kind, input, + ) + if err != nil { + return ValueEnvelope{}, err + } + if env.ActiveFrom == nil { + env.ActiveFrom = &now + } + env.ActiveUntil = nil + env.SupersededAt = nil + return env, nil +} + +func placeholders(count int) string { + if count <= 0 { + return "" + } + return strings.TrimSuffix(strings.Repeat("?,", count), ",") +} + +func organizationNameKindValid(kind string) bool { + switch kind { + case OrganizationNameKindAlias, OrganizationNameKindLegal, OrganizationNameKindFormer, + OrganizationNameKindAbbreviation, OrganizationNameKindSort: + return true + default: + return false + } +} + +func normalizeOrganizationIdentifier(kind, value string) string { + if kind == OrganizationIdentifierKindDomain { + return NormalizeDomain(value) + } + return strings.ToLower(strings.TrimSpace(value)) +} + +func canonicalOrganizationAddressInput(input OrganizationAddressInput) OrganizationAddressInput { + input.PostOfficeBox = trimmedOrNil(input.PostOfficeBox) + input.ExtendedAddress = trimmedOrNil(input.ExtendedAddress) + input.StreetAddress = trimmedOrNil(input.StreetAddress) + input.Locality = trimmedOrNil(input.Locality) + input.Region = trimmedOrNil(input.Region) + input.PostalCode = trimmedOrNil(input.PostalCode) + input.CountryName = trimmedOrNil(input.CountryName) + input.ExtendedComponents = trimmedOrNil(input.ExtendedComponents) + input.FreeText = trimmedOrNil(input.FreeText) + input.Label = trimmedOrNil(input.Label) + input.GeoURI = trimmedOrNil(input.GeoURI) + input.Timezone = trimmedOrNil(input.Timezone) + input.CountryCode = trimmedOrNil(input.CountryCode) + input.PlaceURI = trimmedOrNil(input.PlaceURI) + input.OriginalValue = strings.TrimSpace(input.OriginalValue) + if input.OriginalValue == "" { + input.OriginalValue = personAddressOriginalValue(input) + } + return input +} + +func canonicalOrganizationMediaInput(input OrganizationMediaInput) OrganizationMediaInput { + input.MediaType = trimmedOrNil(input.MediaType) + input.URI = trimmedOrNil(input.URI) + input.OriginalValue = strings.TrimSpace(input.OriginalValue) + if input.OriginalValue == "" && input.URI != nil { + input.OriginalValue = *input.URI + } + return input +} + +func organizationAddressFingerprint(input OrganizationAddressInput) string { + input = canonicalOrganizationAddressInput(input) + return strings.Join([]string{ + string(input.AddressKind), derefString(input.PostOfficeBox), + derefString(input.ExtendedAddress), derefString(input.StreetAddress), + derefString(input.Locality), derefString(input.Region), + derefString(input.PostalCode), derefString(input.CountryName), + derefString(input.ExtendedComponents), derefString(input.FreeText), + derefString(input.Label), derefString(input.GeoURI), + derefString(input.Timezone), derefString(input.CountryCode), + derefString(input.PlaceURI), strings.TrimSpace(input.OriginalValue), + }, "\x1f") +} + +func organizationAddressRowFingerprint(row OrganizationAddress) string { + return organizationAddressFingerprint(OrganizationAddressInput{ + AddressKind: row.AddressKind, PostOfficeBox: row.PostOfficeBox, + ExtendedAddress: row.ExtendedAddress, StreetAddress: row.StreetAddress, + Locality: row.Locality, Region: row.Region, PostalCode: row.PostalCode, + CountryName: row.CountryName, ExtendedComponents: row.ExtendedComponents, + FreeText: row.FreeText, Label: row.Label, GeoURI: row.GeoURI, + Timezone: row.Timezone, CountryCode: row.CountryCode, PlaceURI: row.PlaceURI, + OriginalValue: row.OriginalValue, + }) +} + +func organizationContactKey( + kind ContactAddressKind, serviceID any, scopeKind, scopeValue *string, normalized string, +) string { + scopeKind = trimmedOrNil(scopeKind) + scopeValue = trimmedOrNil(scopeValue) + return strings.Join([]string{ + string(kind), fmt.Sprint(serviceID), derefString(scopeKind), + derefString(scopeValue), normalized, + }, "\x1f") +} + +func organizationMediaFingerprint(input OrganizationMediaInput) string { + input = canonicalOrganizationMediaInput(input) + digest := "" + if len(input.Data) > 0 { + sum := sha256.Sum256(input.Data) + digest = hex.EncodeToString(sum[:]) + } + return strings.Join([]string{ + string(input.MediaKind), derefString(input.MediaType), derefString(input.URI), + digest, strings.TrimSpace(input.OriginalValue), + }, "\x1f") +} + +func organizationMediaRowFingerprint(row OrganizationMedia) string { + input := canonicalOrganizationMediaInput(OrganizationMediaInput{ + MediaKind: row.MediaKind, MediaType: row.MediaType, URI: row.URI, + OriginalValue: row.OriginalValue, + }) + return strings.Join([]string{ + string(input.MediaKind), derefString(input.MediaType), derefString(input.URI), + derefString(row.ContentHash), input.OriginalValue, + }, "\x1f") +} + +type organizationProfileQuerier interface { + QueryContext(ctx context.Context, query string, args ...any) (*loggedRows, error) +} + +func (s *Store) loadOrganizationProfileContext( + ctx context.Context, queryer organizationProfileQuerier, + organization *Organization, includeSuperseded bool, +) (*OrganizationProfile, error) { + profile := &OrganizationProfile{Organization: *organization} + var err error + if profile.Names, err = queryOrganizationRows( + ctx, queryer, organizationNameSelect, organization.ID, includeSuperseded, + scanOrganizationName); err != nil { + return nil, err + } + if profile.Identifiers, err = queryOrganizationRows( + ctx, queryer, organizationIdentifierSelect, organization.ID, includeSuperseded, + scanOrganizationIdentifier); err != nil { + return nil, err + } + if profile.Addresses, err = queryOrganizationRows( + ctx, queryer, organizationAddressSelect, organization.ID, includeSuperseded, + scanOrganizationAddress); err != nil { + return nil, err + } + if profile.ContactPoints, err = queryOrganizationRows( + ctx, queryer, organizationContactSelect, organization.ID, includeSuperseded, + scanOrganizationContact); err != nil { + return nil, err + } + if profile.Media, err = queryOrganizationRows( + ctx, queryer, organizationMediaSelect, organization.ID, includeSuperseded, + scanOrganizationMedia); err != nil { + return nil, err + } + profile.Categories, err = queryOrganizationRows( + ctx, queryer, organizationCategorySelect, organization.ID, includeSuperseded, + scanOrganizationCategory) + if err != nil { + return nil, err + } + return profile, nil +} + +func queryOrganizationRows[T any]( + ctx context.Context, queryer organizationProfileQuerier, base string, + organizationID int64, includeSuperseded bool, + scan func(scanner) (*T, error), +) ([]T, error) { + query := base + ` WHERE organization_id = ?` + if !includeSuperseded { + query += ` AND active_until IS NULL AND superseded_at IS NULL` + } + if includeSuperseded { + query += ` ORDER BY CASE WHEN active_until IS NULL AND superseded_at IS NULL + THEN 0 ELSE 1 END, ordinal, id` + } else { + query += ` ORDER BY ordinal, id` + } + rows, err := queryer.QueryContext(ctx, query, organizationID) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + result := make([]T, 0) + for rows.Next() { + row, err := scan(rows) + if err != nil { + return nil, err + } + result = append(result, *row) + } + return result, rows.Err() +} + +const organizationNameSelect = `SELECT id, organization_id, original_value, + name_normalized, name_kind, ` + profileEnvelopeReadColumns + ` FROM organization_names` +const organizationIdentifierSelect = `SELECT id, organization_id, identifier_kind, + identifier_value, normalized_value, ` + profileEnvelopeReadColumns + ` + FROM organization_identifiers` +const organizationAddressSelect = `SELECT id, organization_id, address_kind, + post_office_box, extended_address, street_address, locality, region, postal_code, + country_name, extended_components, free_text, label, geo_uri, timezone, + country_code, place_uri, original_value, ` + profileEnvelopeReadColumns + ` + FROM organization_addresses` +const organizationContactSelect = `SELECT p.id, p.organization_id, p.address_kind, + p.service_id, (SELECT slug FROM communication_services WHERE id = p.service_id), + p.scope_kind, p.scope_value, p.original_value, p.normalized_value, + p.normalization, p.normalization_version, p.uri, ` + profileEnvelopeReadColumns + ` + FROM organization_contact_points p` +const organizationMediaSelect = `SELECT id, organization_id, media_kind, media_type, + uri, byte_size, content_hash, (data IS NOT NULL) AS has_data, original_value, + ` + profileEnvelopeReadColumns + ` FROM organization_media` +const organizationCategorySelect = `SELECT id, organization_id, original_value, + normalized_value, ` + profileEnvelopeReadColumns + ` FROM organization_categories` + +func scanOrganizationName(row scanner) (*OrganizationName, error) { + var value OrganizationName + var env profileEnvelopeScanValues + dest := []any{ + &value.Envelope.ID, &value.OrganizationID, &value.Name, + &value.NameNormalized, &value.NameKind, + } + dest = append(dest, env.destinations()...) + if err := row.Scan(dest...); err != nil { + return nil, err + } + return &value, env.apply(&value.Envelope) +} + +func scanOrganizationIdentifier(row scanner) (*OrganizationIdentifier, error) { + var value OrganizationIdentifier + var env profileEnvelopeScanValues + dest := []any{ + &value.Envelope.ID, &value.OrganizationID, &value.IdentifierKind, + &value.Value, &value.NormalizedValue, + } + dest = append(dest, env.destinations()...) + if err := row.Scan(dest...); err != nil { + return nil, err + } + return &value, env.apply(&value.Envelope) +} + +func scanOrganizationAddress(row scanner) (*OrganizationAddress, error) { + var value OrganizationAddress + var postOfficeBox, extendedAddress, streetAddress, locality sql.NullString + var region, postalCode, countryName, extendedComponents sql.NullString + var freeText, label, geoURI, timezone, countryCode, placeURI sql.NullString + var env profileEnvelopeScanValues + dest := []any{ + &value.Envelope.ID, &value.OrganizationID, &value.AddressKind, + &postOfficeBox, &extendedAddress, &streetAddress, &locality, ®ion, + &postalCode, &countryName, &extendedComponents, &freeText, &label, + &geoURI, &timezone, &countryCode, &placeURI, &value.OriginalValue, + } + dest = append(dest, env.destinations()...) + if err := row.Scan(dest...); err != nil { + return nil, err + } + value.PostOfficeBox, value.ExtendedAddress = nullStringPtr(postOfficeBox), nullStringPtr(extendedAddress) + value.StreetAddress, value.Locality = nullStringPtr(streetAddress), nullStringPtr(locality) + value.Region, value.PostalCode = nullStringPtr(region), nullStringPtr(postalCode) + value.CountryName, value.ExtendedComponents = nullStringPtr(countryName), nullStringPtr(extendedComponents) + value.FreeText, value.Label = nullStringPtr(freeText), nullStringPtr(label) + value.GeoURI, value.Timezone = nullStringPtr(geoURI), nullStringPtr(timezone) + value.CountryCode, value.PlaceURI = nullStringPtr(countryCode), nullStringPtr(placeURI) + return &value, env.apply(&value.Envelope) +} + +func scanOrganizationContact(row scanner) (*OrganizationContactPoint, error) { + var value OrganizationContactPoint + var serviceSlug, scopeKind, scopeValue, uri sql.NullString + var serviceID sql.NullInt64 + var env profileEnvelopeScanValues + dest := []any{ + &value.Envelope.ID, &value.OrganizationID, &value.AddressKind, + &serviceID, &serviceSlug, &scopeKind, &scopeValue, &value.OriginalValue, + &value.NormalizedValue, &value.Normalization, + &value.NormalizationVersion, &uri, + } + dest = append(dest, env.destinations()...) + if err := row.Scan(dest...); err != nil { + return nil, err + } + value.ServiceSlug, value.ScopeKind = nullStringPtr(serviceSlug), nullStringPtr(scopeKind) + if serviceID.Valid { + value.ServiceID = new(serviceID.Int64) + } + value.ScopeValue, value.URI = nullStringPtr(scopeValue), nullStringPtr(uri) + return &value, env.apply(&value.Envelope) +} + +func scanOrganizationMedia(row scanner) (*OrganizationMedia, error) { + var value OrganizationMedia + var mediaType, uri, contentHash sql.NullString + var byteSize sql.NullInt64 + var env profileEnvelopeScanValues + dest := []any{ + &value.Envelope.ID, &value.OrganizationID, &value.MediaKind, + &mediaType, &uri, &byteSize, &contentHash, &value.HasData, + &value.OriginalValue, + } + dest = append(dest, env.destinations()...) + if err := row.Scan(dest...); err != nil { + return nil, err + } + value.MediaType, value.URI = nullStringPtr(mediaType), nullStringPtr(uri) + if byteSize.Valid { + value.ByteSize = new(byteSize.Int64) + } + value.ContentHash = nullStringPtr(contentHash) + return &value, env.apply(&value.Envelope) +} + +func scanOrganizationCategory(row scanner) (*OrganizationCategory, error) { + var value OrganizationCategory + var env profileEnvelopeScanValues + dest := []any{ + &value.Envelope.ID, &value.OrganizationID, + &value.Category, &value.CategoryNormalized, + } + dest = append(dest, env.destinations()...) + if err := row.Scan(dest...); err != nil { + return nil, err + } + return &value, env.apply(&value.Envelope) +} diff --git a/internal/store/organization_profile_test.go b/internal/store/organization_profile_test.go new file mode 100644 index 000000000..bafdaae26 --- /dev/null +++ b/internal/store/organization_profile_test.go @@ -0,0 +1,324 @@ +package store_test + +import ( + "context" + "log/slog" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestReplaceOrganizationProfileRoundTripsEveryCollectionAndKeepsStableRows(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + + uri := "https://example.com" + first, err := st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{{ + Name: "Example Organisation", NameKind: store.OrganizationNameKindAlias, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + Identifiers: []store.OrganizationIdentifierInput{{ + IdentifierKind: store.OrganizationIdentifierKindDomain, + Value: "Example.COM", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + Addresses: []store.OrganizationAddressInput{{ + AddressKind: store.PersonAddressPostal, StreetAddress: new("1 Example St"), + Locality: new("Exampletown"), OriginalValue: "1 Example St;Exampletown", + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + ContactPoints: []store.OrganizationContactPointInput{{ + AddressKind: store.ContactAddressEmail, OriginalValue: "INFO@EXAMPLE.COM", + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + Media: []store.OrganizationMediaInput{{ + MediaKind: store.PersonMediaLogo, URI: &uri, OriginalValue: uri, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + Categories: []store.OrganizationCategoryInput{{ + Category: "Vendor", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + }) + require.NoError(err) + require.Len(first.Names, 1) + require.Len(first.Identifiers, 1) + require.Len(first.Addresses, 1) + require.Len(first.ContactPoints, 1) + require.Len(first.Media, 1) + require.Len(first.Categories, 1) + assert.Equal("example.com", first.Identifiers[0].NormalizedValue) + assert.Equal("info@example.com", first.ContactPoints[0].NormalizedValue) + assert.Equal(organization.Revision+1, first.Organization.Revision) + + second, err := st.ReplaceOrganizationProfileContext( + ctx, organization.ID, first.Organization.Revision, store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{{Name: "Example Organisation", NameKind: store.OrganizationNameKindAlias, Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}}, + Identifiers: []store.OrganizationIdentifierInput{{IdentifierKind: store.OrganizationIdentifierKindDomain, Value: "Example.COM", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}}, + Addresses: []store.OrganizationAddressInput{{AddressKind: store.PersonAddressPostal, StreetAddress: new("1 Example St"), Locality: new("Exampletown"), OriginalValue: "1 Example St;Exampletown", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}}, + ContactPoints: []store.OrganizationContactPointInput{{AddressKind: store.ContactAddressEmail, OriginalValue: "INFO@EXAMPLE.COM", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}}, + Media: []store.OrganizationMediaInput{{MediaKind: store.PersonMediaLogo, URI: &uri, OriginalValue: uri, Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}}, + }) + require.NoError(err) + assert.Equal(first.Names[0].Envelope.ID, second.Names[0].Envelope.ID) + assert.Equal(first.Identifiers[0].Envelope.ID, second.Identifiers[0].Envelope.ID) + assert.Equal(first.Addresses[0].Envelope.ID, second.Addresses[0].Envelope.ID) + assert.Equal(first.ContactPoints[0].Envelope.ID, second.ContactPoints[0].Envelope.ID) + assert.Equal(first.Media[0].Envelope.ID, second.Media[0].Envelope.ID) + assert.Empty(second.Categories) + + history, err := st.GetOrganizationProfileContext(ctx, organization.ID, true) + require.NoError(err) + require.Len(history.Categories, 1) + assert.False(history.Categories[0].Envelope.IsCurrent()) +} + +func TestReplaceOrganizationProfileAppendsOmittedOrdinalsAcrossHistory(t *testing.T) { + assertions := assert.New(t) + requirements := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext( + ctx, store.OrganizationInput{Name: "Example Org"}) + requirements.NoError(err) + + first, err := st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Categories: []store.OrganizationCategoryInput{ + {Category: "Vendor", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}, + {Category: "Partner", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}, + }, + }) + requirements.NoError(err) + requirements.Len(first.Categories, 2) + assertions.Equal(0, first.Categories[0].Envelope.Ordinal) + assertions.Equal(1, first.Categories[1].Envelope.Ordinal) + + second, err := st.ReplaceOrganizationProfileContext( + ctx, organization.ID, first.Organization.Revision, store.OrganizationProfileInput{ + Categories: []store.OrganizationCategoryInput{{ + Category: "Customer", Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + }) + requirements.NoError(err) + requirements.Len(second.Categories, 1) + assertions.Equal(2, second.Categories[0].Envelope.Ordinal, + "automatic ordinals must not reuse a superseded history slot") +} + +func TestReplaceOrganizationProfileValidatesBeforeWriting(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext( + ctx, store.OrganizationInput{Name: "Example Org"}) + require.NoError(err) + + _, err = st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision+1, store.OrganizationProfileInput{}) + require.ErrorIs(err, store.ErrOrganizationRevisionConflict) + + _, err = st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{{ + Name: " ", NameKind: store.OrganizationNameKindAlias, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + }) + require.ErrorIs(err, store.ErrOrganizationInvalid) + require.ErrorContains(err, "names[0].name is required") + + _, err = st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{ + {Name: "Example Org", NameKind: store.OrganizationNameKindAlias, Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}, + {Name: " example org ", NameKind: store.OrganizationNameKindAlias, Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}}, + }, + }) + require.ErrorIs(err, store.ErrOrganizationInvalid) + require.ErrorContains(err, "names[1] duplicates names[0]") + + _, err = st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Categories: []store.OrganizationCategoryInput{{ + Category: "Vendor", Envelope: store.ValueEnvelopeInput{Source: "guessed"}, + }}, + }) + require.ErrorIs(err, store.ErrInvalidProvenance) +} + +func TestRemovingFutureOrganizationProfileRowRetractsWithoutInvalidWorldInterval( + t *testing.T, +) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext( + ctx, store.OrganizationInput{Name: "Example Org"}) + require.NoError(err) + future := time.Now().UTC().Add(24 * time.Hour) + + withFuture, err := st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{{ + Name: "Future Alias", NameKind: store.OrganizationNameKindAlias, + Envelope: store.ValueEnvelopeInput{ + Source: store.ProvenanceUser, ActiveFrom: &future, + }, + }}, + }) + require.NoError(err) + + _, err = st.ReplaceOrganizationProfileContext( + ctx, organization.ID, withFuture.Organization.Revision, + store.OrganizationProfileInput{}) + require.NoError(err) + history, err := st.GetOrganizationProfileContext(ctx, organization.ID, true) + require.NoError(err) + require.Len(history.Names, 1) + assert.Nil(history.Names[0].Envelope.ActiveUntil) + assert.NotNil(history.Names[0].Envelope.SupersededAt) + require.NotNil(history.Names[0].Envelope.ActiveFrom) + assert.WithinDuration(future, *history.Names[0].Envelope.ActiveFrom, time.Microsecond) +} + +func TestGetOrganizationProfileRejectsMissingOrganization(t *testing.T) { + require := require.New(t) + st := testutil.NewTestStore(t) + _, err := st.GetOrganizationProfileContext(context.Background(), 9999, false) + require.ErrorIs(err, store.ErrOrganizationNotFound) +} + +func TestGetOrganizationProfileReturnsOneRootAndChildrenSnapshot(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext( + ctx, store.OrganizationInput{Name: "Old Root"}) + require.NoError(err) + initial, err := st.ReplaceOrganizationProfileContext( + ctx, organization.ID, organization.Revision, store.OrganizationProfileInput{ + Names: []store.OrganizationNameInput{{ + Name: "Old Alias", NameKind: store.OrganizationNameKindAlias, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + }) + require.NoError(err) + require.Len(initial.Names, 1) + + writer, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + committed := false + t.Cleanup(func() { + if !committed { + _ = writer.Rollback() + } + }) + _, err = writer.ExecContext(ctx, st.Rebind(` + UPDATE organizations + SET name = ?, name_normalized = ?, revision = revision + 1 + WHERE id = ? + `), "New Root", "new root", organization.ID) + require.NoError(err) + _, err = writer.ExecContext(ctx, st.Rebind(` + UPDATE organization_names + SET formatted = ?, original_value = ?, name_normalized = ? + WHERE id = ? + `), "New Alias", "New Alias", "new alias", initial.Names[0].Envelope.ID) + require.NoError(err) + + rootRead := make(chan struct{}) + releaseRootRead := make(chan struct{}) + previousLogger := slog.Default() + slog.SetDefault(slog.New(&organizationProfileBarrierHandler{ + rootRead: rootRead, release: releaseRootRead, + })) + t.Cleanup(func() { slog.SetDefault(previousLogger) }) + + type result struct { + profile *store.OrganizationProfile + err error + } + resultCh := make(chan result, 1) + go func() { + profile, readErr := st.GetOrganizationProfileContext(ctx, organization.ID, false) + resultCh <- result{profile: profile, err: readErr} + }() + + select { + case <-rootRead: + case <-time.After(100 * time.Millisecond): + } + require.NoError(writer.Commit()) + committed = true + close(releaseRootRead) + + got := <-resultCh + require.NoError(got.err) + require.Len(got.profile.Names, 1) + switch got.profile.Organization.Name { + case "Old Root": + assert.Equal(initial.Organization.Revision, got.profile.Organization.Revision) + assert.Equal("Old Alias", got.profile.Names[0].Name) + case "New Root": + assert.Equal(initial.Organization.Revision+1, got.profile.Organization.Revision) + assert.Equal("New Alias", got.profile.Names[0].Name) + default: + assert.Fail("unexpected root snapshot", got.profile.Organization.Name) + } +} + +type organizationProfileBarrierHandler struct { + rootRead chan struct{} + release <-chan struct{} + once sync.Once +} + +func (h *organizationProfileBarrierHandler) Enabled(context.Context, slog.Level) bool { + return true +} + +func (h *organizationProfileBarrierHandler) Handle(_ context.Context, record slog.Record) error { + var kind, statement string + record.Attrs(func(attribute slog.Attr) bool { + switch attribute.Key { + case "kind": + kind = attribute.Value.String() + case "stmt": + statement = attribute.Value.String() + } + return true + }) + if kind == "queryrow" && + strings.Contains(statement, "FROM organizations WHERE id =") { + h.once.Do(func() { + close(h.rootRead) + <-h.release + }) + } + return nil +} + +func (h *organizationProfileBarrierHandler) WithAttrs([]slog.Attr) slog.Handler { + return h +} + +func (h *organizationProfileBarrierHandler) WithGroup(string) slog.Handler { + return h +} + +var _ slog.Handler = (*organizationProfileBarrierHandler)(nil) diff --git a/internal/store/organization_review_regressions_test.go b/internal/store/organization_review_regressions_test.go new file mode 100644 index 000000000..7b20783ac --- /dev/null +++ b/internal/store/organization_review_regressions_test.go @@ -0,0 +1,155 @@ +package store_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestOrganizationProfileUsesStoredCanonicalAddressAndMediaValuesAsKeys(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", + }) + require.NoError(err) + + street := " 1 Example St " + locality := " Exampletown " + uri := " https://example.test/logo.png " + input := store.OrganizationProfileInput{ + Addresses: []store.OrganizationAddressInput{{ + AddressKind: store.PersonAddressPostal, + StreetAddress: &street, + Locality: &locality, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + Media: []store.OrganizationMediaInput{{ + MediaKind: store.PersonMediaLogo, + URI: &uri, + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}, + } + first, err := st.ReplaceOrganizationProfileContext(ctx, organization.ID, organization.Revision, input) + require.NoError(err) + require.Len(first.Addresses, 1) + require.Len(first.Media, 1) + assert.Equal(";;1 Example St;Exampletown;;;", first.Addresses[0].OriginalValue) + assert.Equal("https://example.test/logo.png", first.Media[0].OriginalValue) + require.NotNil(first.Media[0].URI) + assert.Equal("https://example.test/logo.png", *first.Media[0].URI) + + second, err := st.ReplaceOrganizationProfileContext(ctx, organization.ID, first.Organization.Revision, input) + require.NoError(err) + require.Len(second.Addresses, 1) + require.Len(second.Media, 1) + assert.Equal(first.Addresses[0].Envelope.ID, second.Addresses[0].Envelope.ID, + "omitted address original_value must not churn a stable row") + assert.Equal(first.Media[0].Envelope.ID, second.Media[0].Envelope.ID, + "omitted media original_value must not churn a stable row") +} + +func TestOrganizationContactScopePartsAreTrimmedBeforeValidationAndStorage(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{Name: "Example Org"}) + require.NoError(err) + + scopeKind := " workspace " + scopeValue := " T0EXAMPLE " + input := store.OrganizationProfileInput{ContactPoints: []store.OrganizationContactPointInput{{ + AddressKind: store.ContactAddressUsername, + ServiceSlug: new("slack"), + ScopeKind: &scopeKind, + ScopeValue: &scopeValue, + OriginalValue: "alice", + Envelope: store.ValueEnvelopeInput{Source: store.ProvenanceUser}, + }}} + first, err := st.ReplaceOrganizationProfileContext(ctx, organization.ID, organization.Revision, input) + require.NoError(err) + require.Len(first.ContactPoints, 1) + require.NotNil(first.ContactPoints[0].ScopeKind) + require.NotNil(first.ContactPoints[0].ScopeValue) + assert.Equal("workspace", *first.ContactPoints[0].ScopeKind) + assert.Equal("T0EXAMPLE", *first.ContactPoints[0].ScopeValue) + + second, err := st.ReplaceOrganizationProfileContext(ctx, organization.ID, first.Organization.Revision, input) + require.NoError(err) + require.Len(second.ContactPoints, 1) + assert.Equal(first.ContactPoints[0].Envelope.ID, second.ContactPoints[0].Envelope.ID, + "padded scope parts must not create a second identity key") +} + +func TestMergeOrganizationsLocksBothRootsInStableOrder(t *testing.T) { + require := require.New(t) + st := testutil.NewTestStore(t) + if !st.IsPostgreSQL() { + t.Skip("PostgreSQL row locks are required for reciprocal merge deadlock regression") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + first, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{Name: "First Org"}) + require.NoError(err) + second, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{Name: "Second Org"}) + require.NoError(err) + + // Hold the higher ID so the reverse-order merge waits on it first. Then + // start the lower-ID merge, which acquires the lower row and waits on the + // same higher row. This makes the caller-order deadlock deterministic. + blocker, err := st.DB().BeginTx(ctx, nil) + require.NoError(err) + t.Cleanup(func() { _ = blocker.Rollback() }) + var lockedID int64 + require.NoError(blocker.QueryRowContext(ctx, + `SELECT id FROM organizations WHERE id = $1 FOR UPDATE`, second.ID).Scan(&lockedID)) + require.Equal(second.ID, lockedID) + + results := make(chan error, 2) + go func() { + // The reverse-order call waits on the higher row before the lower row. + _, mergeErr := st.MergeOrganizationsContext(ctx, + second.ID, second.Revision, first.ID, first.Revision) + results <- mergeErr + }() + require.Eventually(func() bool { + return postgreSQLWaitingLockCount(t, st) >= 1 + }, 5*time.Second, 10*time.Millisecond, "reverse merge did not reach the held higher row") + go func() { + // The lower-order call owns the lower row and then waits on the higher. + _, mergeErr := st.MergeOrganizationsContext(ctx, + first.ID, first.Revision, second.ID, second.Revision) + results <- mergeErr + }() + require.Eventually(func() bool { + return postgreSQLWaitingLockCount(t, st) >= 2 + }, 5*time.Second, 10*time.Millisecond, "both reciprocal merges did not reach the lock inversion") + require.NoError(blocker.Commit()) + + successes := 0 + for range 2 { + select { + case err := <-results: + switch { + case err == nil: + successes++ + case errors.Is(err, store.ErrOrganizationInvalid), + errors.Is(err, store.ErrOrganizationRevisionConflict): + default: + require.NoError(err, "reciprocal merges must not leak a database deadlock") + } + case <-ctx.Done(): + require.FailNow("reciprocal merges did not finish", ctx.Err()) + } + } + require.Equal(1, successes, "exactly one reciprocal merge should commit") +} diff --git a/internal/store/organization_schema_parity_test.go b/internal/store/organization_schema_parity_test.go new file mode 100644 index 000000000..d7b2409b7 --- /dev/null +++ b/internal/store/organization_schema_parity_test.go @@ -0,0 +1,89 @@ +package store_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestOrganizationChildTablesMirrorPersonChildTables(t *testing.T) { + st := testutil.NewTestStore(t) + pairs := []struct { + person string + organization string + }{ + {person: "person_names", organization: "organization_names"}, + {person: "person_addresses", organization: "organization_addresses"}, + {person: "person_contact_points", organization: "organization_contact_points"}, + {person: "person_media", organization: "organization_media"}, + {person: "person_categories", organization: "organization_categories"}, + { + person: "person_attribute_values", + organization: "organization_attribute_values", + }, + } + for _, pair := range pairs { + t.Run(pair.organization, func(t *testing.T) { + assert := assert.New(t) + personColumns := tableColumns(t, st, pair.person) + organizationColumns := tableColumns(t, st, pair.organization) + assert.Contains(organizationColumns, "organization_id") + assert.NotContains(organizationColumns, "person_id") + for _, column := range personColumns { + if column != "person_id" { + assert.Contains(organizationColumns, column) + } + } + }) + } +} + +func TestPersonsHasNoDenormalizedCompanyOrTitle(t *testing.T) { + assert := assert.New(t) + st := testutil.NewTestStore(t) + columns := tableColumns(t, st, "persons") + for _, forbidden := range []string{ + "company", "company_id", "organization_id", "job_title", "title", "role", "department", + } { + assert.NotContains(columns, forbidden) + } +} + +var organizationProfileEnvelopeColumnNames = []string{ + "pref", "ordinal", "type_label", "type_tokens", + "vcard_property", "vcard_group", "vcard_prop_id", "vcard_pid", "vcard_altid", + "source", "source_ref", "confidence", + "active_from", "active_until", "created_at", "updated_at", "superseded_at", +} + +func TestOrganizationChildTablesCarryTheFullEnvelope(t *testing.T) { + st := testutil.NewTestStore(t) + for _, table := range []string{ + "organization_names", "organization_identifiers", "organization_addresses", + "organization_contact_points", "organization_media", "organization_categories", + } { + t.Run(table, func(t *testing.T) { + assert := assert.New(t) + columns := tableColumns(t, st, table) + for _, column := range organizationProfileEnvelopeColumnNames { + assert.Contains(columns, column) + } + }) + } +} + +func tableColumns(t *testing.T, st *store.Store, table string) []string { + t.Helper() + rows, err := st.DB().QueryContext(context.Background(), + "SELECT * FROM "+table+" WHERE 1 = 0") + require.NoError(t, err, "describe %s", table) + defer func() { _ = rows.Close() }() + columns, err := rows.Columns() + require.NoError(t, err, "columns of %s", table) + require.NoError(t, rows.Err(), "iterate %s", table) + return columns +} diff --git a/internal/store/organizations.go b/internal/store/organizations.go new file mode 100644 index 000000000..c3fcabaa3 --- /dev/null +++ b/internal/store/organizations.go @@ -0,0 +1,602 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/url" + "slices" + "strings" + "time" +) + +var ( + ErrOrganizationNotFound = errors.New("organization not found") + ErrOrganizationRevisionConflict = errors.New("organization revision conflict") + ErrOrganizationInvalid = errors.New("invalid organization") + ErrOrganizationHasEmployments = errors.New("organization still has employment records") + ErrOrganizationMergeConflict = errors.New("organization merge conflict") +) + +// OrganizationKind classifies an organization for presentation. +type OrganizationKind string + +const ( + OrganizationKindCompany OrganizationKind = "company" + OrganizationKindNonprofit OrganizationKind = "nonprofit" + OrganizationKindSchool OrganizationKind = "school" + OrganizationKindGovernment OrganizationKind = "government" + OrganizationKindHousehold OrganizationKind = "household" + OrganizationKindOther OrganizationKind = "other" +) + +// AllOrganizationKinds is the ordered organization-kind vocabulary. +var AllOrganizationKinds = []OrganizationKind{ + OrganizationKindCompany, + OrganizationKindNonprofit, + OrganizationKindSchool, + OrganizationKindGovernment, + OrganizationKindHousehold, + OrganizationKindOther, +} + +// MergeOrganizationsContext folds a losing organization into a survivor +// without deleting employment history. +func (s *Store) MergeOrganizationsContext( + ctx context.Context, survivorID, survivorRevision, losingID, losingRevision int64, +) (*Organization, error) { + if survivorID <= 0 || losingID <= 0 { + return nil, fmt.Errorf("%w: organization id must be positive", ErrOrganizationInvalid) + } + if survivorID == losingID { + return nil, fmt.Errorf("%w: cannot merge an organization into itself", + ErrOrganizationInvalid) + } + + var survivor *Organization + err := s.withTxContext(ctx, func(tx *loggedTx) error { + firstID, secondID := survivorID, losingID + if firstID > secondID { + firstID, secondID = secondID, firstID + } + first, err := getOrganizationForUpdateTx(ctx, tx, s.dialect, firstID) + if err != nil { + return err + } + second, err := getOrganizationForUpdateTx(ctx, tx, s.dialect, secondID) + if err != nil { + return err + } + lockedSurvivor, lockedLosing := first, second + if survivorID != firstID { + lockedSurvivor, lockedLosing = second, first + } + if lockedSurvivor.Revision != survivorRevision || + lockedLosing.Revision != losingRevision { + return ErrOrganizationRevisionConflict + } + if lockedSurvivor.MergedIntoID != nil { + return fmt.Errorf("%w: cannot merge into an already merged organization", + ErrOrganizationInvalid) + } + if lockedLosing.MergedIntoID != nil { + return fmt.Errorf("%w: cannot re-merge an already merged organization", + ErrOrganizationInvalid) + } + + var collisions int64 + collisionQuery := `SELECT COUNT(*) + FROM employments losing_row + WHERE losing_row.organization_id = ? AND ` + + s.dialect.BoolTrueExpr("losing_row.is_current") + ` + AND EXISTS ( + SELECT 1 FROM employments survivor_row + WHERE survivor_row.organization_id = ? + AND survivor_row.person_id = losing_row.person_id + AND survivor_row.title_normalized = losing_row.title_normalized + AND ` + s.dialect.BoolTrueExpr("survivor_row.is_current") + ` + )` + if err := tx.QueryRowContext(ctx, collisionQuery, losingID, survivorID). + Scan(&collisions); err != nil { + return fmt.Errorf("check organization merge employment collisions: %w", err) + } + if collisions > 0 { + return fmt.Errorf("%w: person already has a current employment with this "+ + "organization and title", ErrOrganizationMergeConflict) + } + + if _, err := tx.ExecContext(ctx, fmt.Sprintf(` + UPDATE employments + SET organization_id = ?, address_id = NULL, revision = revision + 1, + updated_at = %s + WHERE organization_id = ? + `, s.dialect.Now()), survivorID, losingID); err != nil { + return fmt.Errorf("repoint organization employments: %w", err) + } + sourceRef := fmt.Sprintf("organization-merge:%d", losingID) + if _, err := tx.ExecContext(ctx, s.dialect.InsertOrIgnore(` + INSERT OR IGNORE INTO organization_names ( + organization_id, name_kind, formatted, original_value, + name_normalized, source, source_ref + ) VALUES (?, 'former', ?, ?, ?, 'system', ?) + `), survivorID, lockedLosing.Name, + lockedLosing.Name, NormalizeOrganizationName(lockedLosing.Name), + sourceRef); err != nil { + return fmt.Errorf("retain merged organization name: %w", err) + } + if _, err := tx.ExecContext(ctx, fmt.Sprintf(` + UPDATE organizations + SET merged_into_id = ?, retired_at = %s, revision = revision + 1, + updated_at = %s + WHERE id = ? AND revision = ? + `, s.dialect.Now(), s.dialect.Now()), + survivorID, losingID, losingRevision); err != nil { + return fmt.Errorf("mark losing organization merged: %w", err) + } + if _, err := tx.ExecContext(ctx, fmt.Sprintf(` + UPDATE organizations + SET revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? + `, s.dialect.Now()), survivorID, survivorRevision); err != nil { + return fmt.Errorf("bump surviving organization revision: %w", err) + } + survivor, err = getOrganizationTx(ctx, tx, survivorID) + return err + }) + if err != nil { + return nil, err + } + return survivor, nil +} + +// Organization is a durable curated real-world entity. +type Organization struct { + ID int64 `json:"id"` + Name string `json:"name"` + Kind OrganizationKind `json:"kind"` + PrimaryDomain *string `json:"primary_domain,omitempty"` + Description *string `json:"description,omitempty"` + Revision int64 `json:"revision"` + MergedIntoID *int64 `json:"merged_into_id,omitempty"` + RetiredAt *time.Time `json:"retired_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// OrganizationInput is the full mutable organization field set. +type OrganizationInput struct { + Name string + Kind OrganizationKind + PrimaryDomain *string + Description *string +} + +// OrganizationFilter bounds an organization listing. +type OrganizationFilter struct { + Query string + IncludeRetired bool + Limit int + Offset int +} + +const ( + DefaultOrganizationPageSize = 100 + MaxOrganizationPageSize = 500 +) + +const organizationColumns = ` + id, name, name_normalized, kind, primary_domain, description, + revision, merged_into_id, retired_at, created_at, updated_at +` + +// CreateOrganizationContext creates a revisioned organization root. +func (s *Store) CreateOrganizationContext( + ctx context.Context, input OrganizationInput, +) (*Organization, error) { + input, err := validateOrganizationInput(input) + if err != nil { + return nil, err + } + row := s.db.QueryRowContext(ctx, ` + INSERT INTO organizations ( + name, name_normalized, kind, primary_domain, description + ) VALUES (?, ?, ?, ?, ?) + RETURNING `+organizationColumns, + input.Name, NormalizeOrganizationName(input.Name), input.Kind, + input.PrimaryDomain, input.Description) + organization, err := scanOrganization(row) + if err != nil { + return nil, fmt.Errorf("create organization: %w", err) + } + return organization, nil +} + +// GetOrganizationContext returns one organization, including retired roots. +func (s *Store) GetOrganizationContext(ctx context.Context, id int64) (*Organization, error) { + organization, err := scanOrganization(s.db.QueryRowContext(ctx, + `SELECT `+organizationColumns+` FROM organizations WHERE id = ?`, id)) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrOrganizationNotFound + } + if err != nil { + return nil, fmt.Errorf("get organization %d: %w", id, err) + } + return organization, nil +} + +// ListOrganizationsContext returns a bounded, stable organization page. +func (s *Store) ListOrganizationsContext( + ctx context.Context, filter OrganizationFilter, +) ([]Organization, error) { + where, args := organizationWhere(filter) + limit, offset := organizationPage(filter) + args = append(args, limit, offset) + rows, err := s.db.QueryContext(ctx, `SELECT `+organizationColumns+ + ` FROM organizations`+where+` ORDER BY name_normalized, id LIMIT ? OFFSET ?`, args...) + if err != nil { + return nil, fmt.Errorf("list organizations: %w", err) + } + defer func() { _ = rows.Close() }() + + organizations := make([]Organization, 0) + for rows.Next() { + organization, err := scanOrganization(rows) + if err != nil { + return nil, fmt.Errorf("scan organization: %w", err) + } + organizations = append(organizations, *organization) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("list organizations: %w", err) + } + return organizations, nil +} + +// CountOrganizationsContext counts the same filtered set as the list method. +func (s *Store) CountOrganizationsContext( + ctx context.Context, filter OrganizationFilter, +) (int64, error) { + where, args := organizationWhere(filter) + var count int64 + if err := s.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM organizations`+where, args...).Scan(&count); err != nil { + return 0, fmt.Errorf("count organizations: %w", err) + } + return count, nil +} + +// UpdateOrganizationContext replaces mutable fields under optimistic revision. +func (s *Store) UpdateOrganizationContext( + ctx context.Context, id, expectedRevision int64, input OrganizationInput, +) (*Organization, error) { + input, err := validateOrganizationInput(input) + if err != nil { + return nil, err + } + return s.mutateOrganizationContext(ctx, id, expectedRevision, fmt.Sprintf(` + UPDATE organizations + SET name = ?, name_normalized = ?, kind = ?, primary_domain = ?, + description = ?, revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? AND merged_into_id IS NULL + RETURNING %s + `, s.dialect.Now(), organizationColumns), + input.Name, NormalizeOrganizationName(input.Name), input.Kind, + input.PrimaryDomain, input.Description, id, expectedRevision) +} + +// ReplaceOrganizationContext atomically replaces root fields and lifecycle state. +func (s *Store) ReplaceOrganizationContext( + ctx context.Context, id, expectedRevision int64, input OrganizationInput, retired bool, +) (*Organization, error) { + input, err := validateOrganizationInput(input) + if err != nil { + return nil, err + } + var organization *Organization + err = s.withTxContext(ctx, func(tx *loggedTx) error { + organization, err = scanOrganization(tx.QueryRowContext(ctx, fmt.Sprintf(` + UPDATE organizations + SET name = ?, name_normalized = ?, kind = ?, primary_domain = ?, + description = ?, + retired_at = CASE WHEN ? THEN COALESCE(retired_at, %s) ELSE NULL END, + revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? AND merged_into_id IS NULL + RETURNING %s + `, s.dialect.Now(), s.dialect.Now(), organizationColumns), + input.Name, NormalizeOrganizationName(input.Name), input.Kind, + input.PrimaryDomain, input.Description, retired, id, expectedRevision)) + if errors.Is(err, sql.ErrNoRows) { + return organizationMutableCASMissTx(ctx, tx, id, expectedRevision) + } + if err != nil { + return fmt.Errorf("replace organization %d: %w", id, err) + } + return nil + }) + if err != nil { + return nil, err + } + return organization, nil +} + +// RetireOrganizationContext soft-retires an organization under CAS. +func (s *Store) RetireOrganizationContext( + ctx context.Context, id, expectedRevision int64, +) (*Organization, error) { + return s.mutateOrganizationContext(ctx, id, expectedRevision, fmt.Sprintf(` + UPDATE organizations + SET retired_at = %s, revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? AND merged_into_id IS NULL + RETURNING %s + `, s.dialect.Now(), s.dialect.Now(), organizationColumns), id, expectedRevision) +} + +// UnretireOrganizationContext restores a non-merged organization under CAS. +func (s *Store) UnretireOrganizationContext( + ctx context.Context, id, expectedRevision int64, +) (*Organization, error) { + var organization *Organization + err := s.withTxContext(ctx, func(tx *loggedTx) error { + current, err := getOrganizationTx(ctx, tx, id) + if err != nil { + return err + } + if current.MergedIntoID != nil { + return fmt.Errorf("%w: cannot unretire a merged organization", + ErrOrganizationInvalid) + } + organization, err = scanOrganization(tx.QueryRowContext(ctx, fmt.Sprintf(` + UPDATE organizations + SET retired_at = NULL, revision = revision + 1, updated_at = %s + WHERE id = ? AND revision = ? + RETURNING %s + `, s.dialect.Now(), organizationColumns), id, expectedRevision)) + if errors.Is(err, sql.ErrNoRows) { + return organizationCASMissTx(ctx, tx, id) + } + return err + }) + if err != nil { + return nil, err + } + return organization, nil +} + +// DeleteOrganizationContext hard-deletes an unreferenced organization. +func (s *Store) DeleteOrganizationContext( + ctx context.Context, id, expectedRevision int64, +) error { + return s.withTxContext(ctx, func(tx *loggedTx) error { + current, err := getOrganizationForUpdateTx(ctx, tx, s.dialect, id) + if err != nil { + return err + } + if current.Revision != expectedRevision { + return ErrOrganizationRevisionConflict + } + if current.MergedIntoID != nil { + return fmt.Errorf("%w: cannot delete a merged organization redirect", + ErrOrganizationInvalid) + } + var employments int64 + err = tx.QueryRowContext(ctx, + `SELECT COUNT(*) FROM employments WHERE organization_id = ?`, id).Scan(&employments) + if err != nil { + return fmt.Errorf("count organization %d employments: %w", id, err) + } + if employments > 0 { + return ErrOrganizationHasEmployments + } + var deletedID int64 + err = tx.QueryRowContext(ctx, + `DELETE FROM organizations WHERE id = ? AND revision = ? RETURNING id`, + id, expectedRevision).Scan(&deletedID) + if errors.Is(err, sql.ErrNoRows) { + return organizationCASMissTx(ctx, tx, id) + } + if err != nil { + return fmt.Errorf("delete organization %d: %w", id, err) + } + return nil + }) +} + +func (s *Store) mutateOrganizationContext( + ctx context.Context, id, expectedRevision int64, query string, args ...any, +) (*Organization, error) { + var organization *Organization + err := s.withTxContext(ctx, func(tx *loggedTx) error { + var err error + organization, err = scanOrganization(tx.QueryRowContext(ctx, query, args...)) + if errors.Is(err, sql.ErrNoRows) { + return organizationMutableCASMissTx(ctx, tx, id, expectedRevision) + } + if err != nil { + return fmt.Errorf("update organization %d: %w", id, err) + } + return nil + }) + if err != nil { + return nil, err + } + return organization, nil +} + +func organizationCASMissTx(ctx context.Context, tx *loggedTx, id int64) error { + var revision int64 + err := tx.QueryRowContext(ctx, `SELECT revision FROM organizations WHERE id = ?`, id). + Scan(&revision) + if errors.Is(err, sql.ErrNoRows) { + return ErrOrganizationNotFound + } + if err != nil { + return fmt.Errorf("check organization %d after revision miss: %w", id, err) + } + return ErrOrganizationRevisionConflict +} + +func organizationMutableCASMissTx( + ctx context.Context, tx *loggedTx, id, expectedRevision int64, +) error { + current, err := getOrganizationTx(ctx, tx, id) + if err != nil { + return err + } + if current.Revision != expectedRevision { + return ErrOrganizationRevisionConflict + } + if current.MergedIntoID != nil { + return fmt.Errorf("%w: cannot mutate a merged organization redirect", + ErrOrganizationInvalid) + } + return ErrOrganizationRevisionConflict +} + +func getOrganizationTx( + ctx context.Context, tx *loggedTx, id int64, +) (*Organization, error) { + organization, err := scanOrganization(tx.QueryRowContext(ctx, + `SELECT `+organizationColumns+` FROM organizations WHERE id = ?`, id)) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrOrganizationNotFound + } + if err != nil { + return nil, fmt.Errorf("get organization %d: %w", id, err) + } + return organization, nil +} + +func getOrganizationForUpdateTx( + ctx context.Context, tx *loggedTx, dialect Dialect, id int64, +) (*Organization, error) { + organization, err := scanOrganization(tx.QueryRowContext(ctx, + `SELECT `+organizationColumns+` FROM organizations WHERE id = ?`+ + dialect.SelectForUpdate(), id)) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrOrganizationNotFound + } + if err != nil { + return nil, fmt.Errorf("lock organization %d: %w", id, err) + } + return organization, nil +} + +func scanOrganization(row scanner) (*Organization, error) { + var ( + organization Organization + nameNormalized string + primaryDomain, description sql.NullString + mergedIntoID sql.NullInt64 + retiredAt sql.NullTime + ) + if err := row.Scan( + &organization.ID, &organization.Name, &nameNormalized, &organization.Kind, + &primaryDomain, &description, &organization.Revision, &mergedIntoID, + &retiredAt, &organization.CreatedAt, &organization.UpdatedAt, + ); err != nil { + return nil, err + } + organization.PrimaryDomain = nullStringPtr(primaryDomain) + organization.Description = nullStringPtr(description) + if mergedIntoID.Valid { + organization.MergedIntoID = new(mergedIntoID.Int64) + } + if retiredAt.Valid { + organization.RetiredAt = new(retiredAt.Time) + } + return &organization, nil +} + +func validateOrganizationInput(input OrganizationInput) (OrganizationInput, error) { + input.Name = strings.TrimSpace(input.Name) + if input.Name == "" { + return OrganizationInput{}, fmt.Errorf("%w: name is required", ErrOrganizationInvalid) + } + if input.Kind == "" { + input.Kind = OrganizationKindOther + } + if !slices.Contains(AllOrganizationKinds, input.Kind) { + return OrganizationInput{}, fmt.Errorf("%w: unknown kind %q", + ErrOrganizationInvalid, input.Kind) + } + if input.PrimaryDomain != nil { + domain := NormalizeDomain(*input.PrimaryDomain) + if domain == "" { + input.PrimaryDomain = nil + } else { + input.PrimaryDomain = &domain + } + } + if input.Description != nil { + description := strings.TrimSpace(*input.Description) + if description == "" { + input.Description = nil + } else { + input.Description = &description + } + } + return input, nil +} + +func organizationWhere(filter OrganizationFilter) (string, []any) { + conditions := make([]string, 0, 2) + args := make([]any, 0, 1) + if !filter.IncludeRetired { + conditions = append(conditions, "retired_at IS NULL AND merged_into_id IS NULL") + } + if query := NormalizeOrganizationName(filter.Query); query != "" { + query = strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(query) + conditions = append(conditions, `name_normalized LIKE ? ESCAPE '\'`) + args = append(args, query+"%") + } + if len(conditions) == 0 { + return "", args + } + return " WHERE " + strings.Join(conditions, " AND "), args +} + +func organizationPage(filter OrganizationFilter) (int, int) { + limit := filter.Limit + if limit <= 0 { + limit = DefaultOrganizationPageSize + } else if limit > MaxOrganizationPageSize { + limit = MaxOrganizationPageSize + } + return limit, max(filter.Offset, 0) +} + +// NormalizeOrganizationName returns a case-folded whitespace-normalized key. +func NormalizeOrganizationName(raw string) string { + return strings.ToLower(strings.Join(strings.Fields(raw), " ")) +} + +// NormalizeDomain reduces a domain, URL, or email to a bare lowercase host. +func NormalizeDomain(raw string) string { + candidate := strings.ToLower(strings.TrimSpace(raw)) + if at := strings.LastIndex(candidate, "@"); at >= 0 { + candidate = candidate[at+1:] + } + var host string + if strings.Contains(candidate, "://") { + parsed, err := url.Parse(candidate) + if err != nil { + return "" + } + host = parsed.Hostname() + } else { + if strings.Contains(candidate, "/") { + return "" + } + parsed, err := url.Parse("//" + candidate) + if err != nil { + return "" + } + host = parsed.Hostname() + } + host = strings.TrimSuffix(strings.TrimPrefix(host, "www."), ".") + if !strings.Contains(host, ".") || strings.ContainsAny(host, "/ \t\r\n") { + return "" + } + return host +} diff --git a/internal/store/organizations_test.go b/internal/store/organizations_test.go new file mode 100644 index 000000000..052f29f8e --- /dev/null +++ b/internal/store/organizations_test.go @@ -0,0 +1,362 @@ +package store_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" +) + +func TestCreateGetAndListOrganizations(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + description := "Synthetic fixture organization." + domain := "Example.COM" + created, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: " Example Org ", Kind: store.OrganizationKindCompany, + PrimaryDomain: &domain, Description: &description, + }) + require.NoError(err) + assert.Positive(created.ID) + assert.Equal("Example Org", created.Name) + assert.Equal(store.OrganizationKindCompany, created.Kind) + require.NotNil(created.PrimaryDomain) + assert.Equal("example.com", *created.PrimaryDomain) + assert.Equal(int64(1), created.Revision) + assert.Nil(created.RetiredAt) + assert.Nil(created.MergedIntoID) + + got, err := st.GetOrganizationContext(ctx, created.ID) + require.NoError(err) + assert.Equal(created.ID, got.ID) + assert.Equal(created.Revision, got.Revision) + + second, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Another Org", Kind: store.OrganizationKindNonprofit, + }) + require.NoError(err) + + listed, err := st.ListOrganizationsContext(ctx, store.OrganizationFilter{Limit: 10}) + require.NoError(err) + require.Len(listed, 2) + assert.Equal(second.ID, listed[0].ID) + assert.Equal(created.ID, listed[1].ID) + + total, err := st.CountOrganizationsContext(ctx, store.OrganizationFilter{}) + require.NoError(err) + assert.Equal(int64(2), total) + + filtered, err := st.ListOrganizationsContext(ctx, store.OrganizationFilter{ + Query: "exam", Limit: 10, + }) + require.NoError(err) + require.Len(filtered, 1) + assert.Equal(created.ID, filtered[0].ID) +} + +func TestCreateOrganizationRejectsInvalidInput(t *testing.T) { + ctx := context.Background() + st := testutil.NewTestStore(t) + + tests := []struct { + name string + input store.OrganizationInput + wantErr string + }{ + { + name: "empty name", + input: store.OrganizationInput{ + Name: " ", Kind: store.OrganizationKindCompany, + }, + wantErr: "name is required", + }, + { + name: "unknown kind", + input: store.OrganizationInput{ + Name: "Example Org", Kind: "conglomerate", + }, + wantErr: `unknown kind "conglomerate"`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + _, err := st.CreateOrganizationContext(ctx, test.input) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationInvalid) + assert.ErrorContains(err, test.wantErr) + }) + } +} + +func TestCreateOrganizationDefaultsKindToOther(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + st := testutil.NewTestStore(t) + + created, err := st.CreateOrganizationContext( + context.Background(), store.OrganizationInput{Name: "Example Org"}) + require.NoError(err) + assert.Equal(store.OrganizationKindOther, created.Kind) +} + +func TestUpdateOrganizationBumpsRevisionAndRejectsStaleWrites(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + created, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + + updated, err := st.UpdateOrganizationContext(ctx, created.ID, created.Revision, + store.OrganizationInput{Name: "Example Group", Kind: store.OrganizationKindCompany}) + require.NoError(err) + assert.Equal("Example Group", updated.Name) + assert.Equal(created.Revision+1, updated.Revision) + + _, err = st.UpdateOrganizationContext(ctx, created.ID, created.Revision, + store.OrganizationInput{Name: "Stale Write", Kind: store.OrganizationKindCompany}) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationRevisionConflict) + + _, err = st.UpdateOrganizationContext(ctx, created.ID+9999, 1, + store.OrganizationInput{Name: "Missing", Kind: store.OrganizationKindCompany}) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationNotFound) +} + +func TestRetireAndUnretireOrganizationHidesItFromDefaultListing(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + created, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + + retired, err := st.RetireOrganizationContext(ctx, created.ID, created.Revision) + require.NoError(err) + require.NotNil(retired.RetiredAt) + assert.Equal(created.Revision+1, retired.Revision) + + listed, err := st.ListOrganizationsContext(ctx, store.OrganizationFilter{Limit: 10}) + require.NoError(err) + assert.Empty(listed) + + withRetired, err := st.ListOrganizationsContext(ctx, store.OrganizationFilter{ + IncludeRetired: true, Limit: 10, + }) + require.NoError(err) + require.Len(withRetired, 1) + assert.Equal(created.ID, withRetired[0].ID) + + revived, err := st.UnretireOrganizationContext(ctx, created.ID, retired.Revision) + require.NoError(err) + assert.Nil(revived.RetiredAt) + assert.Equal(retired.Revision+1, revived.Revision) +} + +func TestDeleteOrganizationSucceedsOnlyWithoutEmploymentHistory(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + created, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + require.NoError(st.DeleteOrganizationContext(ctx, created.ID, created.Revision)) + + _, err = st.GetOrganizationContext(ctx, created.ID) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationNotFound) + + err = st.DeleteOrganizationContext(ctx, created.ID, created.Revision) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationNotFound) +} + +func TestNormalizeOrganizationNameAndDomain(t *testing.T) { + assert := assert.New(t) + nameTests := []struct{ raw, want string }{ + {raw: "Example Org", want: "example org"}, + {raw: " Example Org ", want: "example org"}, + {raw: "EXAMPLE\tORG", want: "example org"}, + {raw: "", want: ""}, + } + for _, test := range nameTests { + assert.Equal(test.want, store.NormalizeOrganizationName(test.raw), "raw %q", test.raw) + } + + domainTests := []struct{ raw, want string }{ + {raw: "Example.COM", want: "example.com"}, + {raw: " WWW.Example.com ", want: "example.com"}, + {raw: "https://example.com/careers", want: "example.com"}, + {raw: "user@example.com", want: "example.com"}, + {raw: "", want: ""}, + } + for _, test := range domainTests { + assert.Equal(test.want, store.NormalizeDomain(test.raw), "raw %q", test.raw) + } +} + +func TestMergeOrganizationsRejectsSelfWithoutChangingTheRoot(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + organization, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + + _, err = st.MergeOrganizationsContext(ctx, + organization.ID, organization.Revision, organization.ID, organization.Revision) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationInvalid) + require.ErrorContains(err, "cannot merge an organization into itself") + + unchanged, err := st.GetOrganizationContext(ctx, organization.ID) + require.NoError(err) + assert.Equal(organization.Revision, unchanged.Revision) + assert.Nil(unchanged.MergedIntoID) +} + +func TestMergedOrganizationRedirectCannotBeRemergedOrDeleted(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + firstSurvivor := mustOrganization(t, st, "Example Org") + secondSurvivor := mustOrganization(t, st, "Another Org") + losing := mustOrganization(t, st, "Former Org") + + _, err := st.MergeOrganizationsContext(ctx, + firstSurvivor.ID, firstSurvivor.Revision, losing.ID, losing.Revision) + require.NoError(err) + redirect, err := st.GetOrganizationContext(ctx, losing.ID) + require.NoError(err) + require.NotNil(redirect.MergedIntoID) + + _, err = st.MergeOrganizationsContext(ctx, + secondSurvivor.ID, secondSurvivor.Revision, redirect.ID, redirect.Revision) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationInvalid) + + err = st.DeleteOrganizationContext(ctx, redirect.ID, redirect.Revision) + require.Error(err) + require.ErrorIs(err, store.ErrOrganizationInvalid) + + unchanged, err := st.GetOrganizationContext(ctx, redirect.ID) + require.NoError(err) + assert.Equal(redirect.Revision, unchanged.Revision) + require.NotNil(unchanged.MergedIntoID) + assert.Equal(firstSurvivor.ID, *unchanged.MergedIntoID) +} + +func TestMergedOrganizationRedirectRejectsRootAndProfileMutations(t *testing.T) { + tests := []struct { + name string + mutate func(context.Context, *store.Store, *store.Organization) error + }{ + { + name: "root update", + mutate: func(ctx context.Context, st *store.Store, redirect *store.Organization) error { + _, err := st.UpdateOrganizationContext( + ctx, redirect.ID, redirect.Revision, + store.OrganizationInput{ + Name: "Hidden Rewrite", Kind: store.OrganizationKindCompany, + }) + return err + }, + }, + { + name: "retire", + mutate: func(ctx context.Context, st *store.Store, redirect *store.Organization) error { + _, err := st.RetireOrganizationContext( + ctx, redirect.ID, redirect.Revision) + return err + }, + }, + { + name: "profile replacement", + mutate: func(ctx context.Context, st *store.Store, redirect *store.Organization) error { + _, err := st.ReplaceOrganizationProfileContext( + ctx, redirect.ID, redirect.Revision, + store.OrganizationProfileInput{}) + return err + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + survivor := mustOrganization(t, st, "Immutable Survivor") + losing := mustOrganization(t, st, "Immutable Redirect") + _, err := st.MergeOrganizationsContext(ctx, + survivor.ID, survivor.Revision, losing.ID, losing.Revision) + require.NoError(err) + redirect, err := st.GetOrganizationContext(ctx, losing.ID) + require.NoError(err) + + err = test.mutate(ctx, st, redirect) + require.ErrorIs(err, store.ErrOrganizationInvalid) + + unchanged, err := st.GetOrganizationContext(ctx, redirect.ID) + require.NoError(err) + assert.Equal(redirect.Revision, unchanged.Revision) + assert.Equal(redirect.Name, unchanged.Name) + assert.Equal(redirect.RetiredAt, unchanged.RetiredAt) + }) + } +} + +func TestOrganizationKindVocabularyIsOpenAtTheDatabaseBoundary(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + var organizationID int64 + err := st.DB().QueryRowContext(ctx, st.Rebind(` + INSERT INTO organizations (name, name_normalized, kind) + VALUES (?, ?, ?) + RETURNING id + `), "Open Vocabulary Cooperative", "open vocabulary cooperative", + "cooperative").Scan(&organizationID) + require.NoError(err) + require.Positive(organizationID) +} + +func TestOrganizationNameKindVocabularyIsOpenAtTheDatabaseBoundary(t *testing.T) { + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + organization := mustOrganization(t, st, "Open Vocabulary Names") + + result, err := st.DB().ExecContext(ctx, st.Rebind(` + INSERT INTO organization_names ( + organization_id, name_kind, original_value, name_normalized, source + ) VALUES (?, ?, ?, ?, ?) + `), organization.ID, "localized", "Nom localisé", "nom localisé", + string(store.ProvenanceUser)) + require.NoError(err) + affected, err := result.RowsAffected() + require.NoError(err) + require.Equal(int64(1), affected) +} diff --git a/internal/store/person_attributes.go b/internal/store/person_attributes.go index 4012fc89d..70586fbfb 100644 --- a/internal/store/person_attributes.go +++ b/internal/store/person_attributes.go @@ -135,32 +135,12 @@ func (s *Store) SetPersonAttributeValueContext( if input.Ordinal != nil && *input.Ordinal < 0 { return nil, fmt.Errorf("%w: ordinal must not be negative", ErrAttributeValueInvalid) } - return s.retryAttributeWrite("set person attribute value", + return retryAttributeWrite(s, "set person attribute value", func() (*PersonAttributeWrite, error) { return s.setPersonAttributeValueOnce(ctx, input) }) } -// retryAttributeWrite retries read-then-write transactions that can fail -// snapshot upgrade (SQLITE_BUSY) or race the current-value unique index. -func (s *Store) retryAttributeWrite( - operation string, attempt func() (*PersonAttributeWrite, error), -) (*PersonAttributeWrite, error) { - var lastErr error - for range maxAttributeWriteAttempts { - write, err := attempt() - if err == nil { - return write, nil - } - if !s.dialect.IsConflictError(err) && !s.dialect.IsBusyError(err) { - return nil, err - } - lastErr = err - } - return nil, fmt.Errorf("%s: gave up after %d attempts: %w", - operation, maxAttributeWriteAttempts, lastErr) -} - func (s *Store) setPersonAttributeValueOnce( ctx context.Context, input PersonAttributeValueInput, ) (*PersonAttributeWrite, error) { @@ -415,7 +395,7 @@ func (s *Store) SupersedePersonAttributeValueContext( if input.At != nil { at = input.At.UTC() } - return s.retryAttributeWrite("supersede person attribute value", + return retryAttributeWrite(s, "supersede person attribute value", func() (*PersonAttributeWrite, error) { return s.supersedePersonAttributeValueOnce(ctx, at, input) }) diff --git a/internal/store/schema.sql b/internal/store/schema.sql index e7f306cf4..1e3413180 100644 --- a/internal/store/schema.sql +++ b/internal/store/schema.sql @@ -809,6 +809,69 @@ CREATE INDEX IF NOT EXISTS idx_person_attribute_values_record_ref ON person_attribute_values(value_record_type, value_record_id) WHERE value_record_id IS NOT NULL; +-- Organization-owned values mirror person_attribute_values field for field. +-- object_type enforcement remains in the store because neither database can +-- portably constrain a foreign row's object_type. +CREATE TABLE IF NOT EXISTS organization_attribute_values ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + definition_id INTEGER NOT NULL + REFERENCES attribute_definitions(id) ON DELETE RESTRICT, + ordinal INTEGER NOT NULL DEFAULT 0, + value_text TEXT, + value_integer BIGINT, + value_real REAL, + value_boolean BOOLEAN, + value_date TEXT, + value_timestamp DATETIME, + value_json JSON, + value_record_type TEXT, + value_record_id INTEGER, + active_from DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME, + source TEXT NOT NULL, + source_ref TEXT, + confidence REAL, + actor TEXT, + CHECK (ordinal >= 0), + CHECK (source IN ('user', 'carddav_import', 'vcard_import', + 'archive_observation', 'extraction', 'enrichment', + 'system')), + CHECK (active_until IS NULL OR active_until >= active_from), + CHECK (confidence IS NULL OR (confidence >= 0.0 AND confidence <= 1.0)), + CHECK (value_date IS NULL OR value_date LIKE '____-__-__'), + CHECK ( + (CASE WHEN value_text IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_integer IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_real IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_boolean IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_date IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_timestamp IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_json IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_record_id IS NOT NULL THEN 1 ELSE 0 END) = 1 + ), + CHECK ((value_record_id IS NULL) = (value_record_type IS NULL)) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_attribute_values_current + ON organization_attribute_values(organization_id, definition_id, ordinal) + WHERE active_until IS NULL AND superseded_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_organization_attribute_values_history + ON organization_attribute_values( + organization_id, definition_id, active_from DESC, id DESC + ); + +CREATE INDEX IF NOT EXISTS idx_organization_attribute_values_definition + ON organization_attribute_values(definition_id, active_until); + +CREATE INDEX IF NOT EXISTS idx_organization_attribute_values_current_text + ON organization_attribute_values(definition_id, value_text) + WHERE value_text IS NOT NULL + AND active_until IS NULL AND superseded_at IS NULL; + -- ============================================================================ -- PEOPLE PROFILE PRIMITIVES -- ============================================================================ @@ -1254,3 +1317,341 @@ CREATE TABLE IF NOT EXISTS attachment_packs ( stored_bytes BIGINT NOT NULL, created_at TEXT NOT NULL ); + +-- ============================================================================ +-- ORGANIZATIONS & EMPLOYMENT +-- ============================================================================ +-- Durable curated organizations. Name and domain are deliberately not unique: +-- overlap becomes a reviewable duplicate suggestion rather than an automatic +-- merge. Employment history later supplies the hard-delete guard. +CREATE TABLE IF NOT EXISTS organizations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + name_normalized TEXT NOT NULL, + kind TEXT NOT NULL DEFAULT 'other', + primary_domain TEXT, + description TEXT, + revision INTEGER NOT NULL DEFAULT 1, + merged_into_id INTEGER REFERENCES organizations(id) ON DELETE RESTRICT, + retired_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (LENGTH(name) > 0), + CHECK (merged_into_id IS NULL OR merged_into_id <> id) +); +CREATE INDEX IF NOT EXISTS idx_organizations_name_normalized + ON organizations(name_normalized); +CREATE INDEX IF NOT EXISTS idx_organizations_primary_domain + ON organizations(primary_domain) + WHERE primary_domain IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_organizations_merged_into + ON organizations(merged_into_id) + WHERE merged_into_id IS NOT NULL; + +CREATE TABLE IF NOT EXISTS organization_names ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + name_kind TEXT NOT NULL, + formatted TEXT, + family_name TEXT, + given_name TEXT, + additional_names TEXT, + honorific_prefixes TEXT, + honorific_suffixes TEXT, + secondary_surname TEXT, + generation TEXT, + language TEXT, + script TEXT, + phonetic_system TEXT, + phonetic_script TEXT, + sort_as TEXT, + is_derived BOOLEAN NOT NULL DEFAULT FALSE, + original_value TEXT NOT NULL, + name_normalized TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from DATETIME, active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_names_active + ON organization_names(organization_id, name_kind, name_normalized) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_organization_names_lookup + ON organization_names(name_normalized) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_names_property_identity + ON organization_names(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_identifiers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + identifier_kind TEXT NOT NULL, + identifier_value TEXT NOT NULL, + normalized_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from DATETIME, active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_identifiers_active + ON organization_identifiers(organization_id, identifier_kind, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_organization_identifiers_lookup + ON organization_identifiers(identifier_kind, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_identifiers_property_identity + ON organization_identifiers(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_addresses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + address_kind TEXT NOT NULL DEFAULT 'postal', + post_office_box TEXT, extended_address TEXT, street_address TEXT, + locality TEXT, region TEXT, postal_code TEXT, country_name TEXT, + extended_components TEXT, free_text TEXT, label TEXT, geo_uri TEXT, + timezone TEXT, country_code TEXT, place_uri TEXT, + original_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from DATETIME, active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME +); +CREATE INDEX IF NOT EXISTS idx_organization_addresses_current + ON organization_addresses(organization_id, address_kind, pref, ordinal) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_addresses_property_identity + ON organization_addresses(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_contact_points ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + address_kind TEXT NOT NULL, + service_id INTEGER REFERENCES communication_services(id) ON DELETE RESTRICT, + scope_kind TEXT, scope_value TEXT, + original_value TEXT NOT NULL, normalized_value TEXT NOT NULL, + normalization TEXT NOT NULL DEFAULT 'none', + normalization_version INTEGER NOT NULL DEFAULT 1, + uri TEXT, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from DATETIME, active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME +); +CREATE INDEX IF NOT EXISTS idx_organization_contact_points_current_lookup + ON organization_contact_points(address_kind, service_id, scope_kind, scope_value, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_contact_points_property_identity + ON organization_contact_points(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + original_value TEXT NOT NULL, + normalized_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from DATETIME, active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_categories_current_value + ON organization_categories(organization_id, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_media ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + media_kind TEXT NOT NULL, media_type TEXT, uri TEXT, data BLOB, + byte_size BIGINT, content_hash TEXT, original_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from DATETIME, active_until DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at DATETIME +); +CREATE INDEX IF NOT EXISTS idx_organization_media_current + ON organization_media(organization_id, media_kind, pref, ordinal) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_media_property_identity + ON organization_media(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +-- The temporal association between a person and an organization. Mutable +-- employment facts live on this edge so concurrent roles and history remain +-- independently queryable. +CREATE TABLE IF NOT EXISTS employments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + person_id INTEGER NOT NULL REFERENCES persons(id) ON DELETE CASCADE, + organization_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE RESTRICT, + title TEXT, + title_normalized TEXT NOT NULL DEFAULT '', + role TEXT, + department TEXT, + location TEXT, + address_id INTEGER REFERENCES organization_addresses(id) ON DELETE SET NULL, + description TEXT, + start_year INTEGER, + start_month INTEGER, + start_day INTEGER, + end_year INTEGER, + end_month INTEGER, + end_day INTEGER, + is_current INTEGER NOT NULL DEFAULT 1, + is_primary INTEGER NOT NULL DEFAULT 0, + source TEXT NOT NULL DEFAULT 'user' + CHECK (source IN ('user', 'carddav_import', 'vcard_import', + 'archive_observation', 'extraction', 'enrichment', 'system')), + source_ref TEXT, + confidence REAL CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import'))), + revision INTEGER NOT NULL DEFAULT 1, + created_by TEXT NOT NULL DEFAULT 'user', + updated_by TEXT NOT NULL DEFAULT 'user', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (is_current IN (0, 1)), + CHECK (is_primary IN (0, 1)), + CHECK (start_year BETWEEN 1 AND 9999), + CHECK (start_month BETWEEN 1 AND 12), + CHECK (start_day BETWEEN 1 AND 31), + CHECK (end_year BETWEEN 1 AND 9999), + CHECK (end_month BETWEEN 1 AND 12), + CHECK (end_day BETWEEN 1 AND 31), + CHECK (start_day IS NULL OR start_month IS NOT NULL), + CHECK (end_day IS NULL OR end_month IS NOT NULL), + CHECK (start_month IS NULL OR start_year IS NOT NULL), + CHECK (end_month IS NULL OR end_year IS NOT NULL) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_employments_one_primary_current + ON employments(person_id) WHERE is_primary = 1 AND is_current = 1; +CREATE UNIQUE INDEX IF NOT EXISTS idx_employments_active_person_org_title + ON employments(person_id, organization_id, title_normalized) WHERE is_current = 1; +CREATE INDEX IF NOT EXISTS idx_employments_person ON employments(person_id); +CREATE INDEX IF NOT EXISTS idx_employments_organization ON employments(organization_id); +CREATE INDEX IF NOT EXISTS idx_employments_person_current ON employments(person_id) WHERE is_current = 1; +CREATE INDEX IF NOT EXISTS idx_employments_address ON employments(address_id) WHERE address_id IS NOT NULL; + +-- Reviewable duplicate-organization suggestions. Domain and name overlap is +-- evidence, never proof: nothing here merges an organization, and accepting a +-- suggestion only records the decision. The unique index makes a repeated scan +-- idempotent and, because rejected rows are retained, prevents the same +-- low-quality inference from being raised again. +CREATE TABLE IF NOT EXISTS organization_duplicate_suggestions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_a_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + organization_b_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + criterion TEXT NOT NULL, + evidence TEXT NOT NULL, + confidence REAL NOT NULL, + status TEXT NOT NULL DEFAULT 'open', + -- Who resolved it, when, and why. resolved_by is the review-row analogue of + -- roadmap PR 6's reviewed_by; the column keeps the resolved_* prefix because + -- this table's status vocabulary is open/accepted/rejected, which is + -- resolution language rather than review language. All three stay NULL while + -- the suggestion is open, and a resolution never clears them, so a rejected + -- pair keeps its audit trail and cannot be silently re-raised. + resolved_at DATETIME, + resolved_by TEXT, + resolution_note TEXT, + source TEXT NOT NULL DEFAULT 'system', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (organization_a_id < organization_b_id), + CHECK (criterion IN ('domain', 'name')), + CHECK (LENGTH(evidence) > 0), + CHECK (status IN ('open', 'accepted', 'rejected')), + CHECK (confidence >= 0 AND confidence <= 1), + CHECK (source IN ('user', 'carddav_import', 'vcard_import', 'archive_observation', 'extraction', 'enrichment', 'system')) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_duplicate_suggestions_pair + ON organization_duplicate_suggestions( + organization_a_id, organization_b_id, criterion, evidence); +CREATE INDEX IF NOT EXISTS idx_organization_duplicate_suggestions_open + ON organization_duplicate_suggestions(status, id); diff --git a/internal/store/schema_pg.sql b/internal/store/schema_pg.sql index e220b5e81..8e38b3ca7 100644 --- a/internal/store/schema_pg.sql +++ b/internal/store/schema_pg.sql @@ -436,6 +436,324 @@ CREATE TABLE IF NOT EXISTS collection_sources ( CREATE INDEX IF NOT EXISTS idx_collection_sources_source_id ON collection_sources(source_id); +-- ============================================================================ +-- ORGANIZATIONS & EMPLOYMENT +-- ============================================================================ +CREATE TABLE IF NOT EXISTS organizations ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT NOT NULL, + name_normalized TEXT NOT NULL, + kind TEXT NOT NULL DEFAULT 'other', + primary_domain TEXT, + description TEXT, + revision BIGINT NOT NULL DEFAULT 1, + merged_into_id BIGINT REFERENCES organizations(id) ON DELETE RESTRICT, + retired_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (LENGTH(name) > 0), + CHECK (merged_into_id IS NULL OR merged_into_id <> id) +); +CREATE INDEX IF NOT EXISTS idx_organizations_name_normalized + ON organizations(name_normalized); +CREATE INDEX IF NOT EXISTS idx_organizations_primary_domain + ON organizations(primary_domain) + WHERE primary_domain IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_organizations_merged_into + ON organizations(merged_into_id) + WHERE merged_into_id IS NOT NULL; + +CREATE TABLE IF NOT EXISTS organization_names ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + name_kind TEXT NOT NULL, + formatted TEXT, family_name TEXT, given_name TEXT, additional_names TEXT, + honorific_prefixes TEXT, honorific_suffixes TEXT, secondary_surname TEXT, + generation TEXT, language TEXT, script TEXT, phonetic_system TEXT, + phonetic_script TEXT, sort_as TEXT, + is_derived BOOLEAN NOT NULL DEFAULT FALSE, + original_value TEXT NOT NULL, name_normalized TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from TIMESTAMPTZ, active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_names_active + ON organization_names(organization_id, name_kind, name_normalized) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_organization_names_lookup + ON organization_names(name_normalized) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_names_property_identity + ON organization_names(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_identifiers ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + identifier_kind TEXT NOT NULL, identifier_value TEXT NOT NULL, + normalized_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from TIMESTAMPTZ, active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_identifiers_active + ON organization_identifiers(organization_id, identifier_kind, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_organization_identifiers_lookup + ON organization_identifiers(identifier_kind, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_identifiers_property_identity + ON organization_identifiers(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_addresses ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + address_kind TEXT NOT NULL DEFAULT 'postal', + post_office_box TEXT, extended_address TEXT, street_address TEXT, + locality TEXT, region TEXT, postal_code TEXT, country_name TEXT, + extended_components TEXT, free_text TEXT, label TEXT, geo_uri TEXT, + timezone TEXT, country_code TEXT, place_uri TEXT, + original_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from TIMESTAMPTZ, active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_organization_addresses_current + ON organization_addresses(organization_id, address_kind, pref, ordinal) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_addresses_property_identity + ON organization_addresses(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_contact_points ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + address_kind TEXT NOT NULL, + service_id BIGINT REFERENCES communication_services(id) ON DELETE RESTRICT, + scope_kind TEXT, scope_value TEXT, + original_value TEXT NOT NULL, normalized_value TEXT NOT NULL, + normalization TEXT NOT NULL DEFAULT 'none', + normalization_version INTEGER NOT NULL DEFAULT 1, + uri TEXT, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from TIMESTAMPTZ, active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_organization_contact_points_current_lookup + ON organization_contact_points(address_kind, service_id, scope_kind, scope_value, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_contact_points_property_identity + ON organization_contact_points(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_categories ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + original_value TEXT NOT NULL, normalized_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from TIMESTAMPTZ, active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_categories_current_value + ON organization_categories(organization_id, normalized_value) + WHERE active_until IS NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS organization_media ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + media_kind TEXT NOT NULL, media_type TEXT, uri TEXT, data BYTEA, + byte_size BIGINT, content_hash TEXT, original_value TEXT NOT NULL, + pref INTEGER CHECK (pref IS NULL OR pref BETWEEN 1 AND 100), + ordinal INTEGER NOT NULL DEFAULT 0, + type_label TEXT, type_tokens TEXT, + vcard_property TEXT, vcard_group TEXT, vcard_prop_id TEXT, + vcard_pid TEXT, vcard_altid TEXT, + source TEXT NOT NULL CHECK (source IN ( + 'user', 'carddav_import', 'vcard_import', 'archive_observation', + 'extraction', 'enrichment', 'system' + )), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR ( + confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import') + )), + active_from TIMESTAMPTZ, active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_organization_media_current + ON organization_media(organization_id, media_kind, pref, ordinal) + WHERE active_until IS NULL AND superseded_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_media_property_identity + ON organization_media(organization_id, source, source_ref, vcard_property, vcard_prop_id) + WHERE source_ref IS NOT NULL AND vcard_prop_id IS NOT NULL AND superseded_at IS NULL; + +CREATE TABLE IF NOT EXISTS employments ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + person_id BIGINT NOT NULL REFERENCES persons(id) ON DELETE CASCADE, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE RESTRICT, + title TEXT, + title_normalized TEXT NOT NULL DEFAULT '', + role TEXT, + department TEXT, + location TEXT, + address_id BIGINT REFERENCES organization_addresses(id) ON DELETE SET NULL, + description TEXT, + start_year INTEGER, + start_month INTEGER, + start_day INTEGER, + end_year INTEGER, + end_month INTEGER, + end_day INTEGER, + is_current BOOLEAN NOT NULL DEFAULT TRUE, + is_primary BOOLEAN NOT NULL DEFAULT FALSE, + source TEXT NOT NULL DEFAULT 'user' + CHECK (source IN ('user', 'carddav_import', 'vcard_import', + 'archive_observation', 'extraction', 'enrichment', 'system')), + source_ref TEXT, + confidence DOUBLE PRECISION CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 1 + AND source NOT IN ('user', 'carddav_import', 'vcard_import'))), + revision BIGINT NOT NULL DEFAULT 1, + created_by TEXT NOT NULL DEFAULT 'user', + updated_by TEXT NOT NULL DEFAULT 'user', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (start_year BETWEEN 1 AND 9999), + CHECK (start_month BETWEEN 1 AND 12), + CHECK (start_day BETWEEN 1 AND 31), + CHECK (end_year BETWEEN 1 AND 9999), + CHECK (end_month BETWEEN 1 AND 12), + CHECK (end_day BETWEEN 1 AND 31), + CHECK (start_day IS NULL OR start_month IS NOT NULL), + CHECK (end_day IS NULL OR end_month IS NOT NULL), + CHECK (start_month IS NULL OR start_year IS NOT NULL), + CHECK (end_month IS NULL OR end_year IS NOT NULL) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_employments_one_primary_current + ON employments(person_id) WHERE is_primary AND is_current; +CREATE UNIQUE INDEX IF NOT EXISTS idx_employments_active_person_org_title + ON employments(person_id, organization_id, title_normalized) WHERE is_current; +CREATE INDEX IF NOT EXISTS idx_employments_person ON employments(person_id); +CREATE INDEX IF NOT EXISTS idx_employments_organization ON employments(organization_id); +CREATE INDEX IF NOT EXISTS idx_employments_person_current ON employments(person_id) WHERE is_current; +CREATE INDEX IF NOT EXISTS idx_employments_address ON employments(address_id) WHERE address_id IS NOT NULL; + +-- Reviewable duplicate-organization suggestions. Domain and name overlap is +-- evidence, never proof: nothing here merges an organization, and accepting a +-- suggestion only records the decision. The unique index makes a repeated scan +-- idempotent and, because rejected rows are retained, prevents the same +-- low-quality inference from being raised again. +CREATE TABLE IF NOT EXISTS organization_duplicate_suggestions ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_a_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + organization_b_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + criterion TEXT NOT NULL, + evidence TEXT NOT NULL, + confidence DOUBLE PRECISION NOT NULL, + status TEXT NOT NULL DEFAULT 'open', + -- Who resolved it, when, and why. resolved_by is the review-row analogue of + -- roadmap PR 6's reviewed_by; the column keeps the resolved_* prefix because + -- this table's status vocabulary is open/accepted/rejected, which is + -- resolution language rather than review language. All three stay NULL while + -- the suggestion is open, and a resolution never clears them, so a rejected + -- pair keeps its audit trail and cannot be silently re-raised. + resolved_at TIMESTAMPTZ, + resolved_by TEXT, + resolution_note TEXT, + source TEXT NOT NULL DEFAULT 'system', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (organization_a_id < organization_b_id), + CHECK (criterion IN ('domain', 'name')), + CHECK (LENGTH(evidence) > 0), + CHECK (status IN ('open', 'accepted', 'rejected')), + CHECK (confidence >= 0 AND confidence <= 1), + CHECK (source IN ('user', 'carddav_import', 'vcard_import', 'archive_observation', 'extraction', 'enrichment', 'system')) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_duplicate_suggestions_pair + ON organization_duplicate_suggestions( + organization_a_id, organization_b_id, criterion, evidence); +CREATE INDEX IF NOT EXISTS idx_organization_duplicate_suggestions_open + ON organization_duplicate_suggestions(status, id); + -- Daemon-owned analytical Saved Views. Canonical state contains only the -- query/view definition; result rows and transient selection remain client -- state and are never persisted here. @@ -589,6 +907,66 @@ CREATE INDEX IF NOT EXISTS idx_person_attribute_values_record_ref ON person_attribute_values(value_record_type, value_record_id) WHERE value_record_id IS NOT NULL; +CREATE TABLE IF NOT EXISTS organization_attribute_values ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + definition_id BIGINT NOT NULL + REFERENCES attribute_definitions(id) ON DELETE RESTRICT, + ordinal BIGINT NOT NULL DEFAULT 0, + value_text TEXT, + value_integer BIGINT, + value_real DOUBLE PRECISION, + value_boolean BOOLEAN, + value_date TEXT, + value_timestamp TIMESTAMPTZ, + value_json JSONB, + value_record_type TEXT, + value_record_id BIGINT, + active_from TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + active_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + superseded_at TIMESTAMPTZ, + source TEXT NOT NULL, + source_ref TEXT, + confidence DOUBLE PRECISION, + actor TEXT, + CHECK (ordinal >= 0), + CHECK (source IN ('user', 'carddav_import', 'vcard_import', + 'archive_observation', 'extraction', 'enrichment', + 'system')), + CHECK (active_until IS NULL OR active_until >= active_from), + CHECK (confidence IS NULL OR (confidence >= 0.0 AND confidence <= 1.0)), + CHECK (value_date IS NULL OR value_date LIKE '____-__-__'), + CHECK ( + (CASE WHEN value_text IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_integer IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_real IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_boolean IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_date IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_timestamp IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_json IS NOT NULL THEN 1 ELSE 0 END) + + (CASE WHEN value_record_id IS NOT NULL THEN 1 ELSE 0 END) = 1 + ), + CHECK ((value_record_id IS NULL) = (value_record_type IS NULL)) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_attribute_values_current + ON organization_attribute_values(organization_id, definition_id, ordinal) + WHERE active_until IS NULL AND superseded_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_organization_attribute_values_history + ON organization_attribute_values( + organization_id, definition_id, active_from DESC, id DESC + ); + +CREATE INDEX IF NOT EXISTS idx_organization_attribute_values_definition + ON organization_attribute_values(definition_id, active_until); + +CREATE INDEX IF NOT EXISTS idx_organization_attribute_values_current_text + ON organization_attribute_values(definition_id, value_text) + WHERE value_text IS NOT NULL + AND active_until IS NULL AND superseded_at IS NULL; + -- ============================================================================ -- PEOPLE PROFILE PRIMITIVES -- ============================================================================ diff --git a/internal/vcardmap/employment.go b/internal/vcardmap/employment.go new file mode 100644 index 000000000..5060082c6 --- /dev/null +++ b/internal/vcardmap/employment.go @@ -0,0 +1,71 @@ +// Package vcardmap projects native Msgvault records onto vCard property +// values. It deliberately depends on nothing but the standard library and the +// store's projection types: it produces unescaped component values, and the +// vCard codec owns delimiter escaping, folding, and rendering. Roadmap PR 8 +// consumes these functions when it builds vCard resources. +package vcardmap + +import ( + "strings" + + "go.kenn.io/msgvault/internal/store" +) + +// unitSeparators are the characters a user may type to nest organizational +// units inside one department field. +const unitSeparators = "/>" + +// Employment is the subset of a derived employment projection that vCard ORG, +// TITLE, and ROLE need. +type Employment struct { + OrganizationName string + Department string + Title string + Role string +} + +// FromProjection adapts the store's derived primary current employment. A +// zero projection, which is what the store returns when a person has no +// primary current employment, yields a zero Employment and therefore no ORG, +// TITLE, or ROLE value. +func FromProjection(projection store.EmploymentProjection) Employment { + return Employment{ + OrganizationName: projection.OrganizationName, + Department: projection.Department, + Title: projection.Title, + Role: projection.Role, + } +} + +// OrgComponents returns the vCard ORG structured value: the organization name +// followed by its organizational unit names, per RFC 6350 section 6.6.4. +// Returns nil when there is no organization name, because a unit without an +// organization is not a representable ORG value. Component values are +// unescaped. +func OrgComponents(employment Employment) []string { + organization := strings.TrimSpace(employment.OrganizationName) + if organization == "" { + return nil + } + components := []string{organization} + for _, unit := range strings.FieldsFunc(employment.Department, func(r rune) bool { + return strings.ContainsRune(unitSeparators, r) + }) { + if unit = strings.TrimSpace(unit); unit != "" { + components = append(components, unit) + } + } + return components +} + +// Title returns the vCard TITLE value for the primary current employment, or +// "" when it has no title. The value is unescaped. +func Title(employment Employment) string { + return strings.TrimSpace(employment.Title) +} + +// Role returns the vCard ROLE value for the primary current employment, or +// "" when it has no role. The value is unescaped. +func Role(employment Employment) string { + return strings.TrimSpace(employment.Role) +} diff --git a/internal/vcardmap/employment_store_test.go b/internal/vcardmap/employment_store_test.go new file mode 100644 index 000000000..fa59561b4 --- /dev/null +++ b/internal/vcardmap/employment_store_test.go @@ -0,0 +1,85 @@ +package vcardmap_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.kenn.io/msgvault/internal/store" + "go.kenn.io/msgvault/internal/testutil" + "go.kenn.io/msgvault/internal/vcardmap" +) + +// TestStoreProjectionMapsToOrgTitleAndRole locks the PR 5 hook that roadmap +// PR 8 consumes: the derived primary current employment, and only that row, +// supplies vCard ORG, TITLE, and ROLE. +func TestStoreProjectionMapsToOrgTitleAndRole(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + participantID, err := st.EnsureParticipant("alice@example.com", "alice", "example.com") + require.NoError(err) + person, created, err := st.CreatePersonFromParticipant(participantID) + require.NoError(err) + require.True(created) + + dayJob, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Example Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + sideJob, err := st.CreateOrganizationContext(ctx, store.OrganizationInput{ + Name: "Another Org", Kind: store.OrganizationKindCompany, + }) + require.NoError(err) + + title := "Staff Engineer" + role := "Engineering" + department := "Archive Platform" + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: dayJob.ID, + Title: &title, Role: &role, Department: &department, + Source: store.ProvenanceUser, + }) + require.NoError(err) + + advisorTitle := "Advisor" + _, err = st.AddEmploymentContext(ctx, store.EmploymentInput{ + PersonID: person.ID, OrganizationID: sideJob.ID, + Title: &advisorTitle, Source: store.ProvenanceUser, + }) + require.NoError(err) + + projection, found, err := st.PrimaryCurrentEmploymentContext(ctx, person.ID) + require.NoError(err) + require.True(found) + + employment := vcardmap.FromProjection(projection) + assert.Equal([]string{"Example Org", "Archive Platform"}, + vcardmap.OrgComponents(employment)) + assert.Equal("Staff Engineer", vcardmap.Title(employment)) + assert.Equal("Engineering", vcardmap.Role(employment)) +} + +func TestNoPrimaryCurrentEmploymentYieldsNoOrgValue(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + ctx := context.Background() + st := testutil.NewTestStore(t) + + participantID, err := st.EnsureParticipant("bob@example.com", "bob", "example.com") + require.NoError(err) + person, _, err := st.CreatePersonFromParticipant(participantID) + require.NoError(err) + + projection, found, err := st.PrimaryCurrentEmploymentContext(ctx, person.ID) + require.NoError(err) + require.False(found) + + employment := vcardmap.FromProjection(projection) + assert.Nil(vcardmap.OrgComponents(employment)) + assert.Empty(vcardmap.Title(employment)) + assert.Empty(vcardmap.Role(employment)) +} diff --git a/internal/vcardmap/employment_test.go b/internal/vcardmap/employment_test.go new file mode 100644 index 000000000..9adcebdf4 --- /dev/null +++ b/internal/vcardmap/employment_test.go @@ -0,0 +1,84 @@ +package vcardmap + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrgComponentsFollowsRFC6350Ordering(t *testing.T) { + tests := []struct { + name string + employment Employment + want []string + }{ + { + name: "organization only", + employment: Employment{OrganizationName: "Example Org"}, + want: []string{"Example Org"}, + }, + { + name: "organization and one unit", + employment: Employment{ + OrganizationName: "Example Org", + Department: "Archive Platform", + }, + want: []string{"Example Org", "Archive Platform"}, + }, + { + name: "nested units split on the unit separator", + employment: Employment{ + OrganizationName: "Example Org", + Department: "Engineering / Archive Platform", + }, + want: []string{"Example Org", "Engineering", "Archive Platform"}, + }, + { + name: "surrounding whitespace is trimmed per component", + employment: Employment{ + OrganizationName: " Example Org ", + Department: " Archive Platform ", + }, + want: []string{"Example Org", "Archive Platform"}, + }, + { + name: "a blank department contributes no unit", + employment: Employment{ + OrganizationName: "Example Org", + Department: " ", + }, + want: []string{"Example Org"}, + }, + { + name: "no organization name yields no ORG value", + employment: Employment{Department: "Archive Platform"}, + want: nil, + }, + { + name: "empty employment yields no ORG value", + employment: Employment{}, + want: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + assert.Equal(test.want, OrgComponents(test.employment)) + }) + } +} + +func TestTitleAndRoleTrimAndPassThroughUnescaped(t *testing.T) { + assert := assert.New(t) + employment := Employment{ + OrganizationName: "Example Org", + Title: " Staff Engineer ", + Role: " Engineering; Archives ", + } + assert.Equal("Staff Engineer", Title(employment)) + assert.Equal("Engineering; Archives", Role(employment), + "values stay unescaped; the vCard codec owns delimiter escaping") + + assert.Empty(Title(Employment{OrganizationName: "Example Org"})) + assert.Empty(Role(Employment{OrganizationName: "Example Org"})) +} diff --git a/pkg/client/generated/client.go b/pkg/client/generated/client.go index 4a10f6cd4..2036570b2 100644 --- a/pkg/client/generated/client.go +++ b/pkg/client/generated/client.go @@ -291,6 +291,30 @@ type ClientInterface interface { GetDomainTimeline(ctx context.Context, options *GetDomainTimelineRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetDomainTimelineResponse, error) GetDomainTimelineWithResponse(ctx context.Context, options *GetDomainTimelineRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetDomainTimelineResp, error) + // CreateEmployment Create an employment record + CreateEmployment(ctx context.Context, options *CreateEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateEmploymentResponse, error) + CreateEmploymentWithResponse(ctx context.Context, options *CreateEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateEmploymentResp, error) + + // DeleteEmployment Delete an employment record + DeleteEmployment(ctx context.Context, options *DeleteEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*struct{}, error) + DeleteEmploymentWithResponse(ctx context.Context, options *DeleteEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*DeleteEmploymentResp, error) + + // GetEmployment Get an employment record + GetEmployment(ctx context.Context, options *GetEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetEmploymentResponse, error) + GetEmploymentWithResponse(ctx context.Context, options *GetEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetEmploymentResp, error) + + // PatchEmployment Replace an employment record's mutable fields + PatchEmployment(ctx context.Context, options *PatchEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchEmploymentResponse, error) + PatchEmploymentWithResponse(ctx context.Context, options *PatchEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchEmploymentResp, error) + + // EndEmployment End an employment record + EndEmployment(ctx context.Context, options *EndEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*EndEmploymentResponse, error) + EndEmploymentWithResponse(ctx context.Context, options *EndEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*EndEmploymentResp, error) + + // SetPrimaryEmployment Set the primary current employment + SetPrimaryEmployment(ctx context.Context, options *SetPrimaryEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetPrimaryEmploymentResponse, error) + SetPrimaryEmploymentWithResponse(ctx context.Context, options *SetPrimaryEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetPrimaryEmploymentResp, error) + // Explore Explore canonical archive entries Explore(ctx context.Context, options *ExploreRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreResponse, error) ExploreWithResponse(ctx context.Context, options *ExploreRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreResp, error) @@ -391,6 +415,62 @@ type ClientInterface interface { UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkMessageTaskResponse, error) UnlinkMessageTaskWithResponse(ctx context.Context, options *UnlinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkMessageTaskResp, error) + // ListOrganizations List organizations + ListOrganizations(ctx context.Context, options *ListOrganizationsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationsResponse, error) + ListOrganizationsWithResponse(ctx context.Context, options *ListOrganizationsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationsResp, error) + + // CreateOrganization Create an organization + CreateOrganization(ctx context.Context, options *CreateOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrganizationResponse, error) + CreateOrganizationWithResponse(ctx context.Context, options *CreateOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrganizationResp, error) + + // ListOrganizationDuplicateSuggestions List organization duplicate suggestions + ListOrganizationDuplicateSuggestions(ctx context.Context, options *ListOrganizationDuplicateSuggestionsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationDuplicateSuggestionsResponse, error) + ListOrganizationDuplicateSuggestionsWithResponse(ctx context.Context, options *ListOrganizationDuplicateSuggestionsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationDuplicateSuggestionsResp, error) + + // RefreshOrganizationDuplicateSuggestions Refresh organization duplicate suggestions + RefreshOrganizationDuplicateSuggestions(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*RefreshOrganizationDuplicateSuggestionsResponseJSON, error) + RefreshOrganizationDuplicateSuggestionsWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*RefreshOrganizationDuplicateSuggestionsResp, error) + + // ResolveOrganizationDuplicateSuggestion Resolve an organization duplicate suggestion + ResolveOrganizationDuplicateSuggestion(ctx context.Context, options *ResolveOrganizationDuplicateSuggestionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ResolveOrganizationDuplicateSuggestionResponse, error) + ResolveOrganizationDuplicateSuggestionWithResponse(ctx context.Context, options *ResolveOrganizationDuplicateSuggestionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ResolveOrganizationDuplicateSuggestionResp, error) + + // DeleteOrganization Delete an organization without employment records + DeleteOrganization(ctx context.Context, options *DeleteOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*struct{}, error) + DeleteOrganizationWithResponse(ctx context.Context, options *DeleteOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*DeleteOrganizationResp, error) + + // GetOrganization Get an organization + GetOrganization(ctx context.Context, options *GetOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationResponse, error) + GetOrganizationWithResponse(ctx context.Context, options *GetOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationResp, error) + + // PatchOrganization Replace an organization's mutable fields + PatchOrganization(ctx context.Context, options *PatchOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchOrganizationResponse, error) + PatchOrganizationWithResponse(ctx context.Context, options *PatchOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchOrganizationResp, error) + + // ListOrganizationAttributes List organization typed attributes + ListOrganizationAttributes(ctx context.Context, options *ListOrganizationAttributesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationAttributesResponse, error) + ListOrganizationAttributesWithResponse(ctx context.Context, options *ListOrganizationAttributesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationAttributesResp, error) + + // SetOrganizationAttribute Set an organization typed attribute + SetOrganizationAttribute(ctx context.Context, options *SetOrganizationAttributeRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetOrganizationAttributeResponseJSON, error) + SetOrganizationAttributeWithResponse(ctx context.Context, options *SetOrganizationAttributeRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetOrganizationAttributeResp, error) + + // ListOrganizationEmployments List an organization's employment records + ListOrganizationEmployments(ctx context.Context, options *ListOrganizationEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationEmploymentsResponse, error) + ListOrganizationEmploymentsWithResponse(ctx context.Context, options *ListOrganizationEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationEmploymentsResp, error) + + // GetOrganizationHistory Get organization profile history + GetOrganizationHistory(ctx context.Context, options *GetOrganizationHistoryRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationHistoryResponse, error) + GetOrganizationHistoryWithResponse(ctx context.Context, options *GetOrganizationHistoryRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationHistoryResp, error) + + // MergeOrganization Merge another organization into this organization + MergeOrganization(ctx context.Context, options *MergeOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*MergeOrganizationResponse, error) + MergeOrganizationWithResponse(ctx context.Context, options *MergeOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*MergeOrganizationResp, error) + + // PutOrganizationProfile Replace organization profile collections + PutOrganizationProfile(ctx context.Context, options *PutOrganizationProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PutOrganizationProfileResponse, error) + PutOrganizationProfileWithResponse(ctx context.Context, options *PutOrganizationProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PutOrganizationProfileResp, error) + // SearchPeople Search analytical people SearchPeople(ctx context.Context, options *SearchPeopleRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchPeopleResponse, error) SearchPeopleWithResponse(ctx context.Context, options *SearchPeopleRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchPeopleResp, error) @@ -443,6 +523,10 @@ type ClientInterface interface { SetPersonAttribute(ctx context.Context, options *SetPersonAttributeRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetPersonAttributeResponse, error) SetPersonAttributeWithResponse(ctx context.Context, options *SetPersonAttributeRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetPersonAttributeResp, error) + // ListPersonEmployments List a person's employment history + ListPersonEmployments(ctx context.Context, options *ListPersonEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListPersonEmploymentsResponse, error) + ListPersonEmploymentsWithResponse(ctx context.Context, options *ListPersonEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListPersonEmploymentsResp, error) + // GetPersonStructuredProfile Get a person's current structured profile GetPersonStructuredProfile(ctx context.Context, options *GetPersonStructuredProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetPersonStructuredProfileResponse, error) GetPersonStructuredProfileWithResponse(ctx context.Context, options *GetPersonStructuredProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetPersonStructuredProfileResp, error) @@ -4513,11 +4597,11 @@ func (c *Client) GetDomainTimeline(ctx context.Context, options *GetDomainTimeli return responseParser(ctx, resp) } -// Explore Explore canonical archive entries -func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreResponse, error) { +// CreateEmployment Create an employment record +func (c *Client) CreateEmployment(ctx context.Context, options *CreateEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateEmploymentResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments", Method: "POST", Options: options, ContentType: "application/json", @@ -4528,10 +4612,10 @@ func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, re return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ExploreResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*CreateEmploymentResponse, error) { bodyBytes := resp.Content - if resp.StatusCode != 200 { - target := new(ExploreErrorResponse) + if resp.StatusCode != 201 { + target := new(CreateEmploymentErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4539,7 +4623,7 @@ func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, re StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreErrorResponse", + TargetType: "CreateEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4552,7 +4636,7 @@ func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, re return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ExploreResponse) + target := new(CreateEmploymentResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -4562,7 +4646,7 @@ func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, re StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreResponse", + TargetType: "CreateEmploymentResponse", Body: bodyBytes, Err: err, } @@ -4570,21 +4654,20 @@ func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, re return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// ListExploreFiles List bounded chronological attachment facts -func (c *Client) ListExploreFiles(ctx context.Context, options *ListExploreFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListExploreFilesResponse, error) { +// DeleteEmployment Delete an employment record +func (c *Client) DeleteEmployment(ctx context.Context, options *DeleteEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*struct{}, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/files", - Method: "POST", - Options: options, - ContentType: "application/json", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}", + Method: "DELETE", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -4592,10 +4675,10 @@ func (c *Client) ListExploreFiles(ctx context.Context, options *ListExploreFiles return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ListExploreFilesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*struct{}, error) { bodyBytes := resp.Content - if resp.StatusCode != 200 { - target := new(ListExploreFilesErrorResponse) + if resp.StatusCode != 204 { + target := new(DeleteEmploymentErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4603,7 +4686,7 @@ func (c *Client) ListExploreFiles(ctx context.Context, options *ListExploreFiles StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListExploreFilesErrorResponse", + TargetType: "DeleteEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4616,39 +4699,23 @@ func (c *Client) ListExploreFiles(ctx context.Context, options *ListExploreFiles return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ListExploreFilesResponse) - // Handle empty response body gracefully - if len(bodyBytes) == 0 { - return target, nil - } - if err = json.Unmarshal(bodyBytes, target); err != nil { - return nil, &runtime.ResponseDecodeError{ - StatusCode: resp.StatusCode, - ContentType: resp.Headers.Get("Content-Type"), - ContentLength: len(bodyBytes), - TargetType: "ListExploreFilesResponse", - Body: bodyBytes, - Err: err, - } - } - return target, nil + return nil, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/files") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// ExploreGroups Group canonical archive entries -func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreGroupsResponse, error) { +// GetEmployment Get an employment record +func (c *Client) GetEmployment(ctx context.Context, options *GetEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetEmploymentResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/groups", - Method: "POST", - Options: options, - ContentType: "application/json", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}", + Method: "GET", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -4656,10 +4723,10 @@ func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsReques return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ExploreGroupsResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetEmploymentResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(ExploreGroupsErrorResponse) + target := new(GetEmploymentErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4667,7 +4734,7 @@ func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreGroupsErrorResponse", + TargetType: "GetEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4680,7 +4747,7 @@ func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsReques return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ExploreGroupsResponse) + target := new(GetEmploymentResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -4690,7 +4757,7 @@ func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreGroupsResponse", + TargetType: "GetEmploymentResponse", Body: bodyBytes, Err: err, } @@ -4698,19 +4765,19 @@ func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsReques return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/groups") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// CountExploreMatches Count exact lexical matches in visible rows -func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreMatchesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CountExploreMatchesResponse, error) { +// PatchEmployment Replace an employment record's mutable fields +func (c *Client) PatchEmployment(ctx context.Context, options *PatchEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchEmploymentResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/match-counts", - Method: "POST", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}", + Method: "PATCH", Options: options, ContentType: "application/json", } @@ -4720,10 +4787,10 @@ func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreM return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*CountExploreMatchesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*PatchEmploymentResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(CountExploreMatchesErrorResponse) + target := new(PatchEmploymentErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4731,7 +4798,7 @@ func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreM StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CountExploreMatchesErrorResponse", + TargetType: "PatchEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4744,7 +4811,7 @@ func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreM return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(CountExploreMatchesResponse) + target := new(PatchEmploymentResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -4754,7 +4821,7 @@ func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreM StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CountExploreMatchesResponse", + TargetType: "PatchEmploymentResponse", Body: bodyBytes, Err: err, } @@ -4762,18 +4829,18 @@ func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreM return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/match-counts") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// PreflightExploreSelection Preflight a revision-pinned analytical selection -func (c *Client) PreflightExploreSelection(ctx context.Context, options *PreflightExploreSelectionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PreflightExploreSelectionResponse, error) { +// EndEmployment End an employment record +func (c *Client) EndEmployment(ctx context.Context, options *EndEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*EndEmploymentResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/preflight", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}/end", Method: "POST", Options: options, ContentType: "application/json", @@ -4784,10 +4851,10 @@ func (c *Client) PreflightExploreSelection(ctx context.Context, options *Preflig return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*PreflightExploreSelectionResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*EndEmploymentResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(PreflightExploreSelectionErrorResponse) + target := new(EndEmploymentErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4795,7 +4862,7 @@ func (c *Client) PreflightExploreSelection(ctx context.Context, options *Preflig StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "PreflightExploreSelectionErrorResponse", + TargetType: "EndEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4808,7 +4875,7 @@ func (c *Client) PreflightExploreSelection(ctx context.Context, options *Preflig return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(PreflightExploreSelectionResponse) + target := new(EndEmploymentResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -4818,7 +4885,7 @@ func (c *Client) PreflightExploreSelection(ctx context.Context, options *Preflig StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "PreflightExploreSelectionResponse", + TargetType: "EndEmploymentResponse", Body: bodyBytes, Err: err, } @@ -4826,21 +4893,20 @@ func (c *Client) PreflightExploreSelection(ctx context.Context, options *Preflig return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/preflight") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}/end") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GroupFiles Group analytical files -func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GroupFilesResponse, error) { +// SetPrimaryEmployment Set the primary current employment +func (c *Client) SetPrimaryEmployment(ctx context.Context, options *SetPrimaryEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetPrimaryEmploymentResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/groups", - Method: "POST", - Options: options, - ContentType: "application/json", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}/primary", + Method: "POST", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -4848,10 +4914,10 @@ func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptio return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GroupFilesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*SetPrimaryEmploymentResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GroupFilesErrorResponse) + target := new(SetPrimaryEmploymentErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4859,7 +4925,7 @@ func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptio StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GroupFilesErrorResponse", + TargetType: "SetPrimaryEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4872,7 +4938,7 @@ func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptio return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GroupFilesResponse) + target := new(SetPrimaryEmploymentResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -4882,7 +4948,7 @@ func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptio StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GroupFilesResponse", + TargetType: "SetPrimaryEmploymentResponse", Body: bodyBytes, Err: err, } @@ -4890,18 +4956,18 @@ func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptio return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/groups") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}/primary") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// SearchFiles Search analytical files -func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchFilesResponse, error) { +// Explore Explore canonical archive entries +func (c *Client) Explore(ctx context.Context, options *ExploreRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/search", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore", Method: "POST", Options: options, ContentType: "application/json", @@ -4912,10 +4978,10 @@ func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOpt return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*SearchFilesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ExploreResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(SearchFilesErrorResponse) + target := new(ExploreErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4923,7 +4989,7 @@ func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOpt StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchFilesErrorResponse", + TargetType: "ExploreErrorResponse", Body: bodyBytes, Err: err, } @@ -4936,7 +5002,7 @@ func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOpt return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(SearchFilesResponse) + target := new(ExploreResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -4946,7 +5012,7 @@ func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOpt StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchFilesResponse", + TargetType: "ExploreResponse", Body: bodyBytes, Err: err, } @@ -4954,20 +5020,21 @@ func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOpt return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/search") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetFile Get authoritative file metadata -func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileResponse, error) { +// ListExploreFiles List bounded chronological attachment facts +func (c *Client) ListExploreFiles(ctx context.Context, options *ListExploreFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListExploreFilesResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/files", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -4975,10 +5042,10 @@ func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, re return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetFileResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListExploreFilesResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GetFileErrorResponse) + target := new(ListExploreFilesErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -4986,7 +5053,7 @@ func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, re StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileErrorResponse", + TargetType: "ListExploreFilesErrorResponse", Body: bodyBytes, Err: err, } @@ -4999,7 +5066,7 @@ func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, re return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GetFileResponse) + target := new(ListExploreFilesResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5009,7 +5076,7 @@ func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, re StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileResponse", + TargetType: "ListExploreFilesResponse", Body: bodyBytes, Err: err, } @@ -5017,20 +5084,21 @@ func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, re return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/files") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetFileContent Download content for one authoritative file row -func (c *Client) GetFileContent(ctx context.Context, options *GetFileContentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileContentResponse, error) { +// ExploreGroups Group canonical archive entries +func (c *Client) ExploreGroups(ctx context.Context, options *ExploreGroupsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreGroupsResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}/content", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/groups", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5038,10 +5106,10 @@ func (c *Client) GetFileContent(ctx context.Context, options *GetFileContentRequ return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetFileContentResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ExploreGroupsResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GetFileContentErrorResponse) + target := new(ExploreGroupsErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5049,7 +5117,7 @@ func (c *Client) GetFileContent(ctx context.Context, options *GetFileContentRequ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileContentErrorResponse", + TargetType: "ExploreGroupsErrorResponse", Body: bodyBytes, Err: err, } @@ -5062,23 +5130,39 @@ func (c *Client) GetFileContent(ctx context.Context, options *GetFileContentRequ return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - result := GetFileContentResponse(bodyBytes) - return &result, nil + target := new(ExploreGroupsResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ExploreGroupsResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}/content") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/groups") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetHealth Get authenticated health details -func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetHealthResponse, error) { +// CountExploreMatches Count exact lexical matches in visible rows +func (c *Client) CountExploreMatches(ctx context.Context, options *CountExploreMatchesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CountExploreMatchesResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/health", - Method: "GET", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/match-counts", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5086,10 +5170,10 @@ func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEdi return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetHealthResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*CountExploreMatchesResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GetHealthErrorResponse) + target := new(CountExploreMatchesErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5097,7 +5181,7 @@ func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEdi StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetHealthErrorResponse", + TargetType: "CountExploreMatchesErrorResponse", Body: bodyBytes, Err: err, } @@ -5110,7 +5194,7 @@ func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEdi return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GetHealthResponse) + target := new(CountExploreMatchesResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5120,7 +5204,7 @@ func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEdi StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetHealthResponse", + TargetType: "CountExploreMatchesResponse", Body: bodyBytes, Err: err, } @@ -5128,18 +5212,18 @@ func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEdi return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/health") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/match-counts") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// LinkIdentityParticipants Assert two participants are the same person -func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*LinkIdentityParticipantsResponse, error) { +// PreflightExploreSelection Preflight a revision-pinned analytical selection +func (c *Client) PreflightExploreSelection(ctx context.Context, options *PreflightExploreSelectionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PreflightExploreSelectionResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/links", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/preflight", Method: "POST", Options: options, ContentType: "application/json", @@ -5150,10 +5234,10 @@ func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIden return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*LinkIdentityParticipantsResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*PreflightExploreSelectionResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(LinkIdentityParticipantsErrorResponse) + target := new(PreflightExploreSelectionErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5161,7 +5245,7 @@ func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIden StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "LinkIdentityParticipantsErrorResponse", + TargetType: "PreflightExploreSelectionErrorResponse", Body: bodyBytes, Err: err, } @@ -5174,7 +5258,7 @@ func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIden return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(LinkIdentityParticipantsResponse) + target := new(PreflightExploreSelectionResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5184,7 +5268,7 @@ func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIden StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "LinkIdentityParticipantsResponse", + TargetType: "PreflightExploreSelectionResponse", Body: bodyBytes, Err: err, } @@ -5192,18 +5276,18 @@ func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIden return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/links") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/preflight") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// UnlinkIdentityParticipants Remove a link edge between two participants -func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *UnlinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkIdentityParticipantsResponse, error) { +// GroupFiles Group analytical files +func (c *Client) GroupFiles(ctx context.Context, options *GroupFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GroupFilesResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/unlinks", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/groups", Method: "POST", Options: options, ContentType: "application/json", @@ -5214,10 +5298,10 @@ func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *Unlink return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*UnlinkIdentityParticipantsResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*GroupFilesResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(UnlinkIdentityParticipantsErrorResponse) + target := new(GroupFilesErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5225,7 +5309,7 @@ func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *Unlink StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "UnlinkIdentityParticipantsErrorResponse", + TargetType: "GroupFilesErrorResponse", Body: bodyBytes, Err: err, } @@ -5238,7 +5322,7 @@ func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *Unlink return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(UnlinkIdentityParticipantsResponse) + target := new(GroupFilesResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5248,7 +5332,7 @@ func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *Unlink StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "UnlinkIdentityParticipantsResponse", + TargetType: "GroupFilesResponse", Body: bodyBytes, Err: err, } @@ -5256,18 +5340,18 @@ func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *Unlink return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/unlinks") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/groups") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// ImportMeeting Import one meeting -func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ImportMeetingResponseJSON, error) { +// SearchFiles Search analytical files +func (c *Client) SearchFiles(ctx context.Context, options *SearchFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchFilesResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/import/meeting", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/search", Method: "POST", Options: options, ContentType: "application/json", @@ -5278,10 +5362,1194 @@ func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingReques return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ImportMeetingResponseJSON, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*SearchFilesResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(SearchFilesErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchFilesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(SearchFilesResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchFilesResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/search") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetFile Get authoritative file metadata +func (c *Client) GetFile(ctx context.Context, options *GetFileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetFileResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetFileErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(GetFileResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetFileContent Download content for one authoritative file row +func (c *Client) GetFileContent(ctx context.Context, options *GetFileContentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileContentResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}/content", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetFileContentResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetFileContentErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileContentErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + result := GetFileContentResponse(bodyBytes) + return &result, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}/content") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetHealth Get authenticated health details +func (c *Client) GetHealth(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetHealthResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/health", + Method: "GET", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetHealthResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetHealthErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetHealthErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(GetHealthResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetHealthResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/health") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// LinkIdentityParticipants Assert two participants are the same person +func (c *Client) LinkIdentityParticipants(ctx context.Context, options *LinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*LinkIdentityParticipantsResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/links", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*LinkIdentityParticipantsResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(LinkIdentityParticipantsErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "LinkIdentityParticipantsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(LinkIdentityParticipantsResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "LinkIdentityParticipantsResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/links") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// UnlinkIdentityParticipants Remove a link edge between two participants +func (c *Client) UnlinkIdentityParticipants(ctx context.Context, options *UnlinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkIdentityParticipantsResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/unlinks", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*UnlinkIdentityParticipantsResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(UnlinkIdentityParticipantsErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "UnlinkIdentityParticipantsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(UnlinkIdentityParticipantsResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "UnlinkIdentityParticipantsResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/unlinks") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// ImportMeeting Import one meeting +func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ImportMeetingResponseJSON, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/import/meeting", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*ImportMeetingResponseJSON, error) { + bodyBytes := resp.Content + if resp.StatusCode != 201 { + target := new(ImportMeetingErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ImportMeetingErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(ImportMeetingResponseJSON) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ImportMeetingResponseJSON", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/import/meeting") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// SearchIntegrationTasks Search tasks in the configured project +func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchIntegrationTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchIntegrationTasksResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/search", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*SearchIntegrationTasksResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(SearchIntegrationTasksErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchIntegrationTasksErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(SearchIntegrationTasksResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchIntegrationTasksResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/search") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetTaskIntegrationStatus Get task integration capability status +func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetTaskIntegrationStatusResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/status", + Method: "GET", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetTaskIntegrationStatusResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetTaskIntegrationStatusErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetTaskIntegrationStatusErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(GetTaskIntegrationStatusResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetTaskIntegrationStatusResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/status") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// TestTaskIntegration Test task integration discovery, authentication, capabilities, and project +func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*TestTaskIntegrationResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/test", + Method: "POST", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*TestTaskIntegrationResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(TestTaskIntegrationErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "TestTaskIntegrationErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(TestTaskIntegrationResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "TestTaskIntegrationResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/test") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// ListMessages List messages +func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessagesResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListMessagesResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(ListMessagesErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListMessagesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(ListMessagesResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListMessagesResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// ListChangedMessages List messages whose content changed since a cursor +func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListChangedMessagesResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/changes", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListChangedMessagesResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(ListChangedMessagesErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(ListChangedMessagesResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/changes") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// FilterMessages List filtered messages +func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*FilterMessagesResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/filter", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*FilterMessagesResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(FilterMessagesErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "FilterMessagesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(FilterMessagesResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "FilterMessagesResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/filter") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetGmailIDsByFilter List Gmail message IDs matching a filter +func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsByFilterRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetGmailIDsByFilterResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/gmail-ids", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetGmailIDsByFilterResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetGmailIDsByFilterErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetGmailIDsByFilterErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(GetGmailIDsByFilterResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetGmailIDsByFilterResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/gmail-ids") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetMessage Get one message +func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetMessageResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetMessageErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(GetMessageResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetMessageInlinePart Get an inline MIME part +func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageInlinePartRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageInlinePartResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/inline", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetMessageInlinePartResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(GetMessageInlinePartErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(GetMessageInlinePartResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/inline") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// ListMessageTasks List tasks linked to an archived email +func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessageTasksResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListMessageTasksResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(ListMessageTasksErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListMessageTasksErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(ListMessageTasksResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListMessageTasksResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// CreateOrLinkMessageTask Create or link a task for an archived email +func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrLinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrLinkMessageTaskResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*CreateOrLinkMessageTaskResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 201 { - target := new(ImportMeetingErrorResponse) + target := new(CreateOrLinkMessageTaskErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CreateOrLinkMessageTaskErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(CreateOrLinkMessageTaskResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CreateOrLinkMessageTaskResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// UnlinkMessageTask Unlink a task from an archived email +func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkMessageTaskResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks/{task_id}", + Method: "DELETE", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*UnlinkMessageTaskResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(UnlinkMessageTaskErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "UnlinkMessageTaskErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(UnlinkMessageTaskResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "UnlinkMessageTaskResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks/{task_id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// ListOrganizations List organizations +func (c *Client) ListOrganizations(ctx context.Context, options *ListOrganizationsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationsResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListOrganizationsResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(ListOrganizationsErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5289,7 +6557,7 @@ func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ImportMeetingErrorResponse", + TargetType: "ListOrganizationsErrorResponse", Body: bodyBytes, Err: err, } @@ -5302,7 +6570,7 @@ func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingReques return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ImportMeetingResponseJSON) + target := new(ListOrganizationsResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5312,7 +6580,7 @@ func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ImportMeetingResponseJSON", + TargetType: "ListOrganizationsResponse", Body: bodyBytes, Err: err, } @@ -5320,20 +6588,21 @@ func (c *Client) ImportMeeting(ctx context.Context, options *ImportMeetingReques return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/import/meeting") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// SearchIntegrationTasks Search tasks in the configured project -func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchIntegrationTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchIntegrationTasksResponse, error) { +// CreateOrganization Create an organization +func (c *Client) CreateOrganization(ctx context.Context, options *CreateOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrganizationResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/search", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5341,10 +6610,10 @@ func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchInte return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*SearchIntegrationTasksResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*CreateOrganizationResponse, error) { bodyBytes := resp.Content - if resp.StatusCode != 200 { - target := new(SearchIntegrationTasksErrorResponse) + if resp.StatusCode != 201 { + target := new(CreateOrganizationErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5352,7 +6621,7 @@ func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchInte StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchIntegrationTasksErrorResponse", + TargetType: "CreateOrganizationErrorResponse", Body: bodyBytes, Err: err, } @@ -5365,7 +6634,7 @@ func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchInte return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(SearchIntegrationTasksResponse) + target := new(CreateOrganizationResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5375,7 +6644,7 @@ func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchInte StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchIntegrationTasksResponse", + TargetType: "CreateOrganizationResponse", Body: bodyBytes, Err: err, } @@ -5383,19 +6652,20 @@ func (c *Client) SearchIntegrationTasks(ctx context.Context, options *SearchInte return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/search") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetTaskIntegrationStatus Get task integration capability status -func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetTaskIntegrationStatusResponse, error) { +// ListOrganizationDuplicateSuggestions List organization duplicate suggestions +func (c *Client) ListOrganizationDuplicateSuggestions(ctx context.Context, options *ListOrganizationDuplicateSuggestionsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationDuplicateSuggestionsResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/status", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/duplicate-suggestions", Method: "GET", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5403,10 +6673,10 @@ func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...run return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetTaskIntegrationStatusResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListOrganizationDuplicateSuggestionsResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GetTaskIntegrationStatusErrorResponse) + target := new(ListOrganizationDuplicateSuggestionsErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5414,7 +6684,7 @@ func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...run StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetTaskIntegrationStatusErrorResponse", + TargetType: "ListOrganizationDuplicateSuggestionsErrorResponse", Body: bodyBytes, Err: err, } @@ -5427,7 +6697,7 @@ func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...run return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GetTaskIntegrationStatusResponse) + target := new(ListOrganizationDuplicateSuggestionsResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5437,7 +6707,7 @@ func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...run StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetTaskIntegrationStatusResponse", + TargetType: "ListOrganizationDuplicateSuggestionsResponse", Body: bodyBytes, Err: err, } @@ -5445,18 +6715,18 @@ func (c *Client) GetTaskIntegrationStatus(ctx context.Context, reqEditors ...run return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/status") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/duplicate-suggestions") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// TestTaskIntegration Test task integration discovery, authentication, capabilities, and project -func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*TestTaskIntegrationResponse, error) { +// RefreshOrganizationDuplicateSuggestions Refresh organization duplicate suggestions +func (c *Client) RefreshOrganizationDuplicateSuggestions(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*RefreshOrganizationDuplicateSuggestionsResponseJSON, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/test", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/duplicate-suggestions/refresh", Method: "POST", } @@ -5465,10 +6735,10 @@ func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime. return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*TestTaskIntegrationResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*RefreshOrganizationDuplicateSuggestionsResponseJSON, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(TestTaskIntegrationErrorResponse) + target := new(RefreshOrganizationDuplicateSuggestionsErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5476,7 +6746,7 @@ func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime. StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "TestTaskIntegrationErrorResponse", + TargetType: "RefreshOrganizationDuplicateSuggestionsErrorResponse", Body: bodyBytes, Err: err, } @@ -5489,7 +6759,7 @@ func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime. return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(TestTaskIntegrationResponse) + target := new(RefreshOrganizationDuplicateSuggestionsResponseJSON) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5499,7 +6769,7 @@ func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime. StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "TestTaskIntegrationResponse", + TargetType: "RefreshOrganizationDuplicateSuggestionsResponseJSON", Body: bodyBytes, Err: err, } @@ -5507,20 +6777,21 @@ func (c *Client) TestTaskIntegration(ctx context.Context, reqEditors ...runtime. return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/test") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/duplicate-suggestions/refresh") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// ListMessages List messages -func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessagesResponse, error) { +// ResolveOrganizationDuplicateSuggestion Resolve an organization duplicate suggestion +func (c *Client) ResolveOrganizationDuplicateSuggestion(ctx context.Context, options *ResolveOrganizationDuplicateSuggestionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ResolveOrganizationDuplicateSuggestionResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/duplicate-suggestions/{id}/resolve", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5528,10 +6799,10 @@ func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestO return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ListMessagesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ResolveOrganizationDuplicateSuggestionResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(ListMessagesErrorResponse) + target := new(ResolveOrganizationDuplicateSuggestionErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5539,7 +6810,7 @@ func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestO StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListMessagesErrorResponse", + TargetType: "ResolveOrganizationDuplicateSuggestionErrorResponse", Body: bodyBytes, Err: err, } @@ -5552,7 +6823,7 @@ func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestO return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ListMessagesResponse) + target := new(ResolveOrganizationDuplicateSuggestionResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5562,7 +6833,7 @@ func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestO StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListMessagesResponse", + TargetType: "ResolveOrganizationDuplicateSuggestionResponse", Body: bodyBytes, Err: err, } @@ -5570,18 +6841,66 @@ func (c *Client) ListMessages(ctx context.Context, options *ListMessagesRequestO return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/duplicate-suggestions/{id}/resolve") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// ListChangedMessages List messages whose content changed since a cursor -func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListChangedMessagesResponse, error) { +// DeleteOrganization Delete an organization without employment records +func (c *Client) DeleteOrganization(ctx context.Context, options *DeleteOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*struct{}, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/changes", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}", + Method: "DELETE", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*struct{}, error) { + bodyBytes := resp.Content + if resp.StatusCode != 204 { + target := new(DeleteOrganizationErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteOrganizationErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + return nil, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + +// GetOrganization Get an organization +func (c *Client) GetOrganization(ctx context.Context, options *GetOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}", Method: "GET", Options: options, } @@ -5591,10 +6910,10 @@ func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMe return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ListChangedMessagesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetOrganizationResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(ListChangedMessagesErrorResponse) + target := new(GetOrganizationErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5602,7 +6921,7 @@ func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMe StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesErrorResponse", + TargetType: "GetOrganizationErrorResponse", Body: bodyBytes, Err: err, } @@ -5615,7 +6934,7 @@ func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMe return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ListChangedMessagesResponse) + target := new(GetOrganizationResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5625,7 +6944,7 @@ func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMe StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesResponse", + TargetType: "GetOrganizationResponse", Body: bodyBytes, Err: err, } @@ -5633,20 +6952,21 @@ func (c *Client) ListChangedMessages(ctx context.Context, options *ListChangedMe return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/changes") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// FilterMessages List filtered messages -func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*FilterMessagesResponse, error) { +// PatchOrganization Replace an organization's mutable fields +func (c *Client) PatchOrganization(ctx context.Context, options *PatchOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchOrganizationResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/filter", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}", + Method: "PATCH", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5654,10 +6974,10 @@ func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequ return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*FilterMessagesResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*PatchOrganizationResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(FilterMessagesErrorResponse) + target := new(PatchOrganizationErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5665,7 +6985,7 @@ func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "FilterMessagesErrorResponse", + TargetType: "PatchOrganizationErrorResponse", Body: bodyBytes, Err: err, } @@ -5678,7 +6998,7 @@ func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequ return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(FilterMessagesResponse) + target := new(PatchOrganizationResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5688,7 +7008,7 @@ func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "FilterMessagesResponse", + TargetType: "PatchOrganizationResponse", Body: bodyBytes, Err: err, } @@ -5696,18 +7016,18 @@ func (c *Client) FilterMessages(ctx context.Context, options *FilterMessagesRequ return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/filter") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetGmailIDsByFilter List Gmail message IDs matching a filter -func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsByFilterRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetGmailIDsByFilterResponse, error) { +// ListOrganizationAttributes List organization typed attributes +func (c *Client) ListOrganizationAttributes(ctx context.Context, options *ListOrganizationAttributesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationAttributesResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/gmail-ids", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/attributes", Method: "GET", Options: options, } @@ -5717,10 +7037,10 @@ func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsBy return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetGmailIDsByFilterResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListOrganizationAttributesResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GetGmailIDsByFilterErrorResponse) + target := new(ListOrganizationAttributesErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5728,7 +7048,7 @@ func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsBy StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetGmailIDsByFilterErrorResponse", + TargetType: "ListOrganizationAttributesErrorResponse", Body: bodyBytes, Err: err, } @@ -5741,7 +7061,7 @@ func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsBy return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GetGmailIDsByFilterResponse) + target := new(ListOrganizationAttributesResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5751,7 +7071,7 @@ func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsBy StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetGmailIDsByFilterResponse", + TargetType: "ListOrganizationAttributesResponse", Body: bodyBytes, Err: err, } @@ -5759,20 +7079,21 @@ func (c *Client) GetGmailIDsByFilter(ctx context.Context, options *GetGmailIDsBy return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/gmail-ids") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/attributes") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetMessage Get one message -func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageResponse, error) { +// SetOrganizationAttribute Set an organization typed attribute +func (c *Client) SetOrganizationAttribute(ctx context.Context, options *SetOrganizationAttributeRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetOrganizationAttributeResponseJSON, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/attributes", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5780,10 +7101,10 @@ func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptio return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetMessageResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*SetOrganizationAttributeResponseJSON, error) { bodyBytes := resp.Content - if resp.StatusCode != 200 { - target := new(GetMessageErrorResponse) + if resp.StatusCode != 201 { + target := new(SetOrganizationAttributeErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5791,7 +7112,7 @@ func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptio StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageErrorResponse", + TargetType: "SetOrganizationAttributeErrorResponse", Body: bodyBytes, Err: err, } @@ -5804,7 +7125,7 @@ func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptio return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GetMessageResponse) + target := new(SetOrganizationAttributeResponseJSON) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5814,7 +7135,7 @@ func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptio StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageResponse", + TargetType: "SetOrganizationAttributeResponseJSON", Body: bodyBytes, Err: err, } @@ -5822,18 +7143,18 @@ func (c *Client) GetMessage(ctx context.Context, options *GetMessageRequestOptio return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/attributes") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// GetMessageInlinePart Get an inline MIME part -func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageInlinePartRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageInlinePartResponse, error) { +// ListOrganizationEmployments List an organization's employment records +func (c *Client) ListOrganizationEmployments(ctx context.Context, options *ListOrganizationEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationEmploymentsResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/inline", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/employments", Method: "GET", Options: options, } @@ -5843,10 +7164,10 @@ func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageIn return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*GetMessageInlinePartResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListOrganizationEmploymentsResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(GetMessageInlinePartErrorResponse) + target := new(ListOrganizationEmploymentsErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5854,7 +7175,7 @@ func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageIn StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponse", + TargetType: "ListOrganizationEmploymentsErrorResponse", Body: bodyBytes, Err: err, } @@ -5867,7 +7188,7 @@ func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageIn return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(GetMessageInlinePartResponse) + target := new(ListOrganizationEmploymentsResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5877,7 +7198,7 @@ func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageIn StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartResponse", + TargetType: "ListOrganizationEmploymentsResponse", Body: bodyBytes, Err: err, } @@ -5885,18 +7206,18 @@ func (c *Client) GetMessageInlinePart(ctx context.Context, options *GetMessageIn return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/inline") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/employments") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// ListMessageTasks List tasks linked to an archived email -func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessageTasksResponse, error) { +// GetOrganizationHistory Get organization profile history +func (c *Client) GetOrganizationHistory(ctx context.Context, options *GetOrganizationHistoryRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationHistoryResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/history", Method: "GET", Options: options, } @@ -5906,10 +7227,10 @@ func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasks return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*ListMessageTasksResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*GetOrganizationHistoryResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(ListMessageTasksErrorResponse) + target := new(GetOrganizationHistoryErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5917,7 +7238,7 @@ func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasks StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListMessageTasksErrorResponse", + TargetType: "GetOrganizationHistoryErrorResponse", Body: bodyBytes, Err: err, } @@ -5930,7 +7251,7 @@ func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasks return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(ListMessageTasksResponse) + target := new(GetOrganizationHistoryResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -5940,7 +7261,7 @@ func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasks StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListMessageTasksResponse", + TargetType: "GetOrganizationHistoryResponse", Body: bodyBytes, Err: err, } @@ -5948,18 +7269,18 @@ func (c *Client) ListMessageTasks(ctx context.Context, options *ListMessageTasks return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/history") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// CreateOrLinkMessageTask Create or link a task for an archived email -func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrLinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrLinkMessageTaskResponse, error) { +// MergeOrganization Merge another organization into this organization +func (c *Client) MergeOrganization(ctx context.Context, options *MergeOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*MergeOrganizationResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/merge", Method: "POST", Options: options, ContentType: "application/json", @@ -5970,10 +7291,10 @@ func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrL return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*CreateOrLinkMessageTaskResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*MergeOrganizationResponse, error) { bodyBytes := resp.Content - if resp.StatusCode != 201 { - target := new(CreateOrLinkMessageTaskErrorResponse) + if resp.StatusCode != 200 { + target := new(MergeOrganizationErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -5981,7 +7302,7 @@ func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrL StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CreateOrLinkMessageTaskErrorResponse", + TargetType: "MergeOrganizationErrorResponse", Body: bodyBytes, Err: err, } @@ -5994,7 +7315,7 @@ func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrL return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(CreateOrLinkMessageTaskResponse) + target := new(MergeOrganizationResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -6004,7 +7325,7 @@ func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrL StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CreateOrLinkMessageTaskResponse", + TargetType: "MergeOrganizationResponse", Body: bodyBytes, Err: err, } @@ -6012,20 +7333,21 @@ func (c *Client) CreateOrLinkMessageTask(ctx context.Context, options *CreateOrL return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/merge") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } return responseParser(ctx, resp) } -// UnlinkMessageTask Unlink a task from an archived email -func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkMessageTaskResponse, error) { +// PutOrganizationProfile Replace organization profile collections +func (c *Client) PutOrganizationProfile(ctx context.Context, options *PutOrganizationProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PutOrganizationProfileResponse, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks/{task_id}", - Method: "DELETE", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/profile", + Method: "PUT", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -6033,10 +7355,10 @@ func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTa return nil, fmt.Errorf("error creating request: %w", err) } - responseParser := func(ctx context.Context, resp *runtime.Response) (*UnlinkMessageTaskResponse, error) { + responseParser := func(ctx context.Context, resp *runtime.Response) (*PutOrganizationProfileResponse, error) { bodyBytes := resp.Content if resp.StatusCode != 200 { - target := new(UnlinkMessageTaskErrorResponse) + target := new(PutOrganizationProfileErrorResponse) // Handle empty error response body gracefully - skip unmarshal if no content if len(bodyBytes) > 0 { if err = json.Unmarshal(bodyBytes, target); err != nil { @@ -6044,7 +7366,7 @@ func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTa StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "UnlinkMessageTaskErrorResponse", + TargetType: "PutOrganizationProfileErrorResponse", Body: bodyBytes, Err: err, } @@ -6057,7 +7379,7 @@ func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTa return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), runtime.WithStatusCode(resp.StatusCode)) } - target := new(UnlinkMessageTaskResponse) + target := new(PutOrganizationProfileResponse) // Handle empty response body gracefully if len(bodyBytes) == 0 { return target, nil @@ -6067,7 +7389,7 @@ func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTa StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "UnlinkMessageTaskResponse", + TargetType: "PutOrganizationProfileResponse", Body: bodyBytes, Err: err, } @@ -6075,7 +7397,7 @@ func (c *Client) UnlinkMessageTask(ctx context.Context, options *UnlinkMessageTa return target, nil } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks/{task_id}") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/profile") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } @@ -6892,6 +8214,69 @@ func (c *Client) SetPersonAttribute(ctx context.Context, options *SetPersonAttri return responseParser(ctx, resp) } +// ListPersonEmployments List a person's employment history +func (c *Client) ListPersonEmployments(ctx context.Context, options *ListPersonEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListPersonEmploymentsResponse, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/persons/{id}/employments", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + responseParser := func(ctx context.Context, resp *runtime.Response) (*ListPersonEmploymentsResponse, error) { + bodyBytes := resp.Content + if resp.StatusCode != 200 { + target := new(ListPersonEmploymentsErrorResponse) + // Handle empty error response body gracefully - skip unmarshal if no content + if len(bodyBytes) > 0 { + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListPersonEmploymentsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + // Return error with (possibly empty) target + if errTarget, ok := any(*target).(error); ok { + return nil, runtime.NewClientAPIError(errTarget, runtime.WithStatusCode(resp.StatusCode)) + } + return nil, runtime.NewClientAPIError(fmt.Errorf("API error (status %d): %v", resp.StatusCode, *target), + runtime.WithStatusCode(resp.StatusCode)) + } + target := new(ListPersonEmploymentsResponse) + // Handle empty response body gracefully + if len(bodyBytes) == 0 { + return target, nil + } + if err = json.Unmarshal(bodyBytes, target); err != nil { + return nil, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListPersonEmploymentsResponse", + Body: bodyBytes, + Err: err, + } + } + return target, nil + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/persons/{id}/employments") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + return responseParser(ctx, resp) +} + // GetPersonStructuredProfile Get a person's current structured profile func (c *Client) GetPersonStructuredProfile(ctx context.Context, options *GetPersonStructuredProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetPersonStructuredProfileResponse, error) { var err error diff --git a/pkg/client/generated/client_options.go b/pkg/client/generated/client_options.go index b84e06bb2..54548fe69 100644 --- a/pkg/client/generated/client_options.go +++ b/pkg/client/generated/client_options.go @@ -2472,14 +2472,14 @@ func (o *GetDomainTimelineRequestOptions) GetHeader() (map[string]string, error) return nil, nil } -// ExploreRequestOptions is the options needed to make a request to Explore. -type ExploreRequestOptions struct { - Body *ExploreBody +// CreateEmploymentRequestOptions is the options needed to make a request to CreateEmployment. +type CreateEmploymentRequestOptions struct { + Body *CreateEmploymentBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ExploreRequestOptions) Validate() error { +func (o *CreateEmploymentRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.Body != nil { @@ -2497,39 +2497,48 @@ func (o *ExploreRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ExploreRequestOptions) GetPathParams() (map[string]any, error) { +func (o *CreateEmploymentRequestOptions) GetPathParams() (map[string]any, error) { return nil, nil } // GetQuery returns the query params as a map. -func (o *ExploreRequestOptions) GetQuery() (map[string]any, error) { +func (o *CreateEmploymentRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ExploreRequestOptions) GetBody() any { +func (o *CreateEmploymentRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *ExploreRequestOptions) GetHeader() (map[string]string, error) { +func (o *CreateEmploymentRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// ListExploreFilesRequestOptions is the options needed to make a request to ListExploreFiles. -type ListExploreFilesRequestOptions struct { - Body *ListExploreFilesBody +// DeleteEmploymentRequestOptions is the options needed to make a request to DeleteEmployment. +type DeleteEmploymentRequestOptions struct { + PathParams *DeleteEmploymentPath + Header *DeleteEmploymentHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ListExploreFilesRequestOptions) Validate() error { +func (o *DeleteEmploymentRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Body != nil { - if v, ok := any(o.Body).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Body", err) + errors = errors.Append("PathParams", err) + } + } + } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) } } } @@ -2541,39 +2550,39 @@ func (o *ListExploreFilesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ListExploreFilesRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *DeleteEmploymentRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *ListExploreFilesRequestOptions) GetQuery() (map[string]any, error) { +func (o *DeleteEmploymentRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ListExploreFilesRequestOptions) GetBody() any { - return o.Body +func (o *DeleteEmploymentRequestOptions) GetBody() any { + return nil } // GetHeader returns the headers as a map. -func (o *ListExploreFilesRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *DeleteEmploymentRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } -// ExploreGroupsRequestOptions is the options needed to make a request to ExploreGroups. -type ExploreGroupsRequestOptions struct { - Body *ExploreGroupsBody +// GetEmploymentRequestOptions is the options needed to make a request to GetEmployment. +type GetEmploymentRequestOptions struct { + PathParams *GetEmploymentPath } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ExploreGroupsRequestOptions) Validate() error { +func (o *GetEmploymentRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Body != nil { - if v, ok := any(o.Body).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Body", err) + errors = errors.Append("PathParams", err) } } } @@ -2585,35 +2594,45 @@ func (o *ExploreGroupsRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ExploreGroupsRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *GetEmploymentRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *ExploreGroupsRequestOptions) GetQuery() (map[string]any, error) { +func (o *GetEmploymentRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ExploreGroupsRequestOptions) GetBody() any { - return o.Body +func (o *GetEmploymentRequestOptions) GetBody() any { + return nil } // GetHeader returns the headers as a map. -func (o *ExploreGroupsRequestOptions) GetHeader() (map[string]string, error) { +func (o *GetEmploymentRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// CountExploreMatchesRequestOptions is the options needed to make a request to CountExploreMatches. -type CountExploreMatchesRequestOptions struct { - Body *CountExploreMatchesBody +// PatchEmploymentRequestOptions is the options needed to make a request to PatchEmployment. +type PatchEmploymentRequestOptions struct { + PathParams *PatchEmploymentPath + Body *PatchEmploymentBody + Header *PatchEmploymentHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *CountExploreMatchesRequestOptions) Validate() error { +func (o *PatchEmploymentRequestOptions) Validate() error { var errors runtime.ValidationErrors + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if o.Body != nil { if v, ok := any(o.Body).(runtime.Validator); ok { if err := v.Validate(); err != nil { @@ -2621,6 +2640,14 @@ func (o *CountExploreMatchesRequestOptions) Validate() error { } } } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) + } + } + } if len(errors) == 0 { return nil } @@ -2629,35 +2656,45 @@ func (o *CountExploreMatchesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *CountExploreMatchesRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *PatchEmploymentRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *CountExploreMatchesRequestOptions) GetQuery() (map[string]any, error) { +func (o *PatchEmploymentRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *CountExploreMatchesRequestOptions) GetBody() any { +func (o *PatchEmploymentRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *CountExploreMatchesRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *PatchEmploymentRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } -// PreflightExploreSelectionRequestOptions is the options needed to make a request to PreflightExploreSelection. -type PreflightExploreSelectionRequestOptions struct { - Body *PreflightExploreSelectionBody +// EndEmploymentRequestOptions is the options needed to make a request to EndEmployment. +type EndEmploymentRequestOptions struct { + PathParams *EndEmploymentPath + Body *EndEmploymentBody + Header *EndEmploymentHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *PreflightExploreSelectionRequestOptions) Validate() error { +func (o *EndEmploymentRequestOptions) Validate() error { var errors runtime.ValidationErrors + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if o.Body != nil { if v, ok := any(o.Body).(runtime.Validator); ok { if err := v.Validate(); err != nil { @@ -2665,6 +2702,14 @@ func (o *PreflightExploreSelectionRequestOptions) Validate() error { } } } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) + } + } + } if len(errors) == 0 { return nil } @@ -2673,39 +2718,48 @@ func (o *PreflightExploreSelectionRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *PreflightExploreSelectionRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *EndEmploymentRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *PreflightExploreSelectionRequestOptions) GetQuery() (map[string]any, error) { +func (o *EndEmploymentRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *PreflightExploreSelectionRequestOptions) GetBody() any { +func (o *EndEmploymentRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *PreflightExploreSelectionRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *EndEmploymentRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } -// GroupFilesRequestOptions is the options needed to make a request to GroupFiles. -type GroupFilesRequestOptions struct { - Body *GroupFilesBody +// SetPrimaryEmploymentRequestOptions is the options needed to make a request to SetPrimaryEmployment. +type SetPrimaryEmploymentRequestOptions struct { + PathParams *SetPrimaryEmploymentPath + Header *SetPrimaryEmploymentHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *GroupFilesRequestOptions) Validate() error { +func (o *SetPrimaryEmploymentRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Body != nil { - if v, ok := any(o.Body).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Body", err) + errors = errors.Append("PathParams", err) + } + } + } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) } } } @@ -2717,33 +2771,33 @@ func (o *GroupFilesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *GroupFilesRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *SetPrimaryEmploymentRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *GroupFilesRequestOptions) GetQuery() (map[string]any, error) { +func (o *SetPrimaryEmploymentRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *GroupFilesRequestOptions) GetBody() any { - return o.Body +func (o *SetPrimaryEmploymentRequestOptions) GetBody() any { + return nil } // GetHeader returns the headers as a map. -func (o *GroupFilesRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *SetPrimaryEmploymentRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } -// SearchFilesRequestOptions is the options needed to make a request to SearchFiles. -type SearchFilesRequestOptions struct { - Body *SearchFilesBody +// ExploreRequestOptions is the options needed to make a request to Explore. +type ExploreRequestOptions struct { + Body *ExploreBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *SearchFilesRequestOptions) Validate() error { +func (o *ExploreRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.Body != nil { @@ -2761,39 +2815,39 @@ func (o *SearchFilesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *SearchFilesRequestOptions) GetPathParams() (map[string]any, error) { +func (o *ExploreRequestOptions) GetPathParams() (map[string]any, error) { return nil, nil } // GetQuery returns the query params as a map. -func (o *SearchFilesRequestOptions) GetQuery() (map[string]any, error) { +func (o *ExploreRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *SearchFilesRequestOptions) GetBody() any { +func (o *ExploreRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *SearchFilesRequestOptions) GetHeader() (map[string]string, error) { +func (o *ExploreRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// GetFileRequestOptions is the options needed to make a request to GetFile. -type GetFileRequestOptions struct { - PathParams *GetFilePath +// ListExploreFilesRequestOptions is the options needed to make a request to ListExploreFiles. +type ListExploreFilesRequestOptions struct { + Body *ListExploreFilesBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *GetFileRequestOptions) Validate() error { +func (o *ListExploreFilesRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.PathParams != nil { - if v, ok := any(o.PathParams).(runtime.Validator); ok { + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("PathParams", err) + errors = errors.Append("Body", err) } } } @@ -2805,39 +2859,39 @@ func (o *GetFileRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *GetFileRequestOptions) GetPathParams() (map[string]any, error) { - return runtime.AsMap[any](o.PathParams) +func (o *ListExploreFilesRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil } // GetQuery returns the query params as a map. -func (o *GetFileRequestOptions) GetQuery() (map[string]any, error) { +func (o *ListExploreFilesRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *GetFileRequestOptions) GetBody() any { - return nil +func (o *ListExploreFilesRequestOptions) GetBody() any { + return o.Body } // GetHeader returns the headers as a map. -func (o *GetFileRequestOptions) GetHeader() (map[string]string, error) { +func (o *ListExploreFilesRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// GetFileContentRequestOptions is the options needed to make a request to GetFileContent. -type GetFileContentRequestOptions struct { - PathParams *GetFileContentPath +// ExploreGroupsRequestOptions is the options needed to make a request to ExploreGroups. +type ExploreGroupsRequestOptions struct { + Body *ExploreGroupsBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *GetFileContentRequestOptions) Validate() error { +func (o *ExploreGroupsRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.PathParams != nil { - if v, ok := any(o.PathParams).(runtime.Validator); ok { + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("PathParams", err) + errors = errors.Append("Body", err) } } } @@ -2849,33 +2903,33 @@ func (o *GetFileContentRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *GetFileContentRequestOptions) GetPathParams() (map[string]any, error) { - return runtime.AsMap[any](o.PathParams) +func (o *ExploreGroupsRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil } // GetQuery returns the query params as a map. -func (o *GetFileContentRequestOptions) GetQuery() (map[string]any, error) { +func (o *ExploreGroupsRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *GetFileContentRequestOptions) GetBody() any { - return nil +func (o *ExploreGroupsRequestOptions) GetBody() any { + return o.Body } // GetHeader returns the headers as a map. -func (o *GetFileContentRequestOptions) GetHeader() (map[string]string, error) { +func (o *ExploreGroupsRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// LinkIdentityParticipantsRequestOptions is the options needed to make a request to LinkIdentityParticipants. -type LinkIdentityParticipantsRequestOptions struct { - Body *LinkIdentityParticipantsBody +// CountExploreMatchesRequestOptions is the options needed to make a request to CountExploreMatches. +type CountExploreMatchesRequestOptions struct { + Body *CountExploreMatchesBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *LinkIdentityParticipantsRequestOptions) Validate() error { +func (o *CountExploreMatchesRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.Body != nil { @@ -2893,33 +2947,33 @@ func (o *LinkIdentityParticipantsRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *LinkIdentityParticipantsRequestOptions) GetPathParams() (map[string]any, error) { +func (o *CountExploreMatchesRequestOptions) GetPathParams() (map[string]any, error) { return nil, nil } // GetQuery returns the query params as a map. -func (o *LinkIdentityParticipantsRequestOptions) GetQuery() (map[string]any, error) { +func (o *CountExploreMatchesRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *LinkIdentityParticipantsRequestOptions) GetBody() any { +func (o *CountExploreMatchesRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *LinkIdentityParticipantsRequestOptions) GetHeader() (map[string]string, error) { +func (o *CountExploreMatchesRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// UnlinkIdentityParticipantsRequestOptions is the options needed to make a request to UnlinkIdentityParticipants. -type UnlinkIdentityParticipantsRequestOptions struct { - Body *UnlinkIdentityParticipantsBody +// PreflightExploreSelectionRequestOptions is the options needed to make a request to PreflightExploreSelection. +type PreflightExploreSelectionRequestOptions struct { + Body *PreflightExploreSelectionBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *UnlinkIdentityParticipantsRequestOptions) Validate() error { +func (o *PreflightExploreSelectionRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.Body != nil { @@ -2937,33 +2991,33 @@ func (o *UnlinkIdentityParticipantsRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *UnlinkIdentityParticipantsRequestOptions) GetPathParams() (map[string]any, error) { +func (o *PreflightExploreSelectionRequestOptions) GetPathParams() (map[string]any, error) { return nil, nil } // GetQuery returns the query params as a map. -func (o *UnlinkIdentityParticipantsRequestOptions) GetQuery() (map[string]any, error) { +func (o *PreflightExploreSelectionRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *UnlinkIdentityParticipantsRequestOptions) GetBody() any { +func (o *PreflightExploreSelectionRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *UnlinkIdentityParticipantsRequestOptions) GetHeader() (map[string]string, error) { +func (o *PreflightExploreSelectionRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// ImportMeetingRequestOptions is the options needed to make a request to ImportMeeting. -type ImportMeetingRequestOptions struct { - Body *ImportMeetingBody +// GroupFilesRequestOptions is the options needed to make a request to GroupFiles. +type GroupFilesRequestOptions struct { + Body *GroupFilesBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ImportMeetingRequestOptions) Validate() error { +func (o *GroupFilesRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.Body != nil { @@ -2981,39 +3035,911 @@ func (o *ImportMeetingRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ImportMeetingRequestOptions) GetPathParams() (map[string]any, error) { +func (o *GroupFilesRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *GroupFilesRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *GroupFilesRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *GroupFilesRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// SearchFilesRequestOptions is the options needed to make a request to SearchFiles. +type SearchFilesRequestOptions struct { + Body *SearchFilesBody +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *SearchFilesRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *SearchFilesRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *SearchFilesRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *SearchFilesRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *SearchFilesRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// GetFileRequestOptions is the options needed to make a request to GetFile. +type GetFileRequestOptions struct { + PathParams *GetFilePath +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *GetFileRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *GetFileRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *GetFileRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *GetFileRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *GetFileRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// GetFileContentRequestOptions is the options needed to make a request to GetFileContent. +type GetFileContentRequestOptions struct { + PathParams *GetFileContentPath +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *GetFileContentRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *GetFileContentRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *GetFileContentRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *GetFileContentRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *GetFileContentRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// LinkIdentityParticipantsRequestOptions is the options needed to make a request to LinkIdentityParticipants. +type LinkIdentityParticipantsRequestOptions struct { + Body *LinkIdentityParticipantsBody +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *LinkIdentityParticipantsRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *LinkIdentityParticipantsRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *LinkIdentityParticipantsRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *LinkIdentityParticipantsRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *LinkIdentityParticipantsRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// UnlinkIdentityParticipantsRequestOptions is the options needed to make a request to UnlinkIdentityParticipants. +type UnlinkIdentityParticipantsRequestOptions struct { + Body *UnlinkIdentityParticipantsBody +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *UnlinkIdentityParticipantsRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *UnlinkIdentityParticipantsRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *UnlinkIdentityParticipantsRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *UnlinkIdentityParticipantsRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *UnlinkIdentityParticipantsRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// ImportMeetingRequestOptions is the options needed to make a request to ImportMeeting. +type ImportMeetingRequestOptions struct { + Body *ImportMeetingBody +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ImportMeetingRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ImportMeetingRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *ImportMeetingRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *ImportMeetingRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *ImportMeetingRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// SearchIntegrationTasksRequestOptions is the options needed to make a request to SearchIntegrationTasks. +type SearchIntegrationTasksRequestOptions struct { + Query *SearchIntegrationTasksQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *SearchIntegrationTasksRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *SearchIntegrationTasksRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *SearchIntegrationTasksRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *SearchIntegrationTasksRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *SearchIntegrationTasksRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// ListMessagesRequestOptions is the options needed to make a request to ListMessages. +type ListMessagesRequestOptions struct { + Query *ListMessagesQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ListMessagesRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ListMessagesRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *ListMessagesRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *ListMessagesRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *ListMessagesRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// ListChangedMessagesRequestOptions is the options needed to make a request to ListChangedMessages. +type ListChangedMessagesRequestOptions struct { + Query *ListChangedMessagesQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ListChangedMessagesRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ListChangedMessagesRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *ListChangedMessagesRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *ListChangedMessagesRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *ListChangedMessagesRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// FilterMessagesRequestOptions is the options needed to make a request to FilterMessages. +type FilterMessagesRequestOptions struct { + Query *FilterMessagesQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *FilterMessagesRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *FilterMessagesRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *FilterMessagesRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *FilterMessagesRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *FilterMessagesRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// GetGmailIDsByFilterRequestOptions is the options needed to make a request to GetGmailIDsByFilter. +type GetGmailIDsByFilterRequestOptions struct { + Query *GetGmailIDsByFilterQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *GetGmailIDsByFilterRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *GetGmailIDsByFilterRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *GetGmailIDsByFilterRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *GetGmailIDsByFilterRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *GetGmailIDsByFilterRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// GetMessageRequestOptions is the options needed to make a request to GetMessage. +type GetMessageRequestOptions struct { + PathParams *GetMessagePath +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *GetMessageRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *GetMessageRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *GetMessageRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *GetMessageRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *GetMessageRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// GetMessageInlinePartRequestOptions is the options needed to make a request to GetMessageInlinePart. +type GetMessageInlinePartRequestOptions struct { + PathParams *GetMessageInlinePartPath + Query *GetMessageInlinePartQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *GetMessageInlinePartRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *GetMessageInlinePartRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *GetMessageInlinePartRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *GetMessageInlinePartRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *GetMessageInlinePartRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// ListMessageTasksRequestOptions is the options needed to make a request to ListMessageTasks. +type ListMessageTasksRequestOptions struct { + PathParams *ListMessageTasksPath +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ListMessageTasksRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ListMessageTasksRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *ListMessageTasksRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *ListMessageTasksRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *ListMessageTasksRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// CreateOrLinkMessageTaskRequestOptions is the options needed to make a request to CreateOrLinkMessageTask. +type CreateOrLinkMessageTaskRequestOptions struct { + PathParams *CreateOrLinkMessageTaskPath + Body *CreateOrLinkMessageTaskBody + Header *CreateOrLinkMessageTaskHeaders +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *CreateOrLinkMessageTaskRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *CreateOrLinkMessageTaskRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *CreateOrLinkMessageTaskRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *CreateOrLinkMessageTaskRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *CreateOrLinkMessageTaskRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) +} + +// UnlinkMessageTaskRequestOptions is the options needed to make a request to UnlinkMessageTask. +type UnlinkMessageTaskRequestOptions struct { + PathParams *UnlinkMessageTaskPath +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *UnlinkMessageTaskRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *UnlinkMessageTaskRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *UnlinkMessageTaskRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *UnlinkMessageTaskRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *UnlinkMessageTaskRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// ListOrganizationsRequestOptions is the options needed to make a request to ListOrganizations. +type ListOrganizationsRequestOptions struct { + Query *ListOrganizationsQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ListOrganizationsRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ListOrganizationsRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *ListOrganizationsRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *ListOrganizationsRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *ListOrganizationsRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// CreateOrganizationRequestOptions is the options needed to make a request to CreateOrganization. +type CreateOrganizationRequestOptions struct { + Body *CreateOrganizationBody +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *CreateOrganizationRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *CreateOrganizationRequestOptions) GetPathParams() (map[string]any, error) { + return nil, nil +} + +// GetQuery returns the query params as a map. +func (o *CreateOrganizationRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *CreateOrganizationRequestOptions) GetBody() any { + return o.Body +} + +// GetHeader returns the headers as a map. +func (o *CreateOrganizationRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + +// ListOrganizationDuplicateSuggestionsRequestOptions is the options needed to make a request to ListOrganizationDuplicateSuggestions. +type ListOrganizationDuplicateSuggestionsRequestOptions struct { + Query *ListOrganizationDuplicateSuggestionsQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ListOrganizationDuplicateSuggestionsRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ListOrganizationDuplicateSuggestionsRequestOptions) GetPathParams() (map[string]any, error) { return nil, nil } // GetQuery returns the query params as a map. -func (o *ImportMeetingRequestOptions) GetQuery() (map[string]any, error) { - return nil, nil +func (o *ListOrganizationDuplicateSuggestionsRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ImportMeetingRequestOptions) GetBody() any { - return o.Body +func (o *ListOrganizationDuplicateSuggestionsRequestOptions) GetBody() any { + return nil } // GetHeader returns the headers as a map. -func (o *ImportMeetingRequestOptions) GetHeader() (map[string]string, error) { +func (o *ListOrganizationDuplicateSuggestionsRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// SearchIntegrationTasksRequestOptions is the options needed to make a request to SearchIntegrationTasks. -type SearchIntegrationTasksRequestOptions struct { - Query *SearchIntegrationTasksQuery +// ResolveOrganizationDuplicateSuggestionRequestOptions is the options needed to make a request to ResolveOrganizationDuplicateSuggestion. +type ResolveOrganizationDuplicateSuggestionRequestOptions struct { + PathParams *ResolveOrganizationDuplicateSuggestionPath + Body *ResolveOrganizationDuplicateSuggestionBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *SearchIntegrationTasksRequestOptions) Validate() error { +func (o *ResolveOrganizationDuplicateSuggestionRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Query != nil { - if v, ok := any(o.Query).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Query", err) + errors = errors.Append("PathParams", err) + } + } + } + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) } } } @@ -3025,39 +3951,48 @@ func (o *SearchIntegrationTasksRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *SearchIntegrationTasksRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *ResolveOrganizationDuplicateSuggestionRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *SearchIntegrationTasksRequestOptions) GetQuery() (map[string]any, error) { - return runtime.AsMap[any](o.Query) +func (o *ResolveOrganizationDuplicateSuggestionRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *SearchIntegrationTasksRequestOptions) GetBody() any { - return nil +func (o *ResolveOrganizationDuplicateSuggestionRequestOptions) GetBody() any { + return o.Body } // GetHeader returns the headers as a map. -func (o *SearchIntegrationTasksRequestOptions) GetHeader() (map[string]string, error) { +func (o *ResolveOrganizationDuplicateSuggestionRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// ListMessagesRequestOptions is the options needed to make a request to ListMessages. -type ListMessagesRequestOptions struct { - Query *ListMessagesQuery +// DeleteOrganizationRequestOptions is the options needed to make a request to DeleteOrganization. +type DeleteOrganizationRequestOptions struct { + PathParams *DeleteOrganizationPath + Header *DeleteOrganizationHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ListMessagesRequestOptions) Validate() error { +func (o *DeleteOrganizationRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Query != nil { - if v, ok := any(o.Query).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Query", err) + errors = errors.Append("PathParams", err) + } + } + } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) } } } @@ -3069,39 +4004,39 @@ func (o *ListMessagesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ListMessagesRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *DeleteOrganizationRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *ListMessagesRequestOptions) GetQuery() (map[string]any, error) { - return runtime.AsMap[any](o.Query) +func (o *DeleteOrganizationRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ListMessagesRequestOptions) GetBody() any { +func (o *DeleteOrganizationRequestOptions) GetBody() any { return nil } // GetHeader returns the headers as a map. -func (o *ListMessagesRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *DeleteOrganizationRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } -// ListChangedMessagesRequestOptions is the options needed to make a request to ListChangedMessages. -type ListChangedMessagesRequestOptions struct { - Query *ListChangedMessagesQuery +// GetOrganizationRequestOptions is the options needed to make a request to GetOrganization. +type GetOrganizationRequestOptions struct { + PathParams *GetOrganizationPath } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ListChangedMessagesRequestOptions) Validate() error { +func (o *GetOrganizationRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Query != nil { - if v, ok := any(o.Query).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Query", err) + errors = errors.Append("PathParams", err) } } } @@ -3113,39 +4048,57 @@ func (o *ListChangedMessagesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ListChangedMessagesRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *GetOrganizationRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *ListChangedMessagesRequestOptions) GetQuery() (map[string]any, error) { - return runtime.AsMap[any](o.Query) +func (o *GetOrganizationRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ListChangedMessagesRequestOptions) GetBody() any { +func (o *GetOrganizationRequestOptions) GetBody() any { return nil } // GetHeader returns the headers as a map. -func (o *ListChangedMessagesRequestOptions) GetHeader() (map[string]string, error) { +func (o *GetOrganizationRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// FilterMessagesRequestOptions is the options needed to make a request to FilterMessages. -type FilterMessagesRequestOptions struct { - Query *FilterMessagesQuery +// PatchOrganizationRequestOptions is the options needed to make a request to PatchOrganization. +type PatchOrganizationRequestOptions struct { + PathParams *PatchOrganizationPath + Body *PatchOrganizationBody + Header *PatchOrganizationHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *FilterMessagesRequestOptions) Validate() error { +func (o *PatchOrganizationRequestOptions) Validate() error { var errors runtime.ValidationErrors - if o.Query != nil { - if v, ok := any(o.Query).(runtime.Validator); ok { + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Query", err) + errors = errors.Append("PathParams", err) + } + } + } + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) } } } @@ -3157,35 +4110,44 @@ func (o *FilterMessagesRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *FilterMessagesRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *PatchOrganizationRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *FilterMessagesRequestOptions) GetQuery() (map[string]any, error) { - return runtime.AsMap[any](o.Query) +func (o *PatchOrganizationRequestOptions) GetQuery() (map[string]any, error) { + return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *FilterMessagesRequestOptions) GetBody() any { - return nil +func (o *PatchOrganizationRequestOptions) GetBody() any { + return o.Body } // GetHeader returns the headers as a map. -func (o *FilterMessagesRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *PatchOrganizationRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } -// GetGmailIDsByFilterRequestOptions is the options needed to make a request to GetGmailIDsByFilter. -type GetGmailIDsByFilterRequestOptions struct { - Query *GetGmailIDsByFilterQuery +// ListOrganizationAttributesRequestOptions is the options needed to make a request to ListOrganizationAttributes. +type ListOrganizationAttributesRequestOptions struct { + PathParams *ListOrganizationAttributesPath + Query *ListOrganizationAttributesQuery } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *GetGmailIDsByFilterRequestOptions) Validate() error { +func (o *ListOrganizationAttributesRequestOptions) Validate() error { var errors runtime.ValidationErrors + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + if o.Query != nil { if v, ok := any(o.Query).(runtime.Validator); ok { if err := v.Validate(); err != nil { @@ -3201,33 +4163,34 @@ func (o *GetGmailIDsByFilterRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *GetGmailIDsByFilterRequestOptions) GetPathParams() (map[string]any, error) { - return nil, nil +func (o *ListOrganizationAttributesRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *GetGmailIDsByFilterRequestOptions) GetQuery() (map[string]any, error) { +func (o *ListOrganizationAttributesRequestOptions) GetQuery() (map[string]any, error) { return runtime.AsMap[any](o.Query) } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *GetGmailIDsByFilterRequestOptions) GetBody() any { +func (o *ListOrganizationAttributesRequestOptions) GetBody() any { return nil } // GetHeader returns the headers as a map. -func (o *GetGmailIDsByFilterRequestOptions) GetHeader() (map[string]string, error) { +func (o *ListOrganizationAttributesRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// GetMessageRequestOptions is the options needed to make a request to GetMessage. -type GetMessageRequestOptions struct { - PathParams *GetMessagePath +// SetOrganizationAttributeRequestOptions is the options needed to make a request to SetOrganizationAttribute. +type SetOrganizationAttributeRequestOptions struct { + PathParams *SetOrganizationAttributePath + Body *SetOrganizationAttributeBody } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *GetMessageRequestOptions) Validate() error { +func (o *SetOrganizationAttributeRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.PathParams != nil { @@ -3237,6 +4200,14 @@ func (o *GetMessageRequestOptions) Validate() error { } } } + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } if len(errors) == 0 { return nil } @@ -3245,34 +4216,34 @@ func (o *GetMessageRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *GetMessageRequestOptions) GetPathParams() (map[string]any, error) { +func (o *SetOrganizationAttributeRequestOptions) GetPathParams() (map[string]any, error) { return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *GetMessageRequestOptions) GetQuery() (map[string]any, error) { +func (o *SetOrganizationAttributeRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *GetMessageRequestOptions) GetBody() any { - return nil +func (o *SetOrganizationAttributeRequestOptions) GetBody() any { + return o.Body } // GetHeader returns the headers as a map. -func (o *GetMessageRequestOptions) GetHeader() (map[string]string, error) { +func (o *SetOrganizationAttributeRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// GetMessageInlinePartRequestOptions is the options needed to make a request to GetMessageInlinePart. -type GetMessageInlinePartRequestOptions struct { - PathParams *GetMessageInlinePartPath - Query *GetMessageInlinePartQuery +// ListOrganizationEmploymentsRequestOptions is the options needed to make a request to ListOrganizationEmployments. +type ListOrganizationEmploymentsRequestOptions struct { + PathParams *ListOrganizationEmploymentsPath + Query *ListOrganizationEmploymentsQuery } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *GetMessageInlinePartRequestOptions) Validate() error { +func (o *ListOrganizationEmploymentsRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.PathParams != nil { @@ -3298,33 +4269,33 @@ func (o *GetMessageInlinePartRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *GetMessageInlinePartRequestOptions) GetPathParams() (map[string]any, error) { +func (o *ListOrganizationEmploymentsRequestOptions) GetPathParams() (map[string]any, error) { return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *GetMessageInlinePartRequestOptions) GetQuery() (map[string]any, error) { +func (o *ListOrganizationEmploymentsRequestOptions) GetQuery() (map[string]any, error) { return runtime.AsMap[any](o.Query) } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *GetMessageInlinePartRequestOptions) GetBody() any { +func (o *ListOrganizationEmploymentsRequestOptions) GetBody() any { return nil } // GetHeader returns the headers as a map. -func (o *GetMessageInlinePartRequestOptions) GetHeader() (map[string]string, error) { +func (o *ListOrganizationEmploymentsRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// ListMessageTasksRequestOptions is the options needed to make a request to ListMessageTasks. -type ListMessageTasksRequestOptions struct { - PathParams *ListMessageTasksPath +// GetOrganizationHistoryRequestOptions is the options needed to make a request to GetOrganizationHistory. +type GetOrganizationHistoryRequestOptions struct { + PathParams *GetOrganizationHistoryPath } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *ListMessageTasksRequestOptions) Validate() error { +func (o *GetOrganizationHistoryRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.PathParams != nil { @@ -3342,35 +4313,35 @@ func (o *ListMessageTasksRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *ListMessageTasksRequestOptions) GetPathParams() (map[string]any, error) { +func (o *GetOrganizationHistoryRequestOptions) GetPathParams() (map[string]any, error) { return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *ListMessageTasksRequestOptions) GetQuery() (map[string]any, error) { +func (o *GetOrganizationHistoryRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *ListMessageTasksRequestOptions) GetBody() any { +func (o *GetOrganizationHistoryRequestOptions) GetBody() any { return nil } // GetHeader returns the headers as a map. -func (o *ListMessageTasksRequestOptions) GetHeader() (map[string]string, error) { +func (o *GetOrganizationHistoryRequestOptions) GetHeader() (map[string]string, error) { return nil, nil } -// CreateOrLinkMessageTaskRequestOptions is the options needed to make a request to CreateOrLinkMessageTask. -type CreateOrLinkMessageTaskRequestOptions struct { - PathParams *CreateOrLinkMessageTaskPath - Body *CreateOrLinkMessageTaskBody - Header *CreateOrLinkMessageTaskHeaders +// MergeOrganizationRequestOptions is the options needed to make a request to MergeOrganization. +type MergeOrganizationRequestOptions struct { + PathParams *MergeOrganizationPath + Body *MergeOrganizationBody + Header *MergeOrganizationHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *CreateOrLinkMessageTaskRequestOptions) Validate() error { +func (o *MergeOrganizationRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.PathParams != nil { @@ -3404,33 +4375,35 @@ func (o *CreateOrLinkMessageTaskRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *CreateOrLinkMessageTaskRequestOptions) GetPathParams() (map[string]any, error) { +func (o *MergeOrganizationRequestOptions) GetPathParams() (map[string]any, error) { return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *CreateOrLinkMessageTaskRequestOptions) GetQuery() (map[string]any, error) { +func (o *MergeOrganizationRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *CreateOrLinkMessageTaskRequestOptions) GetBody() any { +func (o *MergeOrganizationRequestOptions) GetBody() any { return o.Body } // GetHeader returns the headers as a map. -func (o *CreateOrLinkMessageTaskRequestOptions) GetHeader() (map[string]string, error) { +func (o *MergeOrganizationRequestOptions) GetHeader() (map[string]string, error) { return runtime.AsMap[string](o.Header) } -// UnlinkMessageTaskRequestOptions is the options needed to make a request to UnlinkMessageTask. -type UnlinkMessageTaskRequestOptions struct { - PathParams *UnlinkMessageTaskPath +// PutOrganizationProfileRequestOptions is the options needed to make a request to PutOrganizationProfile. +type PutOrganizationProfileRequestOptions struct { + PathParams *PutOrganizationProfilePath + Body *PutOrganizationProfileBody + Header *PutOrganizationProfileHeaders } // Validate validates all the fields in the options. // Use it if fields validation was not run. -func (o *UnlinkMessageTaskRequestOptions) Validate() error { +func (o *PutOrganizationProfileRequestOptions) Validate() error { var errors runtime.ValidationErrors if o.PathParams != nil { @@ -3440,6 +4413,22 @@ func (o *UnlinkMessageTaskRequestOptions) Validate() error { } } } + + if o.Body != nil { + if v, ok := any(o.Body).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Body", err) + } + } + } + + if o.Header != nil { + if v, ok := any(o.Header).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Header", err) + } + } + } if len(errors) == 0 { return nil } @@ -3448,23 +4437,23 @@ func (o *UnlinkMessageTaskRequestOptions) Validate() error { } // GetPathParams returns the path params as a map. -func (o *UnlinkMessageTaskRequestOptions) GetPathParams() (map[string]any, error) { +func (o *PutOrganizationProfileRequestOptions) GetPathParams() (map[string]any, error) { return runtime.AsMap[any](o.PathParams) } // GetQuery returns the query params as a map. -func (o *UnlinkMessageTaskRequestOptions) GetQuery() (map[string]any, error) { +func (o *PutOrganizationProfileRequestOptions) GetQuery() (map[string]any, error) { return nil, nil } // GetBody returns the payload in any type that can be marshalled to JSON by the client. -func (o *UnlinkMessageTaskRequestOptions) GetBody() any { - return nil +func (o *PutOrganizationProfileRequestOptions) GetBody() any { + return o.Body } // GetHeader returns the headers as a map. -func (o *UnlinkMessageTaskRequestOptions) GetHeader() (map[string]string, error) { - return nil, nil +func (o *PutOrganizationProfileRequestOptions) GetHeader() (map[string]string, error) { + return runtime.AsMap[string](o.Header) } // SearchPeopleRequestOptions is the options needed to make a request to SearchPeople. @@ -4085,6 +5074,59 @@ func (o *SetPersonAttributeRequestOptions) GetHeader() (map[string]string, error return nil, nil } +// ListPersonEmploymentsRequestOptions is the options needed to make a request to ListPersonEmployments. +type ListPersonEmploymentsRequestOptions struct { + PathParams *ListPersonEmploymentsPath + Query *ListPersonEmploymentsQuery +} + +// Validate validates all the fields in the options. +// Use it if fields validation was not run. +func (o *ListPersonEmploymentsRequestOptions) Validate() error { + var errors runtime.ValidationErrors + + if o.PathParams != nil { + if v, ok := any(o.PathParams).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("PathParams", err) + } + } + } + + if o.Query != nil { + if v, ok := any(o.Query).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Query", err) + } + } + } + if len(errors) == 0 { + return nil + } + + return errors +} + +// GetPathParams returns the path params as a map. +func (o *ListPersonEmploymentsRequestOptions) GetPathParams() (map[string]any, error) { + return runtime.AsMap[any](o.PathParams) +} + +// GetQuery returns the query params as a map. +func (o *ListPersonEmploymentsRequestOptions) GetQuery() (map[string]any, error) { + return runtime.AsMap[any](o.Query) +} + +// GetBody returns the payload in any type that can be marshalled to JSON by the client. +func (o *ListPersonEmploymentsRequestOptions) GetBody() any { + return nil +} + +// GetHeader returns the headers as a map. +func (o *ListPersonEmploymentsRequestOptions) GetHeader() (map[string]string, error) { + return nil, nil +} + // GetPersonStructuredProfileRequestOptions is the options needed to make a request to GetPersonStructuredProfile. type GetPersonStructuredProfileRequestOptions struct { PathParams *GetPersonStructuredProfilePath diff --git a/pkg/client/generated/client_with_response.go b/pkg/client/generated/client_with_response.go index 64e447b5a..7fbc2f2a0 100644 --- a/pkg/client/generated/client_with_response.go +++ b/pkg/client/generated/client_with_response.go @@ -4730,11 +4730,11 @@ func (c *Client) GetDomainTimelineWithResponse(ctx context.Context, options *Get } } -// Explore Explore canonical archive entries -func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreResp, error) { +// CreateEmployment Create an employment record +func (c *Client) CreateEmploymentWithResponse(ctx context.Context, options *CreateEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateEmploymentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments", Method: "POST", Options: options, ContentType: "application/json", @@ -4745,36 +4745,40 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ExploreResp{ + out := &CreateEmploymentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, } switch resp.StatusCode { - case 200: - out.JSON200 = new(ExploreResponse) + case 201: + out.JSON201 = new(CreateEmploymentResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreResponse", + TargetType: "CreateEmploymentResponse", Body: bodyBytes, Err: err, } } } + out.Headers201 = &CreateEmploymentResp201Headers{ + ETag: resp.Headers.Get("ETag"), + Location: resp.Headers.Get("Location"), + } return out, nil case 400: - out.JSON400 = new(ExploreErrorResponse) + out.JSON400 = new(CreateEmploymentErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { @@ -4782,7 +4786,7 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreErrorResponse", + TargetType: "CreateEmploymentErrorResponse", Body: bodyBytes, Err: err, } @@ -4790,7 +4794,7 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 409: - out.JSON409 = new(ExploreErrorResponseJSON) + out.JSON409 = new(CreateEmploymentErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { @@ -4798,7 +4802,7 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreErrorResponseJSON", + TargetType: "CreateEmploymentErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -4806,7 +4810,7 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(ExploreErrorResponseJSON503) + out.JSON503 = new(CreateEmploymentErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -4814,7 +4818,7 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreErrorResponseJSON503", + TargetType: "CreateEmploymentErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -4826,14 +4830,13 @@ func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreReques } } -// ListExploreFiles List bounded chronological attachment facts -func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *ListExploreFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListExploreFilesResp, error) { +// DeleteEmployment Delete an employment record +func (c *Client) DeleteEmploymentWithResponse(ctx context.Context, options *DeleteEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*DeleteEmploymentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/files", - Method: "POST", - Options: options, - ContentType: "application/json", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}", + Method: "DELETE", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -4841,44 +4844,46 @@ func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *List return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/files") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ListExploreFilesResp{ + out := &DeleteEmploymentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, } switch resp.StatusCode { - case 200: - out.JSON200 = new(ListExploreFilesResponse) + case 204: + return out, nil + case 400: + out.JSON400 = new(DeleteEmploymentErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListExploreFilesResponse", + TargetType: "DeleteEmploymentErrorResponse", Body: bodyBytes, Err: err, } } } - return out, nil - case 400: - out.JSON400 = new(ListExploreFilesErrorResponse) + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(DeleteEmploymentErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListExploreFilesErrorResponse", + TargetType: "DeleteEmploymentErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -4886,7 +4891,7 @@ func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *List } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 409: - out.JSON409 = new(ListExploreFilesErrorResponseJSON) + out.JSON409 = new(DeleteEmploymentErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { @@ -4894,7 +4899,23 @@ func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *List StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListExploreFilesErrorResponseJSON", + TargetType: "DeleteEmploymentErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(DeleteEmploymentErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteEmploymentErrorResponseJSON428", Body: bodyBytes, Err: err, } @@ -4902,7 +4923,7 @@ func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *List } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(ListExploreFilesErrorResponseJSON503) + out.JSON503 = new(DeleteEmploymentErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -4910,7 +4931,7 @@ func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *List StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListExploreFilesErrorResponseJSON503", + TargetType: "DeleteEmploymentErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -4922,14 +4943,13 @@ func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *List } } -// ExploreGroups Group canonical archive entries -func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *ExploreGroupsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreGroupsResp, error) { +// GetEmployment Get an employment record +func (c *Client) GetEmploymentWithResponse(ctx context.Context, options *GetEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetEmploymentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/groups", - Method: "POST", - Options: options, - ContentType: "application/json", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}", + Method: "GET", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -4937,12 +4957,12 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/groups") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ExploreGroupsResp{ + out := &GetEmploymentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -4950,7 +4970,7 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore switch resp.StatusCode { case 200: - out.JSON200 = new(ExploreGroupsResponse) + out.JSON200 = new(GetEmploymentResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -4958,15 +4978,18 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreGroupsResponse", + TargetType: "GetEmploymentResponse", Body: bodyBytes, Err: err, } } } + out.Headers200 = &GetEmploymentResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } return out, nil case 400: - out.JSON400 = new(ExploreGroupsErrorResponse) + out.JSON400 = new(GetEmploymentErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { @@ -4974,23 +4997,23 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreGroupsErrorResponse", + TargetType: "GetEmploymentErrorResponse", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 409: - out.JSON409 = new(ExploreGroupsErrorResponseJSON) + case 404: + out.JSON404 = new(GetEmploymentErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreGroupsErrorResponseJSON", + TargetType: "GetEmploymentErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -4998,7 +5021,7 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(ExploreGroupsErrorResponseJSON503) + out.JSON503 = new(GetEmploymentErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -5006,7 +5029,7 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ExploreGroupsErrorResponseJSON503", + TargetType: "GetEmploymentErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -5018,12 +5041,12 @@ func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *Explore } } -// CountExploreMatches Count exact lexical matches in visible rows -func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *CountExploreMatchesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CountExploreMatchesResp, error) { +// PatchEmployment Replace an employment record's mutable fields +func (c *Client) PatchEmploymentWithResponse(ctx context.Context, options *PatchEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchEmploymentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/match-counts", - Method: "POST", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}", + Method: "PATCH", Options: options, ContentType: "application/json", } @@ -5033,12 +5056,12 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/match-counts") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &CountExploreMatchesResp{ + out := &PatchEmploymentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5046,7 +5069,7 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C switch resp.StatusCode { case 200: - out.JSON200 = new(CountExploreMatchesResponse) + out.JSON200 = new(PatchEmploymentResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5054,15 +5077,18 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CountExploreMatchesResponse", + TargetType: "PatchEmploymentResponse", Body: bodyBytes, Err: err, } } } + out.Headers200 = &PatchEmploymentResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } return out, nil case 400: - out.JSON400 = new(CountExploreMatchesErrorResponse) + out.JSON400 = new(PatchEmploymentErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { @@ -5070,7 +5096,23 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CountExploreMatchesErrorResponse", + TargetType: "PatchEmploymentErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(PatchEmploymentErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchEmploymentErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -5078,7 +5120,7 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 409: - out.JSON409 = new(CountExploreMatchesErrorResponseJSON) + out.JSON409 = new(PatchEmploymentErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { @@ -5086,7 +5128,23 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CountExploreMatchesErrorResponseJSON", + TargetType: "PatchEmploymentErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(PatchEmploymentErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchEmploymentErrorResponseJSON428", Body: bodyBytes, Err: err, } @@ -5094,7 +5152,7 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(CountExploreMatchesErrorResponseJSON503) + out.JSON503 = new(PatchEmploymentErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -5102,7 +5160,7 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CountExploreMatchesErrorResponseJSON503", + TargetType: "PatchEmploymentErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -5114,11 +5172,11 @@ func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *C } } -// PreflightExploreSelection Preflight a revision-pinned analytical selection -func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, options *PreflightExploreSelectionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PreflightExploreSelectionResp, error) { +// EndEmployment End an employment record +func (c *Client) EndEmploymentWithResponse(ctx context.Context, options *EndEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*EndEmploymentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/preflight", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}/end", Method: "POST", Options: options, ContentType: "application/json", @@ -5129,12 +5187,12 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/preflight") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}/end") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &PreflightExploreSelectionResp{ + out := &EndEmploymentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5142,7 +5200,7 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti switch resp.StatusCode { case 200: - out.JSON200 = new(PreflightExploreSelectionResponse) + out.JSON200 = new(EndEmploymentResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5150,15 +5208,18 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "PreflightExploreSelectionResponse", + TargetType: "EndEmploymentResponse", Body: bodyBytes, Err: err, } } } + out.Headers200 = &EndEmploymentResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } return out, nil case 400: - out.JSON400 = new(PreflightExploreSelectionErrorResponse) + out.JSON400 = new(EndEmploymentErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { @@ -5166,7 +5227,23 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "PreflightExploreSelectionErrorResponse", + TargetType: "EndEmploymentErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(EndEmploymentErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "EndEmploymentErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -5174,7 +5251,7 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 409: - out.JSON409 = new(PreflightExploreSelectionErrorResponseJSON) + out.JSON409 = new(EndEmploymentErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { @@ -5182,7 +5259,23 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "PreflightExploreSelectionErrorResponseJSON", + TargetType: "EndEmploymentErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(EndEmploymentErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "EndEmploymentErrorResponseJSON428", Body: bodyBytes, Err: err, } @@ -5190,7 +5283,7 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(PreflightExploreSelectionErrorResponseJSON503) + out.JSON503 = new(EndEmploymentErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -5198,7 +5291,7 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "PreflightExploreSelectionErrorResponseJSON503", + TargetType: "EndEmploymentErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -5210,14 +5303,13 @@ func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, opti } } -// GroupFiles Group analytical files -func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GroupFilesResp, error) { +// SetPrimaryEmployment Set the primary current employment +func (c *Client) SetPrimaryEmploymentWithResponse(ctx context.Context, options *SetPrimaryEmploymentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetPrimaryEmploymentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/groups", - Method: "POST", - Options: options, - ContentType: "application/json", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/employments/{id}/primary", + Method: "POST", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5225,12 +5317,12 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/groups") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/employments/{id}/primary") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GroupFilesResp{ + out := &SetPrimaryEmploymentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5238,7 +5330,7 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles switch resp.StatusCode { case 200: - out.JSON200 = new(GroupFilesResponse) + out.JSON200 = new(SetPrimaryEmploymentResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5246,15 +5338,18 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GroupFilesResponse", + TargetType: "SetPrimaryEmploymentResponse", Body: bodyBytes, Err: err, } } } + out.Headers200 = &SetPrimaryEmploymentResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } return out, nil case 400: - out.JSON400 = new(GroupFilesErrorResponse) + out.JSON400 = new(SetPrimaryEmploymentErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { @@ -5262,7 +5357,23 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GroupFilesErrorResponse", + TargetType: "SetPrimaryEmploymentErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(SetPrimaryEmploymentErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SetPrimaryEmploymentErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -5270,7 +5381,7 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 409: - out.JSON409 = new(GroupFilesErrorResponseJSON) + out.JSON409 = new(SetPrimaryEmploymentErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { @@ -5278,7 +5389,23 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GroupFilesErrorResponseJSON", + TargetType: "SetPrimaryEmploymentErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(SetPrimaryEmploymentErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SetPrimaryEmploymentErrorResponseJSON428", Body: bodyBytes, Err: err, } @@ -5286,7 +5413,7 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(GroupFilesErrorResponseJSON503) + out.JSON503 = new(SetPrimaryEmploymentErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -5294,7 +5421,7 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GroupFilesErrorResponseJSON503", + TargetType: "SetPrimaryEmploymentErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -5306,11 +5433,11 @@ func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFiles } } -// SearchFiles Search analytical files -func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchFilesResp, error) { +// Explore Explore canonical archive entries +func (c *Client) ExploreWithResponse(ctx context.Context, options *ExploreRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/search", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore", Method: "POST", Options: options, ContentType: "application/json", @@ -5321,12 +5448,12 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/search") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &SearchFilesResp{ + out := &ExploreResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5334,7 +5461,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil switch resp.StatusCode { case 200: - out.JSON200 = new(SearchFilesResponse) + out.JSON200 = new(ExploreResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5342,7 +5469,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchFilesResponse", + TargetType: "ExploreResponse", Body: bodyBytes, Err: err, } @@ -5350,7 +5477,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil } return out, nil case 400: - out.JSON400 = new(SearchFilesErrorResponse) + out.JSON400 = new(ExploreErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { @@ -5358,7 +5485,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchFilesErrorResponse", + TargetType: "ExploreErrorResponse", Body: bodyBytes, Err: err, } @@ -5366,7 +5493,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 409: - out.JSON409 = new(SearchFilesErrorResponseJSON) + out.JSON409 = new(ExploreErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { @@ -5374,7 +5501,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchFilesErrorResponseJSON", + TargetType: "ExploreErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -5382,7 +5509,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(SearchFilesErrorResponseJSON503) + out.JSON503 = new(ExploreErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -5390,7 +5517,7 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchFilesErrorResponseJSON503", + TargetType: "ExploreErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -5402,26 +5529,27 @@ func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFil } } -// GetFile Get authoritative file metadata -func (c *Client) GetFileWithResponse(ctx context.Context, options *GetFileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileResp, error) { +// ListExploreFiles List bounded chronological attachment facts +func (c *Client) ListExploreFilesWithResponse(ctx context.Context, options *ListExploreFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListExploreFilesResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}", - Method: "GET", - Options: options, - } + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/files", + Method: "POST", + Options: options, + ContentType: "application/json", + } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/files") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GetFileResp{ + out := &ListExploreFilesResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5429,7 +5557,7 @@ func (c *Client) GetFileWithResponse(ctx context.Context, options *GetFileReques switch resp.StatusCode { case 200: - out.JSON200 = new(GetFileResponse) + out.JSON200 = new(ListExploreFilesResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5437,27 +5565,74 @@ func (c *Client) GetFileWithResponse(ctx context.Context, options *GetFileReques StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileResponse", + TargetType: "ListExploreFilesResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 500: + case 400: + out.JSON400 = new(ListExploreFilesErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListExploreFilesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(ListExploreFilesErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListExploreFilesErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListExploreFilesErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListExploreFilesErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// GetFileContent Download content for one authoritative file row -func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFileContentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileContentResp, error) { +// ExploreGroups Group canonical archive entries +func (c *Client) ExploreGroupsWithResponse(ctx context.Context, options *ExploreGroupsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ExploreGroupsResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}/content", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/groups", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5465,12 +5640,12 @@ func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFil return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}/content") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/groups") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GetFileContentResp{ + out := &ExploreGroupsResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5478,65 +5653,47 @@ func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFil switch resp.StatusCode { case 200: - return out, nil - case 400: - out.JSON400 = new(GetFileContentErrorResponse) - bodyBytes := resp.Content - if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { - return out, &runtime.ResponseDecodeError{ - StatusCode: resp.StatusCode, - ContentType: resp.Headers.Get("Content-Type"), - ContentLength: len(bodyBytes), - TargetType: "GetFileContentErrorResponse", - Body: bodyBytes, - Err: err, - } - } - } - return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 401: - out.JSON401 = new(GetFileContentErrorResponseJSON) + out.JSON200 = new(ExploreGroupsResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON401); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileContentErrorResponseJSON", + TargetType: "ExploreGroupsResponse", Body: bodyBytes, Err: err, } } } - return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 404: - out.JSON404 = new(GetFileContentErrorResponseJSON404) + return out, nil + case 400: + out.JSON400 = new(ExploreGroupsErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileContentErrorResponseJSON404", + TargetType: "ExploreGroupsErrorResponse", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 500: - out.JSON500 = new(GetFileContentErrorResponseJSON500) + case 409: + out.JSON409 = new(ExploreGroupsErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON500); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileContentErrorResponseJSON500", + TargetType: "ExploreGroupsErrorResponseJSON", Body: bodyBytes, Err: err, } @@ -5544,7 +5701,7 @@ func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFil } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(GetFileContentErrorResponseJSON503) + out.JSON503 = new(ExploreGroupsErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -5552,7 +5709,7 @@ func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFil StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetFileContentErrorResponseJSON503", + TargetType: "ExploreGroupsErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -5564,12 +5721,14 @@ func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFil } } -// GetHealth Get authenticated health details -func (c *Client) GetHealthWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetHealthResp, error) { +// CountExploreMatches Count exact lexical matches in visible rows +func (c *Client) CountExploreMatchesWithResponse(ctx context.Context, options *CountExploreMatchesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CountExploreMatchesResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/health", - Method: "GET", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/match-counts", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5577,12 +5736,12 @@ func (c *Client) GetHealthWithResponse(ctx context.Context, reqEditors ...runtim return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/health") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/match-counts") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GetHealthResp{ + out := &CountExploreMatchesResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5590,7 +5749,7 @@ func (c *Client) GetHealthWithResponse(ctx context.Context, reqEditors ...runtim switch resp.StatusCode { case 200: - out.JSON200 = new(GetHealthResponse) + out.JSON200 = new(CountExploreMatchesResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5598,125 +5757,71 @@ func (c *Client) GetHealthWithResponse(ctx context.Context, reqEditors ...runtim StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetHealthResponse", + TargetType: "CountExploreMatchesResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 500: + case 400: + out.JSON400 = new(CountExploreMatchesErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CountExploreMatchesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - default: - return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - } -} - -// LinkIdentityParticipants Assert two participants are the same person -func (c *Client) LinkIdentityParticipantsWithResponse(ctx context.Context, options *LinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*LinkIdentityParticipantsResp, error) { - var err error - reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/links", - Method: "POST", - Options: options, - ContentType: "application/json", - } - - req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/links") - if err != nil { - return nil, fmt.Errorf("error executing request: %w", err) - } - - out := &LinkIdentityParticipantsResp{ - HTTPResponse: resp.Raw, - Body: resp.Content, - StatusCode: resp.StatusCode, - } - - switch resp.StatusCode { - case 200: - out.JSON200 = new(LinkIdentityParticipantsResponse) + case 409: + out.JSON409 = new(CountExploreMatchesErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "LinkIdentityParticipantsResponse", + TargetType: "CountExploreMatchesErrorResponseJSON", Body: bodyBytes, Err: err, } } } - return out, nil - case 500: return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - default: - return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - } -} - -// UnlinkIdentityParticipants Remove a link edge between two participants -func (c *Client) UnlinkIdentityParticipantsWithResponse(ctx context.Context, options *UnlinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkIdentityParticipantsResp, error) { - var err error - reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/unlinks", - Method: "POST", - Options: options, - ContentType: "application/json", - } - - req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/unlinks") - if err != nil { - return nil, fmt.Errorf("error executing request: %w", err) - } - - out := &UnlinkIdentityParticipantsResp{ - HTTPResponse: resp.Raw, - Body: resp.Content, - StatusCode: resp.StatusCode, - } - - switch resp.StatusCode { - case 200: - out.JSON200 = new(UnlinkIdentityParticipantsResponse) + case 503: + out.JSON503 = new(CountExploreMatchesErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "UnlinkIdentityParticipantsResponse", + TargetType: "CountExploreMatchesErrorResponseJSON503", Body: bodyBytes, Err: err, } } } - return out, nil - case 500: return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// ImportMeeting Import one meeting -func (c *Client) ImportMeetingWithResponse(ctx context.Context, options *ImportMeetingRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ImportMeetingResp, error) { +// PreflightExploreSelection Preflight a revision-pinned analytical selection +func (c *Client) PreflightExploreSelectionWithResponse(ctx context.Context, options *PreflightExploreSelectionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PreflightExploreSelectionResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/import/meeting", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/explore/preflight", Method: "POST", Options: options, ContentType: "application/json", @@ -5727,12 +5832,12 @@ func (c *Client) ImportMeetingWithResponse(ctx context.Context, options *ImportM return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/import/meeting") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/explore/preflight") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ImportMeetingResp{ + out := &PreflightExploreSelectionResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5740,7 +5845,7 @@ func (c *Client) ImportMeetingWithResponse(ctx context.Context, options *ImportM switch resp.StatusCode { case 200: - out.JSON200 = new(ImportMeetingResponse) + out.JSON200 = new(PreflightExploreSelectionResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5748,43 +5853,170 @@ func (c *Client) ImportMeetingWithResponse(ctx context.Context, options *ImportM StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ImportMeetingResponse", + TargetType: "PreflightExploreSelectionResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 201: - out.JSON201 = new(ImportMeetingResponseJSON) + case 400: + out.JSON400 = new(PreflightExploreSelectionErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ImportMeetingResponseJSON", + TargetType: "PreflightExploreSelectionErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(PreflightExploreSelectionErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PreflightExploreSelectionErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(PreflightExploreSelectionErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PreflightExploreSelectionErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GroupFiles Group analytical files +func (c *Client) GroupFilesWithResponse(ctx context.Context, options *GroupFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GroupFilesResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/groups", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/groups") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GroupFilesResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(GroupFilesResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GroupFilesResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 500: + case 400: + out.JSON400 = new(GroupFilesErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GroupFilesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(GroupFilesErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GroupFilesErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(GroupFilesErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GroupFilesErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// SearchIntegrationTasks Search tasks in the configured project -func (c *Client) SearchIntegrationTasksWithResponse(ctx context.Context, options *SearchIntegrationTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchIntegrationTasksResp, error) { +// SearchFiles Search analytical files +func (c *Client) SearchFilesWithResponse(ctx context.Context, options *SearchFilesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchFilesResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/search", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/search", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5792,12 +6024,12 @@ func (c *Client) SearchIntegrationTasksWithResponse(ctx context.Context, options return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/search") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/search") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &SearchIntegrationTasksResp{ + out := &SearchFilesResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5805,7 +6037,7 @@ func (c *Client) SearchIntegrationTasksWithResponse(ctx context.Context, options switch resp.StatusCode { case 200: - out.JSON200 = new(SearchIntegrationTasksResponse) + out.JSON200 = new(SearchFilesResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5813,26 +6045,73 @@ func (c *Client) SearchIntegrationTasksWithResponse(ctx context.Context, options StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "SearchIntegrationTasksResponse", + TargetType: "SearchFilesResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 500: + case 400: + out.JSON400 = new(SearchFilesErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchFilesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(SearchFilesErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchFilesErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(SearchFilesErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchFilesErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// GetTaskIntegrationStatus Get task integration capability status -func (c *Client) GetTaskIntegrationStatusWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetTaskIntegrationStatusResp, error) { +// GetFile Get authoritative file metadata +func (c *Client) GetFileWithResponse(ctx context.Context, options *GetFileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/status", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}", Method: "GET", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5840,12 +6119,12 @@ func (c *Client) GetTaskIntegrationStatusWithResponse(ctx context.Context, reqEd return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/status") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GetTaskIntegrationStatusResp{ + out := &GetFileResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5853,7 +6132,7 @@ func (c *Client) GetTaskIntegrationStatusWithResponse(ctx context.Context, reqEd switch resp.StatusCode { case 200: - out.JSON200 = new(GetTaskIntegrationStatusResponse) + out.JSON200 = new(GetFileResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5861,7 +6140,7 @@ func (c *Client) GetTaskIntegrationStatusWithResponse(ctx context.Context, reqEd StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetTaskIntegrationStatusResponse", + TargetType: "GetFileResponse", Body: bodyBytes, Err: err, } @@ -5875,12 +6154,13 @@ func (c *Client) GetTaskIntegrationStatusWithResponse(ctx context.Context, reqEd } } -// TestTaskIntegration Test task integration discovery, authentication, capabilities, and project -func (c *Client) TestTaskIntegrationWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*TestTaskIntegrationResp, error) { +// GetFileContent Download content for one authoritative file row +func (c *Client) GetFileContentWithResponse(ctx context.Context, options *GetFileContentRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetFileContentResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/test", - Method: "POST", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/files/{id}/content", + Method: "GET", + Options: options, } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5888,12 +6168,12 @@ func (c *Client) TestTaskIntegrationWithResponse(ctx context.Context, reqEditors return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/test") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/files/{id}/content") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &TestTaskIntegrationResp{ + out := &GetFileContentResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5901,33 +6181,1830 @@ func (c *Client) TestTaskIntegrationWithResponse(ctx context.Context, reqEditors switch resp.StatusCode { case 200: - out.JSON200 = new(TestTaskIntegrationResponse) + return out, nil + case 400: + out.JSON400 = new(GetFileContentErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileContentErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 401: + out.JSON401 = new(GetFileContentErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON401); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileContentErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(GetFileContentErrorResponseJSON404) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileContentErrorResponseJSON404", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 500: + out.JSON500 = new(GetFileContentErrorResponseJSON500) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON500); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileContentErrorResponseJSON500", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(GetFileContentErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetFileContentErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GetHealth Get authenticated health details +func (c *Client) GetHealthWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetHealthResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/health", + Method: "GET", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/health") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GetHealthResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(GetHealthResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetHealthResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// LinkIdentityParticipants Assert two participants are the same person +func (c *Client) LinkIdentityParticipantsWithResponse(ctx context.Context, options *LinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*LinkIdentityParticipantsResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/links", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/links") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &LinkIdentityParticipantsResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(LinkIdentityParticipantsResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "LinkIdentityParticipantsResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// UnlinkIdentityParticipants Remove a link edge between two participants +func (c *Client) UnlinkIdentityParticipantsWithResponse(ctx context.Context, options *UnlinkIdentityParticipantsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkIdentityParticipantsResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/identity/unlinks", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/identity/unlinks") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &UnlinkIdentityParticipantsResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(UnlinkIdentityParticipantsResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "UnlinkIdentityParticipantsResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ImportMeeting Import one meeting +func (c *Client) ImportMeetingWithResponse(ctx context.Context, options *ImportMeetingRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ImportMeetingResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/import/meeting", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/import/meeting") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ImportMeetingResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ImportMeetingResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ImportMeetingResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 201: + out.JSON201 = new(ImportMeetingResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ImportMeetingResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// SearchIntegrationTasks Search tasks in the configured project +func (c *Client) SearchIntegrationTasksWithResponse(ctx context.Context, options *SearchIntegrationTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SearchIntegrationTasksResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/search", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/search") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &SearchIntegrationTasksResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(SearchIntegrationTasksResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "SearchIntegrationTasksResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GetTaskIntegrationStatus Get task integration capability status +func (c *Client) GetTaskIntegrationStatusWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*GetTaskIntegrationStatusResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/status", + Method: "GET", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/status") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GetTaskIntegrationStatusResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(GetTaskIntegrationStatusResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetTaskIntegrationStatusResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// TestTaskIntegration Test task integration discovery, authentication, capabilities, and project +func (c *Client) TestTaskIntegrationWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*TestTaskIntegrationResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/integrations/tasks/test", + Method: "POST", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/integrations/tasks/test") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &TestTaskIntegrationResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(TestTaskIntegrationResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "TestTaskIntegrationResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ListMessages List messages +func (c *Client) ListMessagesWithResponse(ctx context.Context, options *ListMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessagesResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ListMessagesResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ListMessagesResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListMessagesResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ListChangedMessages List messages whose content changed since a cursor +func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *ListChangedMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListChangedMessagesResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/changes", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/changes") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ListChangedMessagesResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ListChangedMessagesResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 400: + out.JSON400 = new(ListChangedMessagesErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 401: + out.JSON401 = new(ListChangedMessagesErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON401); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 429: + out.JSON429 = new(ListChangedMessagesErrorResponseJSON429) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON429); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesErrorResponseJSON429", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 500: + out.JSON500 = new(ListChangedMessagesErrorResponseJSON500) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON500); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesErrorResponseJSON500", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListChangedMessagesErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListChangedMessagesErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// FilterMessages List filtered messages +func (c *Client) FilterMessagesWithResponse(ctx context.Context, options *FilterMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*FilterMessagesResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/filter", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/filter") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &FilterMessagesResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(FilterMessagesResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "FilterMessagesResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GetGmailIDsByFilter List Gmail message IDs matching a filter +func (c *Client) GetGmailIDsByFilterWithResponse(ctx context.Context, options *GetGmailIDsByFilterRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetGmailIDsByFilterResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/gmail-ids", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/gmail-ids") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GetGmailIDsByFilterResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(GetGmailIDsByFilterResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetGmailIDsByFilterResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GetMessage Get one message +func (c *Client) GetMessageWithResponse(ctx context.Context, options *GetMessageRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GetMessageResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(GetMessageResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GetMessageInlinePart Get an inline MIME part +func (c *Client) GetMessageInlinePartWithResponse(ctx context.Context, options *GetMessageInlinePartRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageInlinePartResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/inline", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/inline") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GetMessageInlinePartResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + return out, nil + case 400: + out.JSON400 = new(GetMessageInlinePartErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 401: + out.JSON401 = new(GetMessageInlinePartErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON401); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(GetMessageInlinePartErrorResponseJSON404) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponseJSON404", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 415: + out.JSON415 = new(GetMessageInlinePartErrorResponseJSON415) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON415); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponseJSON415", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 500: + out.JSON500 = new(GetMessageInlinePartErrorResponseJSON500) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON500); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponseJSON500", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 501: + out.JSON501 = new(GetMessageInlinePartErrorResponseJSON501) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON501); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponseJSON501", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(GetMessageInlinePartErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetMessageInlinePartErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ListMessageTasks List tasks linked to an archived email +func (c *Client) ListMessageTasksWithResponse(ctx context.Context, options *ListMessageTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessageTasksResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ListMessageTasksResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ListMessageTasksResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListMessageTasksResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// CreateOrLinkMessageTask Create or link a task for an archived email +func (c *Client) CreateOrLinkMessageTaskWithResponse(ctx context.Context, options *CreateOrLinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrLinkMessageTaskResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &CreateOrLinkMessageTaskResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 201: + out.JSON201 = new(CreateOrLinkMessageTaskResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CreateOrLinkMessageTaskResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// UnlinkMessageTask Unlink a task from an archived email +func (c *Client) UnlinkMessageTaskWithResponse(ctx context.Context, options *UnlinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkMessageTaskResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks/{task_id}", + Method: "DELETE", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks/{task_id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &UnlinkMessageTaskResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(UnlinkMessageTaskResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "UnlinkMessageTaskResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 500: + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ListOrganizations List organizations +func (c *Client) ListOrganizationsWithResponse(ctx context.Context, options *ListOrganizationsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationsResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ListOrganizationsResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ListOrganizationsResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationsResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 400: + out.JSON400 = new(ListOrganizationsErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListOrganizationsErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationsErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// CreateOrganization Create an organization +func (c *Client) CreateOrganizationWithResponse(ctx context.Context, options *CreateOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrganizationResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &CreateOrganizationResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 201: + out.JSON201 = new(CreateOrganizationResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CreateOrganizationResponse", + Body: bodyBytes, + Err: err, + } + } + } + out.Headers201 = &CreateOrganizationResp201Headers{ + ETag: resp.Headers.Get("ETag"), + Location: resp.Headers.Get("Location"), + } + return out, nil + case 400: + out.JSON400 = new(CreateOrganizationErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CreateOrganizationErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(CreateOrganizationErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "CreateOrganizationErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ListOrganizationDuplicateSuggestions List organization duplicate suggestions +func (c *Client) ListOrganizationDuplicateSuggestionsWithResponse(ctx context.Context, options *ListOrganizationDuplicateSuggestionsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationDuplicateSuggestionsResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/duplicate-suggestions", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/duplicate-suggestions") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ListOrganizationDuplicateSuggestionsResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ListOrganizationDuplicateSuggestionsResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationDuplicateSuggestionsResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 400: + out.JSON400 = new(ListOrganizationDuplicateSuggestionsErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationDuplicateSuggestionsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListOrganizationDuplicateSuggestionsErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationDuplicateSuggestionsErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// RefreshOrganizationDuplicateSuggestions Refresh organization duplicate suggestions +func (c *Client) RefreshOrganizationDuplicateSuggestionsWithResponse(ctx context.Context, reqEditors ...runtime.RequestEditorFn) (*RefreshOrganizationDuplicateSuggestionsResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/duplicate-suggestions/refresh", + Method: "POST", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/duplicate-suggestions/refresh") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &RefreshOrganizationDuplicateSuggestionsResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(RefreshOrganizationDuplicateSuggestionsResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "RefreshOrganizationDuplicateSuggestionsResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 503: + out.JSON503 = new(RefreshOrganizationDuplicateSuggestionsErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "RefreshOrganizationDuplicateSuggestionsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// ResolveOrganizationDuplicateSuggestion Resolve an organization duplicate suggestion +func (c *Client) ResolveOrganizationDuplicateSuggestionWithResponse(ctx context.Context, options *ResolveOrganizationDuplicateSuggestionRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ResolveOrganizationDuplicateSuggestionResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/duplicate-suggestions/{id}/resolve", + Method: "POST", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/duplicate-suggestions/{id}/resolve") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ResolveOrganizationDuplicateSuggestionResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ResolveOrganizationDuplicateSuggestionResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ResolveOrganizationDuplicateSuggestionResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 400: + out.JSON400 = new(ResolveOrganizationDuplicateSuggestionErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ResolveOrganizationDuplicateSuggestionErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(ResolveOrganizationDuplicateSuggestionErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ResolveOrganizationDuplicateSuggestionErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(ResolveOrganizationDuplicateSuggestionErrorResponseJSON409) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ResolveOrganizationDuplicateSuggestionErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ResolveOrganizationDuplicateSuggestionErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ResolveOrganizationDuplicateSuggestionErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// DeleteOrganization Delete an organization without employment records +func (c *Client) DeleteOrganizationWithResponse(ctx context.Context, options *DeleteOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*DeleteOrganizationResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}", + Method: "DELETE", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &DeleteOrganizationResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 204: + return out, nil + case 400: + out.JSON400 = new(DeleteOrganizationErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteOrganizationErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(DeleteOrganizationErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteOrganizationErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(DeleteOrganizationErrorResponseJSON409) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteOrganizationErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(DeleteOrganizationErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteOrganizationErrorResponseJSON428", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(DeleteOrganizationErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "DeleteOrganizationErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// GetOrganization Get an organization +func (c *Client) GetOrganizationWithResponse(ctx context.Context, options *GetOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &GetOrganizationResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(GetOrganizationResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationResponse", + Body: bodyBytes, + Err: err, + } + } + } + out.Headers200 = &GetOrganizationResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } + return out, nil + case 400: + out.JSON400 = new(GetOrganizationErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(GetOrganizationErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(GetOrganizationErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + +// PatchOrganization Replace an organization's mutable fields +func (c *Client) PatchOrganizationWithResponse(ctx context.Context, options *PatchOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PatchOrganizationResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}", + Method: "PATCH", + Options: options, + ContentType: "application/json", + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &PatchOrganizationResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(PatchOrganizationResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchOrganizationResponse", + Body: bodyBytes, + Err: err, + } + } + } + out.Headers200 = &PatchOrganizationResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } + return out, nil + case 400: + out.JSON400 = new(PatchOrganizationErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchOrganizationErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(PatchOrganizationErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchOrganizationErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 409: + out.JSON409 = new(PatchOrganizationErrorResponseJSON409) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchOrganizationErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(PatchOrganizationErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PatchOrganizationErrorResponseJSON428", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(PatchOrganizationErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "TestTaskIntegrationResponse", + TargetType: "PatchOrganizationErrorResponseJSON503", Body: bodyBytes, Err: err, } } } - return out, nil - case 500: return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// ListMessages List messages -func (c *Client) ListMessagesWithResponse(ctx context.Context, options *ListMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessagesResp, error) { +// ListOrganizationAttributes List organization typed attributes +func (c *Client) ListOrganizationAttributesWithResponse(ctx context.Context, options *ListOrganizationAttributesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationAttributesResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/attributes", Method: "GET", Options: options, } @@ -5937,12 +8014,12 @@ func (c *Client) ListMessagesWithResponse(ctx context.Context, options *ListMess return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/attributes") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ListMessagesResp{ + out := &ListOrganizationAttributesResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5950,7 +8027,7 @@ func (c *Client) ListMessagesWithResponse(ctx context.Context, options *ListMess switch resp.StatusCode { case 200: - out.JSON200 = new(ListMessagesResponse) + out.JSON200 = new(ListOrganizationAttributesResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -5958,27 +8035,74 @@ func (c *Client) ListMessagesWithResponse(ctx context.Context, options *ListMess StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListMessagesResponse", + TargetType: "ListOrganizationAttributesResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 500: + case 400: + out.JSON400 = new(ListOrganizationAttributesErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationAttributesErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(ListOrganizationAttributesErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationAttributesErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListOrganizationAttributesErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationAttributesErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// ListChangedMessages List messages whose content changed since a cursor -func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *ListChangedMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListChangedMessagesResp, error) { +// SetOrganizationAttribute Set an organization typed attribute +func (c *Client) SetOrganizationAttributeWithResponse(ctx context.Context, options *SetOrganizationAttributeRequestOptions, reqEditors ...runtime.RequestEditorFn) (*SetOrganizationAttributeResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/changes", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/attributes", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -5986,12 +8110,12 @@ func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *L return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/changes") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/attributes") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ListChangedMessagesResp{ + out := &SetOrganizationAttributeResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -5999,7 +8123,7 @@ func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *L switch resp.StatusCode { case 200: - out.JSON200 = new(ListChangedMessagesResponse) + out.JSON200 = new(SetOrganizationAttributeResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -6007,71 +8131,71 @@ func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *L StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesResponse", + TargetType: "SetOrganizationAttributeResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 400: - out.JSON400 = new(ListChangedMessagesErrorResponse) + case 201: + out.JSON201 = new(SetOrganizationAttributeResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesErrorResponse", + TargetType: "SetOrganizationAttributeResponseJSON", Body: bodyBytes, Err: err, } } } - return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 401: - out.JSON401 = new(ListChangedMessagesErrorResponseJSON) + return out, nil + case 400: + out.JSON400 = new(SetOrganizationAttributeErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON401); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesErrorResponseJSON", + TargetType: "SetOrganizationAttributeErrorResponse", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 429: - out.JSON429 = new(ListChangedMessagesErrorResponseJSON429) + case 404: + out.JSON404 = new(SetOrganizationAttributeErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON429); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesErrorResponseJSON429", + TargetType: "SetOrganizationAttributeErrorResponseJSON", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 500: - out.JSON500 = new(ListChangedMessagesErrorResponseJSON500) + case 409: + out.JSON409 = new(SetOrganizationAttributeErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON500); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesErrorResponseJSON500", + TargetType: "SetOrganizationAttributeErrorResponseJSON409", Body: bodyBytes, Err: err, } @@ -6079,7 +8203,7 @@ func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *L } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(ListChangedMessagesErrorResponseJSON503) + out.JSON503 = new(SetOrganizationAttributeErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -6087,7 +8211,7 @@ func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *L StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListChangedMessagesErrorResponseJSON503", + TargetType: "SetOrganizationAttributeErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -6099,11 +8223,11 @@ func (c *Client) ListChangedMessagesWithResponse(ctx context.Context, options *L } } -// FilterMessages List filtered messages -func (c *Client) FilterMessagesWithResponse(ctx context.Context, options *FilterMessagesRequestOptions, reqEditors ...runtime.RequestEditorFn) (*FilterMessagesResp, error) { +// ListOrganizationEmployments List an organization's employment records +func (c *Client) ListOrganizationEmploymentsWithResponse(ctx context.Context, options *ListOrganizationEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListOrganizationEmploymentsResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/filter", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/employments", Method: "GET", Options: options, } @@ -6113,12 +8237,12 @@ func (c *Client) FilterMessagesWithResponse(ctx context.Context, options *Filter return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/filter") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/employments") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &FilterMessagesResp{ + out := &ListOrganizationEmploymentsResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -6126,7 +8250,7 @@ func (c *Client) FilterMessagesWithResponse(ctx context.Context, options *Filter switch resp.StatusCode { case 200: - out.JSON200 = new(FilterMessagesResponse) + out.JSON200 = new(ListOrganizationEmploymentsResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -6134,74 +8258,71 @@ func (c *Client) FilterMessagesWithResponse(ctx context.Context, options *Filter StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "FilterMessagesResponse", + TargetType: "ListOrganizationEmploymentsResponse", Body: bodyBytes, Err: err, } } } return out, nil - case 500: + case 400: + out.JSON400 = new(ListOrganizationEmploymentsErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationEmploymentsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - default: - return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - } -} - -// GetGmailIDsByFilter List Gmail message IDs matching a filter -func (c *Client) GetGmailIDsByFilterWithResponse(ctx context.Context, options *GetGmailIDsByFilterRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetGmailIDsByFilterResp, error) { - var err error - reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/gmail-ids", - Method: "GET", - Options: options, - } - - req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/gmail-ids") - if err != nil { - return nil, fmt.Errorf("error executing request: %w", err) - } - - out := &GetGmailIDsByFilterResp{ - HTTPResponse: resp.Raw, - Body: resp.Content, - StatusCode: resp.StatusCode, - } - - switch resp.StatusCode { - case 200: - out.JSON200 = new(GetGmailIDsByFilterResponse) + case 404: + out.JSON404 = new(ListOrganizationEmploymentsErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetGmailIDsByFilterResponse", + TargetType: "ListOrganizationEmploymentsErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListOrganizationEmploymentsErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListOrganizationEmploymentsErrorResponseJSON503", Body: bodyBytes, Err: err, } } } - return out, nil - case 500: return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// GetMessage Get one message -func (c *Client) GetMessageWithResponse(ctx context.Context, options *GetMessageRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageResp, error) { +// GetOrganizationHistory Get organization profile history +func (c *Client) GetOrganizationHistoryWithResponse(ctx context.Context, options *GetOrganizationHistoryRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetOrganizationHistoryResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}", + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/history", Method: "GET", Options: options, } @@ -6211,12 +8332,12 @@ func (c *Client) GetMessageWithResponse(ctx context.Context, options *GetMessage return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/history") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GetMessageResp{ + out := &GetOrganizationHistoryResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -6224,7 +8345,7 @@ func (c *Client) GetMessageWithResponse(ctx context.Context, options *GetMessage switch resp.StatusCode { case 200: - out.JSON200 = new(GetMessageResponse) + out.JSON200 = new(GetOrganizationHistoryResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -6232,27 +8353,77 @@ func (c *Client) GetMessageWithResponse(ctx context.Context, options *GetMessage StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageResponse", + TargetType: "GetOrganizationHistoryResponse", Body: bodyBytes, Err: err, } } } + out.Headers200 = &GetOrganizationHistoryResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } return out, nil - case 500: + case 400: + out.JSON400 = new(GetOrganizationHistoryErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationHistoryErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(GetOrganizationHistoryErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationHistoryErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(GetOrganizationHistoryErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "GetOrganizationHistoryErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) } } -// GetMessageInlinePart Get an inline MIME part -func (c *Client) GetMessageInlinePartWithResponse(ctx context.Context, options *GetMessageInlinePartRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetMessageInlinePartResp, error) { +// MergeOrganization Merge another organization into this organization +func (c *Client) MergeOrganizationWithResponse(ctx context.Context, options *MergeOrganizationRequestOptions, reqEditors ...runtime.RequestEditorFn) (*MergeOrganizationResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/inline", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/merge", + Method: "POST", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -6260,110 +8431,95 @@ func (c *Client) GetMessageInlinePartWithResponse(ctx context.Context, options * return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/inline") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/merge") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &GetMessageInlinePartResp{ + out := &MergeOrganizationResp{ HTTPResponse: resp.Raw, - Body: resp.Content, - StatusCode: resp.StatusCode, - } - - switch resp.StatusCode { - case 200: - return out, nil - case 400: - out.JSON400 = new(GetMessageInlinePartErrorResponse) - bodyBytes := resp.Content - if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { - return out, &runtime.ResponseDecodeError{ - StatusCode: resp.StatusCode, - ContentType: resp.Headers.Get("Content-Type"), - ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponse", - Body: bodyBytes, - Err: err, - } - } - } - return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 401: - out.JSON401 = new(GetMessageInlinePartErrorResponseJSON) + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(MergeOrganizationResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON401); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponseJSON", + TargetType: "MergeOrganizationResponse", Body: bodyBytes, Err: err, } } } - return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 404: - out.JSON404 = new(GetMessageInlinePartErrorResponseJSON404) + out.Headers200 = &MergeOrganizationResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } + return out, nil + case 400: + out.JSON400 = new(MergeOrganizationErrorResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponseJSON404", + TargetType: "MergeOrganizationErrorResponse", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 415: - out.JSON415 = new(GetMessageInlinePartErrorResponseJSON415) + case 404: + out.JSON404 = new(MergeOrganizationErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON415); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponseJSON415", + TargetType: "MergeOrganizationErrorResponseJSON", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 500: - out.JSON500 = new(GetMessageInlinePartErrorResponseJSON500) + case 409: + out.JSON409 = new(MergeOrganizationErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON500); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponseJSON500", + TargetType: "MergeOrganizationErrorResponseJSON409", Body: bodyBytes, Err: err, } } } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - case 501: - out.JSON501 = new(GetMessageInlinePartErrorResponseJSON501) + case 428: + out.JSON428 = new(MergeOrganizationErrorResponseJSON428) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON501); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponseJSON501", + TargetType: "MergeOrganizationErrorResponseJSON428", Body: bodyBytes, Err: err, } @@ -6371,7 +8527,7 @@ func (c *Client) GetMessageInlinePartWithResponse(ctx context.Context, options * } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) case 503: - out.JSON503 = new(GetMessageInlinePartErrorResponseJSON503) + out.JSON503 = new(MergeOrganizationErrorResponseJSON503) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { @@ -6379,7 +8535,7 @@ func (c *Client) GetMessageInlinePartWithResponse(ctx context.Context, options * StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "GetMessageInlinePartErrorResponseJSON503", + TargetType: "MergeOrganizationErrorResponseJSON503", Body: bodyBytes, Err: err, } @@ -6391,13 +8547,14 @@ func (c *Client) GetMessageInlinePartWithResponse(ctx context.Context, options * } } -// ListMessageTasks List tasks linked to an archived email -func (c *Client) ListMessageTasksWithResponse(ctx context.Context, options *ListMessageTasksRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListMessageTasksResp, error) { +// PutOrganizationProfile Replace organization profile collections +func (c *Client) PutOrganizationProfileWithResponse(ctx context.Context, options *PutOrganizationProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*PutOrganizationProfileResp, error) { var err error reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", - Method: "GET", - Options: options, + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/organizations/{id}/profile", + Method: "PUT", + Options: options, + ContentType: "application/json", } req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) @@ -6405,12 +8562,12 @@ func (c *Client) ListMessageTasksWithResponse(ctx context.Context, options *List return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/organizations/{id}/profile") if err != nil { return nil, fmt.Errorf("error executing request: %w", err) } - out := &ListMessageTasksResp{ + out := &PutOrganizationProfileResp{ HTTPResponse: resp.Raw, Body: resp.Content, StatusCode: resp.StatusCode, @@ -6418,7 +8575,7 @@ func (c *Client) ListMessageTasksWithResponse(ctx context.Context, options *List switch resp.StatusCode { case 200: - out.JSON200 = new(ListMessageTasksResponse) + out.JSON200 = new(PutOrganizationProfileResponse) bodyBytes := resp.Content if len(bodyBytes) > 0 { if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { @@ -6426,113 +8583,95 @@ func (c *Client) ListMessageTasksWithResponse(ctx context.Context, options *List StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "ListMessageTasksResponse", + TargetType: "PutOrganizationProfileResponse", Body: bodyBytes, Err: err, } } } + out.Headers200 = &PutOrganizationProfileResp200Headers{ + ETag: resp.Headers.Get("ETag"), + } return out, nil - case 500: + case 400: + out.JSON400 = new(PutOrganizationProfileErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PutOrganizationProfileErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - default: - return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - } -} - -// CreateOrLinkMessageTask Create or link a task for an archived email -func (c *Client) CreateOrLinkMessageTaskWithResponse(ctx context.Context, options *CreateOrLinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*CreateOrLinkMessageTaskResp, error) { - var err error - reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks", - Method: "POST", - Options: options, - ContentType: "application/json", - } - - req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks") - if err != nil { - return nil, fmt.Errorf("error executing request: %w", err) - } - - out := &CreateOrLinkMessageTaskResp{ - HTTPResponse: resp.Raw, - Body: resp.Content, - StatusCode: resp.StatusCode, - } - - switch resp.StatusCode { - case 201: - out.JSON201 = new(CreateOrLinkMessageTaskResponse) + case 404: + out.JSON404 = new(PutOrganizationProfileErrorResponseJSON) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON201); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "CreateOrLinkMessageTaskResponse", + TargetType: "PutOrganizationProfileErrorResponseJSON", Body: bodyBytes, Err: err, } } } - return out, nil - case 500: return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - default: - return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) - } -} - -// UnlinkMessageTask Unlink a task from an archived email -func (c *Client) UnlinkMessageTaskWithResponse(ctx context.Context, options *UnlinkMessageTaskRequestOptions, reqEditors ...runtime.RequestEditorFn) (*UnlinkMessageTaskResp, error) { - var err error - reqParams := runtime.RequestOptionsParameters{ - RequestURL: c.apiClient.GetBaseURL() + "/api/v1/messages/{id}/tasks/{task_id}", - Method: "DELETE", - Options: options, - } - - req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/messages/{id}/tasks/{task_id}") - if err != nil { - return nil, fmt.Errorf("error executing request: %w", err) - } - - out := &UnlinkMessageTaskResp{ - HTTPResponse: resp.Raw, - Body: resp.Content, - StatusCode: resp.StatusCode, - } - - switch resp.StatusCode { - case 200: - out.JSON200 = new(UnlinkMessageTaskResponse) + case 409: + out.JSON409 = new(PutOrganizationProfileErrorResponseJSON409) bodyBytes := resp.Content if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + if err := json.Unmarshal(bodyBytes, out.JSON409); err != nil { return out, &runtime.ResponseDecodeError{ StatusCode: resp.StatusCode, ContentType: resp.Headers.Get("Content-Type"), ContentLength: len(bodyBytes), - TargetType: "UnlinkMessageTaskResponse", + TargetType: "PutOrganizationProfileErrorResponseJSON409", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 428: + out.JSON428 = new(PutOrganizationProfileErrorResponseJSON428) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON428); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PutOrganizationProfileErrorResponseJSON428", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(PutOrganizationProfileErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "PutOrganizationProfileErrorResponseJSON503", Body: bodyBytes, Err: err, } } } - return out, nil - case 500: return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) default: return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) @@ -7780,6 +9919,101 @@ func (c *Client) SetPersonAttributeWithResponse(ctx context.Context, options *Se } } +// ListPersonEmployments List a person's employment history +func (c *Client) ListPersonEmploymentsWithResponse(ctx context.Context, options *ListPersonEmploymentsRequestOptions, reqEditors ...runtime.RequestEditorFn) (*ListPersonEmploymentsResp, error) { + var err error + reqParams := runtime.RequestOptionsParameters{ + RequestURL: c.apiClient.GetBaseURL() + "/api/v1/persons/{id}/employments", + Method: "GET", + Options: options, + } + + req, err := c.apiClient.CreateRequest(ctx, reqParams, reqEditors...) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + resp, err := c.apiClient.ExecuteRequest(ctx, req, "/api/v1/persons/{id}/employments") + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + out := &ListPersonEmploymentsResp{ + HTTPResponse: resp.Raw, + Body: resp.Content, + StatusCode: resp.StatusCode, + } + + switch resp.StatusCode { + case 200: + out.JSON200 = new(ListPersonEmploymentsResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON200); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListPersonEmploymentsResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, nil + case 400: + out.JSON400 = new(ListPersonEmploymentsErrorResponse) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON400); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListPersonEmploymentsErrorResponse", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 404: + out.JSON404 = new(ListPersonEmploymentsErrorResponseJSON) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON404); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListPersonEmploymentsErrorResponseJSON", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + case 503: + out.JSON503 = new(ListPersonEmploymentsErrorResponseJSON503) + bodyBytes := resp.Content + if len(bodyBytes) > 0 { + if err := json.Unmarshal(bodyBytes, out.JSON503); err != nil { + return out, &runtime.ResponseDecodeError{ + StatusCode: resp.StatusCode, + ContentType: resp.Headers.Get("Content-Type"), + ContentLength: len(bodyBytes), + TargetType: "ListPersonEmploymentsErrorResponseJSON503", + Body: bodyBytes, + Err: err, + } + } + } + return out, runtime.NewClientAPIError(fmt.Errorf("API error (status %d)", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + default: + return out, runtime.NewClientAPIError(fmt.Errorf("unexpected status code: %d", resp.StatusCode), runtime.WithStatusCode(resp.StatusCode)) + } +} + // GetPersonStructuredProfile Get a person's current structured profile func (c *Client) GetPersonStructuredProfileWithResponse(ctx context.Context, options *GetPersonStructuredProfileRequestOptions, reqEditors ...runtime.RequestEditorFn) (*GetPersonStructuredProfileResp, error) { var err error diff --git a/pkg/client/generated/enums.go b/pkg/client/generated/enums.go index ae8281e51..bbddc5e00 100644 --- a/pkg/client/generated/enums.go +++ b/pkg/client/generated/enums.go @@ -63,14 +63,14 @@ func (c CreateAttributeDefinitionRequestCardinality) Validate() error { type CreateAttributeDefinitionRequestObjectType string const ( - CreateAttributeDefinitionRequestObjectTypePerson CreateAttributeDefinitionRequestObjectType = "person" - Organization CreateAttributeDefinitionRequestObjectType = "organization" + CreateAttributeDefinitionRequestObjectTypeOrganization CreateAttributeDefinitionRequestObjectType = "organization" + CreateAttributeDefinitionRequestObjectTypePerson CreateAttributeDefinitionRequestObjectType = "person" ) // Validate checks if the CreateAttributeDefinitionRequestObjectType value is valid func (c CreateAttributeDefinitionRequestObjectType) Validate() error { switch c { - case CreateAttributeDefinitionRequestObjectTypePerson, Organization: + case CreateAttributeDefinitionRequestObjectTypeOrganization, CreateAttributeDefinitionRequestObjectTypePerson: return nil default: return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid CreateAttributeDefinitionRequestObjectType value, got: %v", c)) @@ -134,6 +134,28 @@ func (d DiscoverEventType) Validate() error { } } +type EmploymentBodySource string + +const ( + ArchiveObservation EmploymentBodySource = "archive_observation" + CarddavImport EmploymentBodySource = "carddav_import" + Enrichment EmploymentBodySource = "enrichment" + Extraction EmploymentBodySource = "extraction" + System EmploymentBodySource = "system" + User EmploymentBodySource = "user" + VcardImport EmploymentBodySource = "vcard_import" +) + +// Validate checks if the EmploymentBodySource value is valid +func (e EmploymentBodySource) Validate() error { + switch e { + case ArchiveObservation, CarddavImport, Enrichment, Extraction, System, User, VcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid EmploymentBodySource value, got: %v", e)) + } +} + type ExploreCacheUnavailableResponseReadiness string const ( @@ -460,6 +482,265 @@ func (m MeetingImportResponseStatus) Validate() error { } } +type OrganizationAddressBodySource string + +const ( + OrganizationAddressBodySourceArchiveObservation OrganizationAddressBodySource = "archive_observation" + OrganizationAddressBodySourceCarddavImport OrganizationAddressBodySource = "carddav_import" + OrganizationAddressBodySourceEnrichment OrganizationAddressBodySource = "enrichment" + OrganizationAddressBodySourceExtraction OrganizationAddressBodySource = "extraction" + OrganizationAddressBodySourceSystem OrganizationAddressBodySource = "system" + OrganizationAddressBodySourceUser OrganizationAddressBodySource = "user" + OrganizationAddressBodySourceVcardImport OrganizationAddressBodySource = "vcard_import" +) + +// Validate checks if the OrganizationAddressBodySource value is valid +func (o OrganizationAddressBodySource) Validate() error { + switch o { + case OrganizationAddressBodySourceArchiveObservation, OrganizationAddressBodySourceCarddavImport, OrganizationAddressBodySourceEnrichment, OrganizationAddressBodySourceExtraction, OrganizationAddressBodySourceSystem, OrganizationAddressBodySourceUser, OrganizationAddressBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationAddressBodySource value, got: %v", o)) + } +} + +type OrganizationBodyKind string + +const ( + Company OrganizationBodyKind = "company" + Government OrganizationBodyKind = "government" + Household OrganizationBodyKind = "household" + Nonprofit OrganizationBodyKind = "nonprofit" + Other OrganizationBodyKind = "other" + School OrganizationBodyKind = "school" +) + +// Validate checks if the OrganizationBodyKind value is valid +func (o OrganizationBodyKind) Validate() error { + switch o { + case Company, Government, Household, Nonprofit, Other, School: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationBodyKind value, got: %v", o)) + } +} + +type OrganizationCategoryBodySource string + +const ( + OrganizationCategoryBodySourceArchiveObservation OrganizationCategoryBodySource = "archive_observation" + OrganizationCategoryBodySourceCarddavImport OrganizationCategoryBodySource = "carddav_import" + OrganizationCategoryBodySourceEnrichment OrganizationCategoryBodySource = "enrichment" + OrganizationCategoryBodySourceExtraction OrganizationCategoryBodySource = "extraction" + OrganizationCategoryBodySourceSystem OrganizationCategoryBodySource = "system" + OrganizationCategoryBodySourceUser OrganizationCategoryBodySource = "user" + OrganizationCategoryBodySourceVcardImport OrganizationCategoryBodySource = "vcard_import" +) + +// Validate checks if the OrganizationCategoryBodySource value is valid +func (o OrganizationCategoryBodySource) Validate() error { + switch o { + case OrganizationCategoryBodySourceArchiveObservation, OrganizationCategoryBodySourceCarddavImport, OrganizationCategoryBodySourceEnrichment, OrganizationCategoryBodySourceExtraction, OrganizationCategoryBodySourceSystem, OrganizationCategoryBodySourceUser, OrganizationCategoryBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationCategoryBodySource value, got: %v", o)) + } +} + +type OrganizationContactPointBodyContactKind string + +const ( + Calendar OrganizationContactPointBodyContactKind = "calendar" + ContactURI OrganizationContactPointBodyContactKind = "contact_uri" + Email OrganizationContactPointBodyContactKind = "email" + Impp OrganizationContactPointBodyContactKind = "impp" + Language OrganizationContactPointBodyContactKind = "language" + OrgDirectory OrganizationContactPointBodyContactKind = "org_directory" + Phone OrganizationContactPointBodyContactKind = "phone" + Social OrganizationContactPointBodyContactKind = "social" + URL OrganizationContactPointBodyContactKind = "url" + Username OrganizationContactPointBodyContactKind = "username" +) + +// Validate checks if the OrganizationContactPointBodyContactKind value is valid +func (o OrganizationContactPointBodyContactKind) Validate() error { + switch o { + case Calendar, ContactURI, Email, Impp, Language, OrgDirectory, Phone, Social, URL, Username: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationContactPointBodyContactKind value, got: %v", o)) + } +} + +type OrganizationContactPointBodySource string + +const ( + OrganizationContactPointBodySourceArchiveObservation OrganizationContactPointBodySource = "archive_observation" + OrganizationContactPointBodySourceCarddavImport OrganizationContactPointBodySource = "carddav_import" + OrganizationContactPointBodySourceEnrichment OrganizationContactPointBodySource = "enrichment" + OrganizationContactPointBodySourceExtraction OrganizationContactPointBodySource = "extraction" + OrganizationContactPointBodySourceSystem OrganizationContactPointBodySource = "system" + OrganizationContactPointBodySourceUser OrganizationContactPointBodySource = "user" + OrganizationContactPointBodySourceVcardImport OrganizationContactPointBodySource = "vcard_import" +) + +// Validate checks if the OrganizationContactPointBodySource value is valid +func (o OrganizationContactPointBodySource) Validate() error { + switch o { + case OrganizationContactPointBodySourceArchiveObservation, OrganizationContactPointBodySourceCarddavImport, OrganizationContactPointBodySourceEnrichment, OrganizationContactPointBodySourceExtraction, OrganizationContactPointBodySourceSystem, OrganizationContactPointBodySourceUser, OrganizationContactPointBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationContactPointBodySource value, got: %v", o)) + } +} + +type OrganizationCreateBodyKind string + +const ( + OrganizationCreateBodyKindCompany OrganizationCreateBodyKind = "company" + OrganizationCreateBodyKindGovernment OrganizationCreateBodyKind = "government" + OrganizationCreateBodyKindHousehold OrganizationCreateBodyKind = "household" + OrganizationCreateBodyKindNonprofit OrganizationCreateBodyKind = "nonprofit" + OrganizationCreateBodyKindOther OrganizationCreateBodyKind = "other" + OrganizationCreateBodyKindSchool OrganizationCreateBodyKind = "school" +) + +// Validate checks if the OrganizationCreateBodyKind value is valid +func (o OrganizationCreateBodyKind) Validate() error { + switch o { + case OrganizationCreateBodyKindCompany, OrganizationCreateBodyKindGovernment, OrganizationCreateBodyKindHousehold, OrganizationCreateBodyKindNonprofit, OrganizationCreateBodyKindOther, OrganizationCreateBodyKindSchool: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationCreateBodyKind value, got: %v", o)) + } +} + +type OrganizationIdentifierBodyIdentifierKind string + +const ( + Domain OrganizationIdentifierBodyIdentifierKind = "domain" + Duns OrganizationIdentifierBodyIdentifierKind = "duns" + Linkedin OrganizationIdentifierBodyIdentifierKind = "linkedin" + OrganizationIdentifierBodyIdentifierKindOther OrganizationIdentifierBodyIdentifierKind = "other" + Registry OrganizationIdentifierBodyIdentifierKind = "registry" + TaxID OrganizationIdentifierBodyIdentifierKind = "tax_id" +) + +// Validate checks if the OrganizationIdentifierBodyIdentifierKind value is valid +func (o OrganizationIdentifierBodyIdentifierKind) Validate() error { + switch o { + case Domain, Duns, Linkedin, OrganizationIdentifierBodyIdentifierKindOther, Registry, TaxID: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationIdentifierBodyIdentifierKind value, got: %v", o)) + } +} + +type OrganizationIdentifierBodySource string + +const ( + OrganizationIdentifierBodySourceArchiveObservation OrganizationIdentifierBodySource = "archive_observation" + OrganizationIdentifierBodySourceCarddavImport OrganizationIdentifierBodySource = "carddav_import" + OrganizationIdentifierBodySourceEnrichment OrganizationIdentifierBodySource = "enrichment" + OrganizationIdentifierBodySourceExtraction OrganizationIdentifierBodySource = "extraction" + OrganizationIdentifierBodySourceSystem OrganizationIdentifierBodySource = "system" + OrganizationIdentifierBodySourceUser OrganizationIdentifierBodySource = "user" + OrganizationIdentifierBodySourceVcardImport OrganizationIdentifierBodySource = "vcard_import" +) + +// Validate checks if the OrganizationIdentifierBodySource value is valid +func (o OrganizationIdentifierBodySource) Validate() error { + switch o { + case OrganizationIdentifierBodySourceArchiveObservation, OrganizationIdentifierBodySourceCarddavImport, OrganizationIdentifierBodySourceEnrichment, OrganizationIdentifierBodySourceExtraction, OrganizationIdentifierBodySourceSystem, OrganizationIdentifierBodySourceUser, OrganizationIdentifierBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationIdentifierBodySource value, got: %v", o)) + } +} + +type OrganizationMediaBodyMediaKind string + +const ( + Key OrganizationMediaBodyMediaKind = "key" + Logo OrganizationMediaBodyMediaKind = "logo" + Photo OrganizationMediaBodyMediaKind = "photo" + Sound OrganizationMediaBodyMediaKind = "sound" +) + +// Validate checks if the OrganizationMediaBodyMediaKind value is valid +func (o OrganizationMediaBodyMediaKind) Validate() error { + switch o { + case Key, Logo, Photo, Sound: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationMediaBodyMediaKind value, got: %v", o)) + } +} + +type OrganizationMediaBodySource string + +const ( + OrganizationMediaBodySourceArchiveObservation OrganizationMediaBodySource = "archive_observation" + OrganizationMediaBodySourceCarddavImport OrganizationMediaBodySource = "carddav_import" + OrganizationMediaBodySourceEnrichment OrganizationMediaBodySource = "enrichment" + OrganizationMediaBodySourceExtraction OrganizationMediaBodySource = "extraction" + OrganizationMediaBodySourceSystem OrganizationMediaBodySource = "system" + OrganizationMediaBodySourceUser OrganizationMediaBodySource = "user" + OrganizationMediaBodySourceVcardImport OrganizationMediaBodySource = "vcard_import" +) + +// Validate checks if the OrganizationMediaBodySource value is valid +func (o OrganizationMediaBodySource) Validate() error { + switch o { + case OrganizationMediaBodySourceArchiveObservation, OrganizationMediaBodySourceCarddavImport, OrganizationMediaBodySourceEnrichment, OrganizationMediaBodySourceExtraction, OrganizationMediaBodySourceSystem, OrganizationMediaBodySourceUser, OrganizationMediaBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationMediaBodySource value, got: %v", o)) + } +} + +type OrganizationNameBodyNameKind string + +const ( + Abbreviation OrganizationNameBodyNameKind = "abbreviation" + Alias OrganizationNameBodyNameKind = "alias" + Former OrganizationNameBodyNameKind = "former" + Legal OrganizationNameBodyNameKind = "legal" + Sort OrganizationNameBodyNameKind = "sort" +) + +// Validate checks if the OrganizationNameBodyNameKind value is valid +func (o OrganizationNameBodyNameKind) Validate() error { + switch o { + case Abbreviation, Alias, Former, Legal, Sort: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationNameBodyNameKind value, got: %v", o)) + } +} + +type OrganizationNameBodySource string + +const ( + OrganizationNameBodySourceArchiveObservation OrganizationNameBodySource = "archive_observation" + OrganizationNameBodySourceCarddavImport OrganizationNameBodySource = "carddav_import" + OrganizationNameBodySourceEnrichment OrganizationNameBodySource = "enrichment" + OrganizationNameBodySourceExtraction OrganizationNameBodySource = "extraction" + OrganizationNameBodySourceSystem OrganizationNameBodySource = "system" + OrganizationNameBodySourceUser OrganizationNameBodySource = "user" + OrganizationNameBodySourceVcardImport OrganizationNameBodySource = "vcard_import" +) + +// Validate checks if the OrganizationNameBodySource value is valid +func (o OrganizationNameBodySource) Validate() error { + switch o { + case OrganizationNameBodySourceArchiveObservation, OrganizationNameBodySourceCarddavImport, OrganizationNameBodySourceEnrichment, OrganizationNameBodySourceExtraction, OrganizationNameBodySourceSystem, OrganizationNameBodySourceUser, OrganizationNameBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid OrganizationNameBodySource value, got: %v", o)) + } +} + type RemoveResultCacheState string const ( @@ -477,6 +758,23 @@ func (r RemoveResultCacheState) Validate() error { } } +type ResolveOrganizationDuplicateSuggestionBodyStatus string + +const ( + Accepted ResolveOrganizationDuplicateSuggestionBodyStatus = "accepted" + Rejected ResolveOrganizationDuplicateSuggestionBodyStatus = "rejected" +) + +// Validate checks if the ResolveOrganizationDuplicateSuggestionBodyStatus value is valid +func (r ResolveOrganizationDuplicateSuggestionBodyStatus) Validate() error { + switch r { + case Accepted, Rejected: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid ResolveOrganizationDuplicateSuggestionBodyStatus value, got: %v", r)) + } +} + type SavedViewSortDirection string const ( @@ -586,22 +884,44 @@ func (s SessionStatusAuthMode) Validate() error { } } +type SetOrganizationAttributeBodySource string + +const ( + SetOrganizationAttributeBodySourceArchiveObservation SetOrganizationAttributeBodySource = "archive_observation" + SetOrganizationAttributeBodySourceCarddavImport SetOrganizationAttributeBodySource = "carddav_import" + SetOrganizationAttributeBodySourceEnrichment SetOrganizationAttributeBodySource = "enrichment" + SetOrganizationAttributeBodySourceExtraction SetOrganizationAttributeBodySource = "extraction" + SetOrganizationAttributeBodySourceSystem SetOrganizationAttributeBodySource = "system" + SetOrganizationAttributeBodySourceUser SetOrganizationAttributeBodySource = "user" + SetOrganizationAttributeBodySourceVcardImport SetOrganizationAttributeBodySource = "vcard_import" +) + +// Validate checks if the SetOrganizationAttributeBodySource value is valid +func (s SetOrganizationAttributeBodySource) Validate() error { + switch s { + case SetOrganizationAttributeBodySourceArchiveObservation, SetOrganizationAttributeBodySourceCarddavImport, SetOrganizationAttributeBodySourceEnrichment, SetOrganizationAttributeBodySourceExtraction, SetOrganizationAttributeBodySourceSystem, SetOrganizationAttributeBodySourceUser, SetOrganizationAttributeBodySourceVcardImport: + return nil + default: + return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid SetOrganizationAttributeBodySource value, got: %v", s)) + } +} + type SetPersonAttributeRequestSource string const ( - ArchiveObservation SetPersonAttributeRequestSource = "archive_observation" - CarddavImport SetPersonAttributeRequestSource = "carddav_import" - Enrichment SetPersonAttributeRequestSource = "enrichment" - Extraction SetPersonAttributeRequestSource = "extraction" - System SetPersonAttributeRequestSource = "system" - User SetPersonAttributeRequestSource = "user" - VcardImport SetPersonAttributeRequestSource = "vcard_import" + SetPersonAttributeRequestSourceArchiveObservation SetPersonAttributeRequestSource = "archive_observation" + SetPersonAttributeRequestSourceCarddavImport SetPersonAttributeRequestSource = "carddav_import" + SetPersonAttributeRequestSourceEnrichment SetPersonAttributeRequestSource = "enrichment" + SetPersonAttributeRequestSourceExtraction SetPersonAttributeRequestSource = "extraction" + SetPersonAttributeRequestSourceSystem SetPersonAttributeRequestSource = "system" + SetPersonAttributeRequestSourceUser SetPersonAttributeRequestSource = "user" + SetPersonAttributeRequestSourceVcardImport SetPersonAttributeRequestSource = "vcard_import" ) // Validate checks if the SetPersonAttributeRequestSource value is valid func (s SetPersonAttributeRequestSource) Validate() error { switch s { - case ArchiveObservation, CarddavImport, Enrichment, Extraction, System, User, VcardImport: + case SetPersonAttributeRequestSourceArchiveObservation, SetPersonAttributeRequestSourceCarddavImport, SetPersonAttributeRequestSourceEnrichment, SetPersonAttributeRequestSourceExtraction, SetPersonAttributeRequestSourceSystem, SetPersonAttributeRequestSourceUser, SetPersonAttributeRequestSourceVcardImport: return nil default: return runtime.NewValidationErrorsFromString("Enum", fmt.Sprintf("must be a valid SetPersonAttributeRequestSource value, got: %v", s)) diff --git a/pkg/client/generated/headers.go b/pkg/client/generated/headers.go index b3366635a..a2cff40b1 100644 --- a/pkg/client/generated/headers.go +++ b/pkg/client/generated/headers.go @@ -24,6 +24,42 @@ func (p PatchAttributeDefinitionHeaders) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(p)) } +type DeleteEmploymentHeaders struct { + // IfMatch Strong ETag returned by the latest employment read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (d DeleteEmploymentHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(d)) +} + +type PatchEmploymentHeaders struct { + // IfMatch Strong ETag returned by the latest employment read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (p PatchEmploymentHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(p)) +} + +type EndEmploymentHeaders struct { + // IfMatch Strong ETag returned by the latest employment read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (e EndEmploymentHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(e)) +} + +type SetPrimaryEmploymentHeaders struct { + // IfMatch Strong ETag returned by the latest employment read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (s SetPrimaryEmploymentHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(s)) +} + type CreateOrLinkMessageTaskHeaders struct { // XRequestID Browser-generated retry-stable request ID XRequestID string `json:"X-Request-Id" validate:"required"` @@ -33,6 +69,42 @@ func (c CreateOrLinkMessageTaskHeaders) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(c)) } +type DeleteOrganizationHeaders struct { + // IfMatch Strong ETag returned by the latest organization read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (d DeleteOrganizationHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(d)) +} + +type PatchOrganizationHeaders struct { + // IfMatch Strong ETag returned by the latest organization read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (p PatchOrganizationHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(p)) +} + +type MergeOrganizationHeaders struct { + // IfMatch Strong ETag returned by the latest organization read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (m MergeOrganizationHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(m)) +} + +type PutOrganizationProfileHeaders struct { + // IfMatch Strong ETag returned by the latest organization read + IfMatch string `json:"If-Match" validate:"required"` +} + +func (p PutOrganizationProfileHeaders) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(p)) +} + type DeletePersonHeaders struct { // IfMatch Strong ETag returned by the latest person profile read. Must be the exact single tag from that read; the RFC 7232 forms `*` and comma-separated tag lists are not supported. IfMatch string `json:"If-Match" validate:"required"` diff --git a/pkg/client/generated/paths.go b/pkg/client/generated/paths.go index 0619e6271..76cefeab0 100644 --- a/pkg/client/generated/paths.go +++ b/pkg/client/generated/paths.go @@ -130,6 +130,31 @@ func (g GetDomainTimelinePath) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(g)) } +type DeleteEmploymentPath struct { + // ID Employment ID + ID int64 `json:"id"` +} + +type GetEmploymentPath struct { + // ID Employment ID + ID int64 `json:"id"` +} + +type PatchEmploymentPath struct { + // ID Employment ID + ID int64 `json:"id"` +} + +type EndEmploymentPath struct { + // ID Employment ID + ID int64 `json:"id"` +} + +type SetPrimaryEmploymentPath struct { + // ID Employment ID + ID int64 `json:"id"` +} + type GetFilePath struct { // ID File attachment ID ID int64 `json:"id"` @@ -172,6 +197,56 @@ func (u UnlinkMessageTaskPath) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(u)) } +type ResolveOrganizationDuplicateSuggestionPath struct { + // ID Duplicate suggestion ID + ID int64 `json:"id"` +} + +type DeleteOrganizationPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type GetOrganizationPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type PatchOrganizationPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type ListOrganizationAttributesPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type SetOrganizationAttributePath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type ListOrganizationEmploymentsPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type GetOrganizationHistoryPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type MergeOrganizationPath struct { + // ID Organization ID + ID int64 `json:"id"` +} + +type PutOrganizationProfilePath struct { + // ID Organization ID + ID int64 `json:"id"` +} + type GetPersonPath struct { // ID Durable participant ID ID int64 `json:"id"` @@ -236,6 +311,11 @@ func (s SetPersonAttributePath) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(s)) } +type ListPersonEmploymentsPath struct { + // ID Person ID + ID int64 `json:"id"` +} + type GetPersonStructuredProfilePath struct { // ID Durable person ID ID int64 `json:"id"` diff --git a/pkg/client/generated/payloads.go b/pkg/client/generated/payloads.go index 767121ce2..417798358 100644 --- a/pkg/client/generated/payloads.go +++ b/pkg/client/generated/payloads.go @@ -60,6 +60,10 @@ type GetDomainContextSummaryBody = ExploreHTTPRequest type GetDomainTimelineBody = ExploreHTTPRequest +type CreateEmploymentBody = EmploymentBody + +type PatchEmploymentBody = EmploymentBody + type ExploreBody = ExploreHTTPRequest type ListExploreFilesBody = ExploreFilesHTTPRequest @@ -82,6 +86,12 @@ type ImportMeetingBody = MeetingImportRequest type CreateOrLinkMessageTaskBody = TaskLinkMutationRequest +type CreateOrganizationBody = OrganizationCreateBody + +type PatchOrganizationBody = OrganizationBody + +type PutOrganizationProfileBody = OrganizationProfileBody + type SearchPeopleBody = IdentitySearchHTTPRequest type SearchPersonFilesBody = FileSearchHTTPRequest diff --git a/pkg/client/generated/queries.go b/pkg/client/generated/queries.go index afd9aa177..ae29b1ba8 100644 --- a/pkg/client/generated/queries.go +++ b/pkg/client/generated/queries.go @@ -448,6 +448,50 @@ func (g GetMessageInlinePartQuery) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(g)) } +type ListOrganizationsQuery struct { + // Limit Maximum results + Limit *int64 `json:"limit,omitempty"` + + // Offset Results to skip + Offset *int64 `json:"offset,omitempty"` + + // IncludeRetired Include retired organizations + IncludeRetired *bool `json:"include_retired,omitempty"` + + // Q Normalized-name search + Q *string `json:"q,omitempty"` +} + +type ListOrganizationDuplicateSuggestionsQuery struct { + // Status Filter by status + Status *string `json:"status,omitempty"` + + // Limit Maximum results + Limit *int64 `json:"limit,omitempty"` + + // Offset Results to skip + Offset *int64 `json:"offset,omitempty"` +} + +type ListOrganizationAttributesQuery struct { + // IncludeSuperseded Include superseded values + IncludeSuperseded *bool `json:"include_superseded,omitempty"` + + // DefinitionSlug Restrict to one definition + DefinitionSlug *string `json:"definition_slug,omitempty"` +} + +type ListOrganizationEmploymentsQuery struct { + // CurrentOnly Only current employments + CurrentOnly *bool `json:"current_only,omitempty"` + + // Limit Maximum results + Limit *int64 `json:"limit,omitempty"` + + // Offset Results to skip + Offset *int64 `json:"offset,omitempty"` +} + type ListPersonAttributesQuery struct { // History Include superseded values History *bool `json:"history,omitempty"` @@ -472,6 +516,17 @@ type SetPersonAttributeQuery struct { DryRun *bool `json:"dry_run,omitempty"` } +type ListPersonEmploymentsQuery struct { + // CurrentOnly Only current employments + CurrentOnly *bool `json:"current_only,omitempty"` + + // Limit Maximum results + Limit *int64 `json:"limit,omitempty"` + + // Offset Results to skip + Offset *int64 `json:"offset,omitempty"` +} + type SearchMessagesQuery struct { // Q Search query Q string `json:"q" validate:"required"` diff --git a/pkg/client/generated/responses.go b/pkg/client/generated/responses.go index cfe83d31f..2e21db800 100644 --- a/pkg/client/generated/responses.go +++ b/pkg/client/generated/responses.go @@ -575,6 +575,68 @@ func (g *GetDomainTimelineErrorResponseJSON503) UnmarshalJSON(data []byte) error return nil } +type CreateEmploymentResponse = Employment + +type CreateEmploymentErrorResponse = ErrorResponse + +type CreateEmploymentErrorResponseJSON = ErrorResponse + +type CreateEmploymentErrorResponseJSON503 = ErrorResponse + +type DeleteEmploymentErrorResponse = ErrorResponse + +type DeleteEmploymentErrorResponseJSON = ErrorResponse + +type DeleteEmploymentErrorResponseJSON409 = ErrorResponse + +type DeleteEmploymentErrorResponseJSON428 = ErrorResponse + +type DeleteEmploymentErrorResponseJSON503 = ErrorResponse + +type GetEmploymentResponse = Employment + +type GetEmploymentErrorResponse = ErrorResponse + +type GetEmploymentErrorResponseJSON = ErrorResponse + +type GetEmploymentErrorResponseJSON503 = ErrorResponse + +type PatchEmploymentResponse = Employment + +type PatchEmploymentErrorResponse = ErrorResponse + +type PatchEmploymentErrorResponseJSON = ErrorResponse + +type PatchEmploymentErrorResponseJSON409 = ErrorResponse + +type PatchEmploymentErrorResponseJSON428 = ErrorResponse + +type PatchEmploymentErrorResponseJSON503 = ErrorResponse + +type EndEmploymentResponse = Employment + +type EndEmploymentErrorResponse = ErrorResponse + +type EndEmploymentErrorResponseJSON = ErrorResponse + +type EndEmploymentErrorResponseJSON409 = ErrorResponse + +type EndEmploymentErrorResponseJSON428 = ErrorResponse + +type EndEmploymentErrorResponseJSON503 = ErrorResponse + +type SetPrimaryEmploymentResponse = Employment + +type SetPrimaryEmploymentErrorResponse = ErrorResponse + +type SetPrimaryEmploymentErrorResponseJSON = ErrorResponse + +type SetPrimaryEmploymentErrorResponseJSON409 = ErrorResponse + +type SetPrimaryEmploymentErrorResponseJSON428 = ErrorResponse + +type SetPrimaryEmploymentErrorResponseJSON503 = ErrorResponse + type ExploreResponse = ExploreHTTPResponse type ExploreErrorResponse = ErrorResponse @@ -985,6 +1047,128 @@ type UnlinkMessageTaskResponse = TaskLinkMutationResponse type UnlinkMessageTaskErrorResponse = ErrorResponse +type ListOrganizationsResponse = OrganizationsResponse + +type ListOrganizationsErrorResponse = ErrorResponse + +type ListOrganizationsErrorResponseJSON = ErrorResponse + +type CreateOrganizationResponse = Organization + +type CreateOrganizationErrorResponse = ErrorResponse + +type CreateOrganizationErrorResponseJSON = ErrorResponse + +type ListOrganizationDuplicateSuggestionsResponse = OrganizationDuplicateSuggestionsResponse + +type ListOrganizationDuplicateSuggestionsErrorResponse = ErrorResponse + +type ListOrganizationDuplicateSuggestionsErrorResponseJSON = ErrorResponse + +type RefreshOrganizationDuplicateSuggestionsResponseJSON = RefreshOrganizationDuplicateSuggestionsResponse + +type RefreshOrganizationDuplicateSuggestionsErrorResponse = ErrorResponse + +type ResolveOrganizationDuplicateSuggestionResponse = OrganizationDuplicateSuggestion + +type ResolveOrganizationDuplicateSuggestionErrorResponse = ErrorResponse + +type ResolveOrganizationDuplicateSuggestionErrorResponseJSON = ErrorResponse + +type ResolveOrganizationDuplicateSuggestionErrorResponseJSON409 = ErrorResponse + +type ResolveOrganizationDuplicateSuggestionErrorResponseJSON503 = ErrorResponse + +type DeleteOrganizationErrorResponse = ErrorResponse + +type DeleteOrganizationErrorResponseJSON = ErrorResponse + +type DeleteOrganizationErrorResponseJSON409 = ErrorResponse + +type DeleteOrganizationErrorResponseJSON428 = ErrorResponse + +type DeleteOrganizationErrorResponseJSON503 = ErrorResponse + +type GetOrganizationResponse = OrganizationProfile + +type GetOrganizationErrorResponse = ErrorResponse + +type GetOrganizationErrorResponseJSON = ErrorResponse + +type GetOrganizationErrorResponseJSON503 = ErrorResponse + +type PatchOrganizationResponse = Organization + +type PatchOrganizationErrorResponse = ErrorResponse + +type PatchOrganizationErrorResponseJSON = ErrorResponse + +type PatchOrganizationErrorResponseJSON409 = ErrorResponse + +type PatchOrganizationErrorResponseJSON428 = ErrorResponse + +type PatchOrganizationErrorResponseJSON503 = ErrorResponse + +type ListOrganizationAttributesResponse = OrganizationAttributesResponse + +type ListOrganizationAttributesErrorResponse = ErrorResponse + +type ListOrganizationAttributesErrorResponseJSON = ErrorResponse + +type ListOrganizationAttributesErrorResponseJSON503 = ErrorResponse + +type SetOrganizationAttributeResponse = OrganizationAttributeWrite + +type SetOrganizationAttributeResponseJSON = OrganizationAttributeWrite + +type SetOrganizationAttributeErrorResponse = ErrorResponse + +type SetOrganizationAttributeErrorResponseJSON = ErrorResponse + +type SetOrganizationAttributeErrorResponseJSON409 = ErrorResponse + +type SetOrganizationAttributeErrorResponseJSON503 = ErrorResponse + +type ListOrganizationEmploymentsResponse = EmploymentsResponse + +type ListOrganizationEmploymentsErrorResponse = ErrorResponse + +type ListOrganizationEmploymentsErrorResponseJSON = ErrorResponse + +type ListOrganizationEmploymentsErrorResponseJSON503 = ErrorResponse + +type GetOrganizationHistoryResponse = OrganizationProfile + +type GetOrganizationHistoryErrorResponse = ErrorResponse + +type GetOrganizationHistoryErrorResponseJSON = ErrorResponse + +type GetOrganizationHistoryErrorResponseJSON503 = ErrorResponse + +type MergeOrganizationResponse = Organization + +type MergeOrganizationErrorResponse = ErrorResponse + +type MergeOrganizationErrorResponseJSON = ErrorResponse + +type MergeOrganizationErrorResponseJSON409 = ErrorResponse + +type MergeOrganizationErrorResponseJSON428 = ErrorResponse + +type MergeOrganizationErrorResponseJSON503 = ErrorResponse + +type PutOrganizationProfileResponse = OrganizationProfile + +type PutOrganizationProfileErrorResponse = ErrorResponse + +type PutOrganizationProfileErrorResponseJSON = ErrorResponse + +type PutOrganizationProfileErrorResponseJSON409 = ErrorResponse + +type PutOrganizationProfileErrorResponseJSON428 = ErrorResponse + +type PutOrganizationProfileErrorResponseJSON503 = ErrorResponse + type SearchPeopleResponse = PersonSearchHTTPResponse type SearchPeopleErrorResponse = ErrorResponse @@ -1233,6 +1417,14 @@ type SetPersonAttributeErrorResponseJSON409 = ErrorResponse type SetPersonAttributeErrorResponseJSON503 = ErrorResponse +type ListPersonEmploymentsResponse = EmploymentsResponse + +type ListPersonEmploymentsErrorResponse = ErrorResponse + +type ListPersonEmploymentsErrorResponseJSON = ErrorResponse + +type ListPersonEmploymentsErrorResponseJSON503 = ErrorResponse + type GetPersonStructuredProfileResponse = StructuredPersonProfile type GetPersonStructuredProfileErrorResponse = ErrorResponse @@ -2103,6 +2295,99 @@ type GetDomainTimelineResp struct { JSON503 *GetDomainTimelineErrorResponseJSON503 } +type CreateEmploymentResp201Headers struct { + ETag string `header:"ETag"` + Location string `header:"Location"` +} + +type CreateEmploymentResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON201 *CreateEmploymentResponse + Headers201 *CreateEmploymentResp201Headers + JSON400 *CreateEmploymentErrorResponse + JSON409 *CreateEmploymentErrorResponseJSON + JSON503 *CreateEmploymentErrorResponseJSON503 +} + +type DeleteEmploymentResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON400 *DeleteEmploymentErrorResponse + JSON404 *DeleteEmploymentErrorResponseJSON + JSON409 *DeleteEmploymentErrorResponseJSON409 + JSON428 *DeleteEmploymentErrorResponseJSON428 + JSON503 *DeleteEmploymentErrorResponseJSON503 +} + +type GetEmploymentResp200Headers struct { + ETag string `header:"ETag"` +} + +type GetEmploymentResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *GetEmploymentResponse + Headers200 *GetEmploymentResp200Headers + JSON400 *GetEmploymentErrorResponse + JSON404 *GetEmploymentErrorResponseJSON + JSON503 *GetEmploymentErrorResponseJSON503 +} + +type PatchEmploymentResp200Headers struct { + ETag string `header:"ETag"` +} + +type PatchEmploymentResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *PatchEmploymentResponse + Headers200 *PatchEmploymentResp200Headers + JSON400 *PatchEmploymentErrorResponse + JSON404 *PatchEmploymentErrorResponseJSON + JSON409 *PatchEmploymentErrorResponseJSON409 + JSON428 *PatchEmploymentErrorResponseJSON428 + JSON503 *PatchEmploymentErrorResponseJSON503 +} + +type EndEmploymentResp200Headers struct { + ETag string `header:"ETag"` +} + +type EndEmploymentResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *EndEmploymentResponse + Headers200 *EndEmploymentResp200Headers + JSON400 *EndEmploymentErrorResponse + JSON404 *EndEmploymentErrorResponseJSON + JSON409 *EndEmploymentErrorResponseJSON409 + JSON428 *EndEmploymentErrorResponseJSON428 + JSON503 *EndEmploymentErrorResponseJSON503 +} + +type SetPrimaryEmploymentResp200Headers struct { + ETag string `header:"ETag"` +} + +type SetPrimaryEmploymentResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *SetPrimaryEmploymentResponse + Headers200 *SetPrimaryEmploymentResp200Headers + JSON400 *SetPrimaryEmploymentErrorResponse + JSON404 *SetPrimaryEmploymentErrorResponseJSON + JSON409 *SetPrimaryEmploymentErrorResponseJSON409 + JSON428 *SetPrimaryEmploymentErrorResponseJSON428 + JSON503 *SetPrimaryEmploymentErrorResponseJSON503 +} + type ExploreResp struct { HTTPResponse *http.Response Body []byte @@ -2315,6 +2600,182 @@ type UnlinkMessageTaskResp struct { JSON200 *UnlinkMessageTaskResponse } +type ListOrganizationsResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *ListOrganizationsResponse + JSON400 *ListOrganizationsErrorResponse + JSON503 *ListOrganizationsErrorResponseJSON +} + +type CreateOrganizationResp201Headers struct { + ETag string `header:"ETag"` + Location string `header:"Location"` +} + +type CreateOrganizationResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON201 *CreateOrganizationResponse + Headers201 *CreateOrganizationResp201Headers + JSON400 *CreateOrganizationErrorResponse + JSON503 *CreateOrganizationErrorResponseJSON +} + +type ListOrganizationDuplicateSuggestionsResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *ListOrganizationDuplicateSuggestionsResponse + JSON400 *ListOrganizationDuplicateSuggestionsErrorResponse + JSON503 *ListOrganizationDuplicateSuggestionsErrorResponseJSON +} + +type RefreshOrganizationDuplicateSuggestionsResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *RefreshOrganizationDuplicateSuggestionsResponseJSON + JSON503 *RefreshOrganizationDuplicateSuggestionsErrorResponse +} + +type ResolveOrganizationDuplicateSuggestionResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *ResolveOrganizationDuplicateSuggestionResponse + JSON400 *ResolveOrganizationDuplicateSuggestionErrorResponse + JSON404 *ResolveOrganizationDuplicateSuggestionErrorResponseJSON + JSON409 *ResolveOrganizationDuplicateSuggestionErrorResponseJSON409 + JSON503 *ResolveOrganizationDuplicateSuggestionErrorResponseJSON503 +} + +type DeleteOrganizationResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON400 *DeleteOrganizationErrorResponse + JSON404 *DeleteOrganizationErrorResponseJSON + JSON409 *DeleteOrganizationErrorResponseJSON409 + JSON428 *DeleteOrganizationErrorResponseJSON428 + JSON503 *DeleteOrganizationErrorResponseJSON503 +} + +type GetOrganizationResp200Headers struct { + ETag string `header:"ETag"` +} + +type GetOrganizationResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *GetOrganizationResponse + Headers200 *GetOrganizationResp200Headers + JSON400 *GetOrganizationErrorResponse + JSON404 *GetOrganizationErrorResponseJSON + JSON503 *GetOrganizationErrorResponseJSON503 +} + +type PatchOrganizationResp200Headers struct { + ETag string `header:"ETag"` +} + +type PatchOrganizationResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *PatchOrganizationResponse + Headers200 *PatchOrganizationResp200Headers + JSON400 *PatchOrganizationErrorResponse + JSON404 *PatchOrganizationErrorResponseJSON + JSON409 *PatchOrganizationErrorResponseJSON409 + JSON428 *PatchOrganizationErrorResponseJSON428 + JSON503 *PatchOrganizationErrorResponseJSON503 +} + +type ListOrganizationAttributesResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *ListOrganizationAttributesResponse + JSON400 *ListOrganizationAttributesErrorResponse + JSON404 *ListOrganizationAttributesErrorResponseJSON + JSON503 *ListOrganizationAttributesErrorResponseJSON503 +} + +type SetOrganizationAttributeResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *SetOrganizationAttributeResponse + JSON201 *SetOrganizationAttributeResponseJSON + JSON400 *SetOrganizationAttributeErrorResponse + JSON404 *SetOrganizationAttributeErrorResponseJSON + JSON409 *SetOrganizationAttributeErrorResponseJSON409 + JSON503 *SetOrganizationAttributeErrorResponseJSON503 +} + +type ListOrganizationEmploymentsResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *ListOrganizationEmploymentsResponse + JSON400 *ListOrganizationEmploymentsErrorResponse + JSON404 *ListOrganizationEmploymentsErrorResponseJSON + JSON503 *ListOrganizationEmploymentsErrorResponseJSON503 +} + +type GetOrganizationHistoryResp200Headers struct { + ETag string `header:"ETag"` +} + +type GetOrganizationHistoryResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *GetOrganizationHistoryResponse + Headers200 *GetOrganizationHistoryResp200Headers + JSON400 *GetOrganizationHistoryErrorResponse + JSON404 *GetOrganizationHistoryErrorResponseJSON + JSON503 *GetOrganizationHistoryErrorResponseJSON503 +} + +type MergeOrganizationResp200Headers struct { + ETag string `header:"ETag"` +} + +type MergeOrganizationResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *MergeOrganizationResponse + Headers200 *MergeOrganizationResp200Headers + JSON400 *MergeOrganizationErrorResponse + JSON404 *MergeOrganizationErrorResponseJSON + JSON409 *MergeOrganizationErrorResponseJSON409 + JSON428 *MergeOrganizationErrorResponseJSON428 + JSON503 *MergeOrganizationErrorResponseJSON503 +} + +type PutOrganizationProfileResp200Headers struct { + ETag string `header:"ETag"` +} + +type PutOrganizationProfileResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *PutOrganizationProfileResponse + Headers200 *PutOrganizationProfileResp200Headers + JSON400 *PutOrganizationProfileErrorResponse + JSON404 *PutOrganizationProfileErrorResponseJSON + JSON409 *PutOrganizationProfileErrorResponseJSON409 + JSON428 *PutOrganizationProfileErrorResponseJSON428 + JSON503 *PutOrganizationProfileErrorResponseJSON503 +} + type SearchPeopleResp struct { HTTPResponse *http.Response Body []byte @@ -2464,6 +2925,16 @@ type SetPersonAttributeResp struct { JSON503 *SetPersonAttributeErrorResponseJSON503 } +type ListPersonEmploymentsResp struct { + HTTPResponse *http.Response + Body []byte + StatusCode int + JSON200 *ListPersonEmploymentsResponse + JSON400 *ListPersonEmploymentsErrorResponse + JSON404 *ListPersonEmploymentsErrorResponseJSON + JSON503 *ListPersonEmploymentsErrorResponseJSON503 +} + type GetPersonStructuredProfileResp200Headers struct { ETag string `header:"ETag"` } diff --git a/pkg/client/generated/types.go b/pkg/client/generated/types.go index 76afe4371..079bd22f0 100644 --- a/pkg/client/generated/types.go +++ b/pkg/client/generated/types.go @@ -1662,6 +1662,157 @@ func (d DomainSummary) Validate() error { return errors } +type Employment struct { + AddressID *int64 `json:"address_id,omitempty"` + Confidence *float64 `json:"confidence,omitempty"` + CreatedAt time.Time `json:"created_at" validate:"required"` + Department *string `json:"department,omitempty"` + Description *string `json:"description,omitempty"` + EndDate *PartialDate `json:"end_date,omitempty"` + ID int64 `json:"id"` + IsCurrent bool `json:"is_current"` + IsPrimary bool `json:"is_primary"` + Location *string `json:"location,omitempty"` + OrganizationID int64 `json:"organization_id"` + PersonID int64 `json:"person_id"` + Revision int64 `json:"revision"` + Role *string `json:"role,omitempty"` + Source string `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + StartDate *PartialDate `json:"start_date,omitempty"` + Title *string `json:"title,omitempty"` + UpdatedAt time.Time `json:"updated_at" validate:"required"` +} + +func (e Employment) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(e.CreatedAt, "required"); err != nil { + errors = errors.Append("CreatedAt", err) + } + if e.EndDate != nil { + if v, ok := any(e.EndDate).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("EndDate", err) + } + } + } + if err := typesValidator.Var(e.Source, "required"); err != nil { + errors = errors.Append("Source", err) + } + if e.StartDate != nil { + if v, ok := any(e.StartDate).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("StartDate", err) + } + } + } + if err := typesValidator.Var(e.UpdatedAt, "required"); err != nil { + errors = errors.Append("UpdatedAt", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type EmploymentBody struct { + AddressID *int64 `json:"address_id,omitempty"` + Confidence *float64 `json:"confidence,omitempty"` + Department *string `json:"department,omitempty"` + Description *string `json:"description,omitempty"` + EndDate *string `json:"end_date,omitempty"` + IsCurrent *bool `json:"is_current,omitempty"` + IsPrimary *bool `json:"is_primary,omitempty"` + Location *string `json:"location,omitempty"` + OrganizationID int64 `json:"organization_id"` + PersonID int64 `json:"person_id"` + Role *string `json:"role,omitempty"` + Source EmploymentBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + StartDate *string `json:"start_date,omitempty"` + Title *string `json:"title,omitempty"` +} + +func (e EmploymentBody) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(e.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type EmploymentProjectionResponse struct { + Department *string `json:"department,omitempty"` + EmploymentID int64 `json:"employment_id"` + OrganizationID int64 `json:"organization_id"` + OrganizationName string `json:"organization_name" validate:"required"` + Role *string `json:"role,omitempty"` + Title *string `json:"title,omitempty"` + Vcard EmploymentVCard `json:"vcard"` +} + +func (e EmploymentProjectionResponse) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(e.OrganizationName, "required"); err != nil { + errors = errors.Append("OrganizationName", err) + } + if v, ok := any(e.Vcard).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Vcard", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type EmploymentVCard struct { + Org []string `json:"org,omitempty"` + Role *string `json:"role,omitempty"` + Title *string `json:"title,omitempty"` +} + +type EmploymentsResponse struct { + Employments []Employment `json:"employments,omitempty" validate:"required"` + Projection *EmploymentProjectionResponse `json:"projection,omitempty"` +} + +func (e EmploymentsResponse) Validate() error { + var errors runtime.ValidationErrors + for i, item := range e.Employments { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Employments[%d]", i), err) + } + } + } + if e.Projection != nil { + if v, ok := any(e.Projection).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Projection", err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type EndEmploymentBody struct { + EndDate string `json:"end_date" validate:"required"` +} + +func (e EndEmploymentBody) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(e)) +} + type EntryRow struct { AnchorMessageID *int64 `json:"anchor_message_id,omitempty"` AttachmentCount int64 `json:"attachment_count"` @@ -3049,20 +3200,734 @@ func (m Meeting) Validate() error { } } } - if err := typesValidator.Var(m.StartedAt, "required"); err != nil { - errors = errors.Append("StartedAt", err) - } - if m.Title != nil { - if err := typesValidator.Var(m.Title, "omitempty,max=4096"); err != nil { - errors = errors.Append("Title", err) - } + if err := typesValidator.Var(m.StartedAt, "required"); err != nil { + errors = errors.Append("StartedAt", err) + } + if m.Title != nil { + if err := typesValidator.Var(m.Title, "omitempty,max=4096"); err != nil { + errors = errors.Append("Title", err) + } + } + for i, item := range m.TranscriptSegments { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("TranscriptSegments[%d]", i), err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type MeetingImportRequest struct { + Meeting Meeting `json:"meeting"` + Source Source `json:"source"` +} + +func (m MeetingImportRequest) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(m.Meeting).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Meeting", err) + } + } + if v, ok := any(m.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type MeetingImportResponse struct { + MessageID int64 `json:"message_id"` + SourceID int64 `json:"source_id"` + SourceMessageID string `json:"source_message_id" validate:"required"` + Status MeetingImportResponseStatus `json:"status" validate:"required"` +} + +func (m MeetingImportResponse) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(m.SourceMessageID, "required"); err != nil { + errors = errors.Append("SourceMessageID", err) + } + if v, ok := any(m.Status).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Status", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type MeetingPerson struct { + Email string `json:"email" validate:"required"` + Name *string `json:"name,omitempty"` +} + +func (m MeetingPerson) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(m)) +} + +type MergeOrganizationBody struct { + LosingOrganizationID int64 `json:"losing_organization_id"` + LosingRevision int64 `json:"losing_revision"` +} + +type MessageDetail struct { + Attachments []AttachmentInfo `json:"attachments,omitempty" validate:"required"` + Bcc []string `json:"bcc,omitempty"` + Body string `json:"body" validate:"required"` + BodyHTML *string `json:"body_html,omitempty"` + BodyOmitted *bool `json:"body_omitted,omitempty"` + Cc []string `json:"cc,omitempty"` + ConversationID *int64 `json:"conversation_id,omitempty"` + DeletedAt *string `json:"deleted_at,omitempty"` + From string `json:"from" validate:"required"` + FromEmail *string `json:"from_email,omitempty"` + FromName *string `json:"from_name,omitempty"` + FromPhone *string `json:"from_phone,omitempty"` + HasAttachments bool `json:"has_attachments"` + ID int64 `json:"id"` + Labels []string `json:"labels,omitempty" validate:"required"` + MessageType *string `json:"message_type,omitempty"` + SentAt string `json:"sent_at" validate:"required"` + SizeBytes int64 `json:"size_bytes"` + Snippet string `json:"snippet" validate:"required"` + SourceID *int64 `json:"source_id,omitempty"` + SourceMessageID *string `json:"source_message_id,omitempty"` + Subject string `json:"subject" validate:"required"` + To []string `json:"to,omitempty" validate:"required"` +} + +func (m MessageDetail) Validate() error { + var errors runtime.ValidationErrors + for i, item := range m.Attachments { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Attachments[%d]", i), err) + } + } + } + if err := typesValidator.Var(m.Body, "required"); err != nil { + errors = errors.Append("Body", err) + } + if err := typesValidator.Var(m.From, "required"); err != nil { + errors = errors.Append("From", err) + } + if err := typesValidator.Var(m.Labels, "required"); err != nil { + errors = errors.Append("Labels", err) + } + if err := typesValidator.Var(m.SentAt, "required"); err != nil { + errors = errors.Append("SentAt", err) + } + if err := typesValidator.Var(m.Snippet, "required"); err != nil { + errors = errors.Append("Snippet", err) + } + if err := typesValidator.Var(m.Subject, "required"); err != nil { + errors = errors.Append("Subject", err) + } + if err := typesValidator.Var(m.To, "required"); err != nil { + errors = errors.Append("To", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type MessageListResponse struct { + Messages []MessageSummary `json:"messages,omitempty" validate:"required"` + Page int64 `json:"page"` + PageSize int64 `json:"page_size"` + Total int64 `json:"total"` +} + +func (m MessageListResponse) Validate() error { + var errors runtime.ValidationErrors + for i, item := range m.Messages { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Messages[%d]", i), err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type MessageSummary struct { + Bcc []string `json:"bcc,omitempty"` + Cc []string `json:"cc,omitempty"` + ConversationID *int64 `json:"conversation_id,omitempty"` + DeletedAt *string `json:"deleted_at,omitempty"` + From string `json:"from" validate:"required"` + FromEmail *string `json:"from_email,omitempty"` + FromName *string `json:"from_name,omitempty"` + FromPhone *string `json:"from_phone,omitempty"` + HasAttachments bool `json:"has_attachments"` + ID int64 `json:"id"` + Labels []string `json:"labels,omitempty" validate:"required"` + MessageType *string `json:"message_type,omitempty"` + SentAt string `json:"sent_at" validate:"required"` + SizeBytes int64 `json:"size_bytes"` + Snippet string `json:"snippet" validate:"required"` + SourceID *int64 `json:"source_id,omitempty"` + SourceMessageID *string `json:"source_message_id,omitempty"` + Subject string `json:"subject" validate:"required"` + To []string `json:"to,omitempty" validate:"required"` +} + +func (m MessageSummary) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(m)) +} + +type MutationResult struct { + Name string `json:"name" validate:"required"` + SourceCount *int64 `json:"source_count,omitempty"` +} + +func (m MutationResult) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(m)) +} + +type OperationHealth struct { + Busy bool `json:"busy"` + Label *string `json:"label,omitempty"` + StartedAt *time.Time `json:"started_at,omitempty"` +} + +type Organization struct { + CreatedAt time.Time `json:"created_at" validate:"required"` + Description *string `json:"description,omitempty"` + ID int64 `json:"id"` + Kind string `json:"kind" validate:"required"` + MergedIntoID *int64 `json:"merged_into_id,omitempty"` + Name string `json:"name" validate:"required"` + PrimaryDomain *string `json:"primary_domain,omitempty"` + RetiredAt *time.Time `json:"retired_at,omitempty"` + Revision int64 `json:"revision"` + UpdatedAt time.Time `json:"updated_at" validate:"required"` +} + +func (o Organization) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(o)) +} + +type OrganizationAddress struct { + AddressKind string `json:"address_kind" validate:"required"` + CountryCode *string `json:"country_code,omitempty"` + CountryName *string `json:"country_name,omitempty"` + Envelope ValueEnvelope `json:"envelope"` + ExtendedAddress *string `json:"extended_address,omitempty"` + ExtendedComponents *string `json:"extended_components,omitempty"` + FreeText *string `json:"free_text,omitempty"` + GeoURI *string `json:"geo_uri,omitempty"` + Label *string `json:"label,omitempty"` + Locality *string `json:"locality,omitempty"` + OrganizationID int64 `json:"organization_id"` + OriginalValue string `json:"original_value" validate:"required"` + PlaceURI *string `json:"place_uri,omitempty"` + PostOfficeBox *string `json:"post_office_box,omitempty"` + PostalCode *string `json:"postal_code,omitempty"` + Region *string `json:"region,omitempty"` + StreetAddress *string `json:"street_address,omitempty"` + Timezone *string `json:"timezone,omitempty"` +} + +func (o OrganizationAddress) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.AddressKind, "required"); err != nil { + errors = errors.Append("AddressKind", err) + } + if v, ok := any(o.Envelope).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Envelope", err) + } + } + if err := typesValidator.Var(o.OriginalValue, "required"); err != nil { + errors = errors.Append("OriginalValue", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationAddressBody struct { + AddressKind string `json:"address_kind" validate:"required"` + CountryCode *string `json:"country_code,omitempty"` + CountryName *string `json:"country_name,omitempty"` + ExtendedAddress *string `json:"extended_address,omitempty"` + ExtendedComponents *string `json:"extended_components,omitempty"` + FreeText *string `json:"free_text,omitempty"` + GeoURI *string `json:"geo_uri,omitempty"` + Label *string `json:"label,omitempty"` + Locality *string `json:"locality,omitempty"` + Ordinal *int64 `json:"ordinal,omitempty" validate:"omitempty,gte=0"` + OriginalValue *string `json:"original_value,omitempty"` + PlaceURI *string `json:"place_uri,omitempty"` + PostOfficeBox *string `json:"post_office_box,omitempty"` + PostalCode *string `json:"postal_code,omitempty"` + Pref *int64 `json:"pref,omitempty"` + Region *string `json:"region,omitempty"` + Source OrganizationAddressBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + StreetAddress *string `json:"street_address,omitempty"` + Timezone *string `json:"timezone,omitempty"` + VcardAltid *string `json:"vcard_altid,omitempty"` + VcardGroup *string `json:"vcard_group,omitempty"` + VcardPid []string `json:"vcard_pid,omitempty"` + VcardPropID *string `json:"vcard_prop_id,omitempty"` + VcardProperty *string `json:"vcard_property,omitempty"` +} + +func (o OrganizationAddressBody) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.AddressKind, "required"); err != nil { + errors = errors.Append("AddressKind", err) + } + if o.Ordinal != nil { + if err := typesValidator.Var(o.Ordinal, "omitempty,gte=0"); err != nil { + errors = errors.Append("Ordinal", err) + } + } + if v, ok := any(o.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationAttributeValue struct { + ActiveFrom time.Time `json:"active_from" validate:"required"` + ActiveUntil *time.Time `json:"active_until,omitempty"` + Actor *string `json:"actor,omitempty"` + Confidence *float64 `json:"confidence,omitempty"` + CreatedAt time.Time `json:"created_at" validate:"required"` + DefinitionID int64 `json:"definition_id"` + DefinitionSlug string `json:"definition_slug" validate:"required"` + ID int64 `json:"id"` + Ordinal int64 `json:"ordinal"` + OrganizationID int64 `json:"organization_id"` + Source string `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + SupersededAt *time.Time `json:"superseded_at,omitempty"` + Value AttributeValue `json:"value"` +} + +func (o OrganizationAttributeValue) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.ActiveFrom, "required"); err != nil { + errors = errors.Append("ActiveFrom", err) + } + if err := typesValidator.Var(o.CreatedAt, "required"); err != nil { + errors = errors.Append("CreatedAt", err) + } + if err := typesValidator.Var(o.DefinitionSlug, "required"); err != nil { + errors = errors.Append("DefinitionSlug", err) + } + if err := typesValidator.Var(o.Source, "required"); err != nil { + errors = errors.Append("Source", err) + } + if v, ok := any(o.Value).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Value", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationAttributeWrite struct { + DryRun bool `json:"dry_run"` + Superseded *OrganizationAttributeValue `json:"superseded,omitempty"` + Value *OrganizationAttributeValue `json:"value,omitempty"` +} + +func (o OrganizationAttributeWrite) Validate() error { + var errors runtime.ValidationErrors + if o.Superseded != nil { + if v, ok := any(o.Superseded).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Superseded", err) + } + } + } + if o.Value != nil { + if v, ok := any(o.Value).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Value", err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationAttributesResponse struct { + Values []OrganizationAttributeValue `json:"values,omitempty" validate:"required"` +} + +func (o OrganizationAttributesResponse) Validate() error { + var errors runtime.ValidationErrors + for i, item := range o.Values { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Values[%d]", i), err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationBody struct { + Description *string `json:"description,omitempty"` + Kind *OrganizationBodyKind `json:"kind,omitempty"` + Name string `json:"name" validate:"required"` + PrimaryDomain *string `json:"primary_domain,omitempty"` + Retired *bool `json:"retired,omitempty"` +} + +func (o OrganizationBody) Validate() error { + var errors runtime.ValidationErrors + if o.Kind != nil { + if v, ok := any(o.Kind).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Kind", err) + } + } + } + if err := typesValidator.Var(o.Name, "required"); err != nil { + errors = errors.Append("Name", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationCategory struct { + Category string `json:"category" validate:"required"` + CategoryNormalized string `json:"category_normalized" validate:"required"` + Envelope ValueEnvelope `json:"envelope"` + OrganizationID int64 `json:"organization_id"` +} + +func (o OrganizationCategory) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.Category, "required"); err != nil { + errors = errors.Append("Category", err) + } + if err := typesValidator.Var(o.CategoryNormalized, "required"); err != nil { + errors = errors.Append("CategoryNormalized", err) + } + if v, ok := any(o.Envelope).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Envelope", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationCategoryBody struct { + Category string `json:"category" validate:"required"` + Ordinal *int64 `json:"ordinal,omitempty" validate:"omitempty,gte=0"` + Pref *int64 `json:"pref,omitempty"` + Source OrganizationCategoryBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + VcardAltid *string `json:"vcard_altid,omitempty"` + VcardGroup *string `json:"vcard_group,omitempty"` + VcardPid []string `json:"vcard_pid,omitempty"` + VcardPropID *string `json:"vcard_prop_id,omitempty"` + VcardProperty *string `json:"vcard_property,omitempty"` +} + +func (o OrganizationCategoryBody) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.Category, "required"); err != nil { + errors = errors.Append("Category", err) + } + if o.Ordinal != nil { + if err := typesValidator.Var(o.Ordinal, "omitempty,gte=0"); err != nil { + errors = errors.Append("Ordinal", err) + } + } + if v, ok := any(o.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationContactPoint struct { + AddressKind string `json:"address_kind" validate:"required"` + Envelope ValueEnvelope `json:"envelope"` + Normalization string `json:"normalization" validate:"required"` + NormalizationVersion int64 `json:"normalization_version"` + NormalizedValue string `json:"normalized_value" validate:"required"` + OrganizationID int64 `json:"organization_id"` + OriginalValue string `json:"original_value" validate:"required"` + ScopeKind *string `json:"scope_kind,omitempty"` + ScopeValue *string `json:"scope_value,omitempty"` + ServiceSlug *string `json:"service_slug,omitempty"` + URI *string `json:"uri,omitempty"` +} + +func (o OrganizationContactPoint) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.AddressKind, "required"); err != nil { + errors = errors.Append("AddressKind", err) + } + if v, ok := any(o.Envelope).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Envelope", err) + } + } + if err := typesValidator.Var(o.Normalization, "required"); err != nil { + errors = errors.Append("Normalization", err) + } + if err := typesValidator.Var(o.NormalizedValue, "required"); err != nil { + errors = errors.Append("NormalizedValue", err) + } + if err := typesValidator.Var(o.OriginalValue, "required"); err != nil { + errors = errors.Append("OriginalValue", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationContactPointBody struct { + ContactKind OrganizationContactPointBodyContactKind `json:"contact_kind" validate:"required"` + Ordinal *int64 `json:"ordinal,omitempty" validate:"omitempty,gte=0"` + OriginalValue string `json:"original_value" validate:"required"` + Pref *int64 `json:"pref,omitempty"` + ScopeKind *string `json:"scope_kind,omitempty"` + ScopeValue *string `json:"scope_value,omitempty"` + ServiceSlug *string `json:"service_slug,omitempty"` + Source OrganizationContactPointBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + URI *string `json:"uri,omitempty"` + VcardAltid *string `json:"vcard_altid,omitempty"` + VcardGroup *string `json:"vcard_group,omitempty"` + VcardPid []string `json:"vcard_pid,omitempty"` + VcardPropID *string `json:"vcard_prop_id,omitempty"` + VcardProperty *string `json:"vcard_property,omitempty"` +} + +func (o OrganizationContactPointBody) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(o.ContactKind).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("ContactKind", err) + } + } + if o.Ordinal != nil { + if err := typesValidator.Var(o.Ordinal, "omitempty,gte=0"); err != nil { + errors = errors.Append("Ordinal", err) + } + } + if err := typesValidator.Var(o.OriginalValue, "required"); err != nil { + errors = errors.Append("OriginalValue", err) + } + if v, ok := any(o.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationCreateBody struct { + Description *string `json:"description,omitempty"` + Kind *OrganizationCreateBodyKind `json:"kind,omitempty"` + Name string `json:"name" validate:"required"` + PrimaryDomain *string `json:"primary_domain,omitempty"` +} + +func (o OrganizationCreateBody) Validate() error { + var errors runtime.ValidationErrors + if o.Kind != nil { + if v, ok := any(o.Kind).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Kind", err) + } + } + } + if err := typesValidator.Var(o.Name, "required"); err != nil { + errors = errors.Append("Name", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationDuplicateSuggestion struct { + Confidence float64 `json:"confidence"` + CreatedAt time.Time `json:"created_at" validate:"required"` + Criterion string `json:"criterion" validate:"required"` + Evidence string `json:"evidence" validate:"required"` + ID int64 `json:"id"` + OrganizationAId int64 `json:"organization_a_id"` + OrganizationBId int64 `json:"organization_b_id"` + ResolutionNote *string `json:"resolution_note,omitempty"` + ResolvedAt *time.Time `json:"resolved_at,omitempty"` + ResolvedBy *string `json:"resolved_by,omitempty"` + Source string `json:"source" validate:"required"` + Status string `json:"status" validate:"required"` + UpdatedAt time.Time `json:"updated_at" validate:"required"` +} + +func (o OrganizationDuplicateSuggestion) Validate() error { + return runtime.ConvertValidatorError(typesValidator.Struct(o)) +} + +type OrganizationDuplicateSuggestionsResponse struct { + Suggestions []OrganizationDuplicateSuggestion `json:"suggestions,omitempty" validate:"required"` +} + +func (o OrganizationDuplicateSuggestionsResponse) Validate() error { + var errors runtime.ValidationErrors + for i, item := range o.Suggestions { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Suggestions[%d]", i), err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationIdentifier struct { + Envelope ValueEnvelope `json:"envelope"` + IdentifierKind string `json:"identifier_kind" validate:"required"` + IdentifierValue string `json:"identifier_value" validate:"required"` + NormalizedValue string `json:"normalized_value" validate:"required"` + OrganizationID int64 `json:"organization_id"` +} + +func (o OrganizationIdentifier) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(o.Envelope).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Envelope", err) + } + } + if err := typesValidator.Var(o.IdentifierKind, "required"); err != nil { + errors = errors.Append("IdentifierKind", err) + } + if err := typesValidator.Var(o.IdentifierValue, "required"); err != nil { + errors = errors.Append("IdentifierValue", err) + } + if err := typesValidator.Var(o.NormalizedValue, "required"); err != nil { + errors = errors.Append("NormalizedValue", err) + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationIdentifierBody struct { + IdentifierKind OrganizationIdentifierBodyIdentifierKind `json:"identifier_kind" validate:"required"` + IdentifierValue string `json:"identifier_value" validate:"required"` + Ordinal *int64 `json:"ordinal,omitempty" validate:"omitempty,gte=0"` + Pref *int64 `json:"pref,omitempty"` + Source OrganizationIdentifierBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + VcardAltid *string `json:"vcard_altid,omitempty"` + VcardGroup *string `json:"vcard_group,omitempty"` + VcardPid []string `json:"vcard_pid,omitempty"` + VcardPropID *string `json:"vcard_prop_id,omitempty"` + VcardProperty *string `json:"vcard_property,omitempty"` +} + +func (o OrganizationIdentifierBody) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(o.IdentifierKind).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("IdentifierKind", err) + } + } + if err := typesValidator.Var(o.IdentifierValue, "required"); err != nil { + errors = errors.Append("IdentifierValue", err) + } + if o.Ordinal != nil { + if err := typesValidator.Var(o.Ordinal, "omitempty,gte=0"); err != nil { + errors = errors.Append("Ordinal", err) + } + } + if v, ok := any(o.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + +type OrganizationMedia struct { + ByteSize *int64 `json:"byte_size,omitempty"` + ContentHash *string `json:"content_hash,omitempty"` + Envelope ValueEnvelope `json:"envelope"` + HasData bool `json:"has_data"` + MediaKind string `json:"media_kind" validate:"required"` + MediaType *string `json:"media_type,omitempty"` + OrganizationID int64 `json:"organization_id"` + OriginalValue string `json:"original_value" validate:"required"` + URI *string `json:"uri,omitempty"` +} + +func (o OrganizationMedia) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(o.Envelope).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Envelope", err) + } + } + if err := typesValidator.Var(o.MediaKind, "required"); err != nil { + errors = errors.Append("MediaKind", err) } - for i, item := range m.TranscriptSegments { - if v, ok := any(item).(runtime.Validator); ok { - if err := v.Validate(); err != nil { - errors = errors.Append(fmt.Sprintf("TranscriptSegments[%d]", i), err) - } - } + if err := typesValidator.Var(o.OriginalValue, "required"); err != nil { + errors = errors.Append("OriginalValue", err) } if len(errors) == 0 { return nil @@ -3070,19 +3935,36 @@ func (m Meeting) Validate() error { return errors } -type MeetingImportRequest struct { - Meeting Meeting `json:"meeting"` - Source Source `json:"source"` +type OrganizationMediaBody struct { + Data *string `json:"data,omitempty"` + MediaKind OrganizationMediaBodyMediaKind `json:"media_kind" validate:"required"` + MediaType *string `json:"media_type,omitempty"` + Ordinal *int64 `json:"ordinal,omitempty" validate:"omitempty,gte=0"` + OriginalValue *string `json:"original_value,omitempty"` + Pref *int64 `json:"pref,omitempty"` + Source OrganizationMediaBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + URI *string `json:"uri,omitempty"` + VcardAltid *string `json:"vcard_altid,omitempty"` + VcardGroup *string `json:"vcard_group,omitempty"` + VcardPid []string `json:"vcard_pid,omitempty"` + VcardPropID *string `json:"vcard_prop_id,omitempty"` + VcardProperty *string `json:"vcard_property,omitempty"` } -func (m MeetingImportRequest) Validate() error { +func (o OrganizationMediaBody) Validate() error { var errors runtime.ValidationErrors - if v, ok := any(m.Meeting).(runtime.Validator); ok { + if v, ok := any(o.MediaKind).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Meeting", err) + errors = errors.Append("MediaKind", err) } } - if v, ok := any(m.Source).(runtime.Validator); ok { + if o.Ordinal != nil { + if err := typesValidator.Var(o.Ordinal, "omitempty,gte=0"); err != nil { + errors = errors.Append("Ordinal", err) + } + } + if v, ok := any(o.Source).(runtime.Validator); ok { if err := v.Validate(); err != nil { errors = errors.Append("Source", err) } @@ -3093,93 +3975,134 @@ func (m MeetingImportRequest) Validate() error { return errors } -type MeetingImportResponse struct { - MessageID int64 `json:"message_id"` - SourceID int64 `json:"source_id"` - SourceMessageID string `json:"source_message_id" validate:"required"` - Status MeetingImportResponseStatus `json:"status" validate:"required"` +type OrganizationName struct { + Envelope ValueEnvelope `json:"envelope"` + Name string `json:"name" validate:"required"` + NameKind string `json:"name_kind" validate:"required"` + NameNormalized string `json:"name_normalized" validate:"required"` + OrganizationID int64 `json:"organization_id"` } -func (m MeetingImportResponse) Validate() error { +func (o OrganizationName) Validate() error { var errors runtime.ValidationErrors - if err := typesValidator.Var(m.SourceMessageID, "required"); err != nil { - errors = errors.Append("SourceMessageID", err) - } - if v, ok := any(m.Status).(runtime.Validator); ok { + if v, ok := any(o.Envelope).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append("Status", err) + errors = errors.Append("Envelope", err) } } + if err := typesValidator.Var(o.Name, "required"); err != nil { + errors = errors.Append("Name", err) + } + if err := typesValidator.Var(o.NameKind, "required"); err != nil { + errors = errors.Append("NameKind", err) + } + if err := typesValidator.Var(o.NameNormalized, "required"); err != nil { + errors = errors.Append("NameNormalized", err) + } if len(errors) == 0 { return nil } return errors } -type MeetingPerson struct { - Email string `json:"email" validate:"required"` - Name *string `json:"name,omitempty"` +type OrganizationNameBody struct { + Name string `json:"name" validate:"required"` + NameKind OrganizationNameBodyNameKind `json:"name_kind" validate:"required"` + Ordinal *int64 `json:"ordinal,omitempty" validate:"omitempty,gte=0"` + Pref *int64 `json:"pref,omitempty"` + Source OrganizationNameBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + VcardAltid *string `json:"vcard_altid,omitempty"` + VcardGroup *string `json:"vcard_group,omitempty"` + VcardPid []string `json:"vcard_pid,omitempty"` + VcardPropID *string `json:"vcard_prop_id,omitempty"` + VcardProperty *string `json:"vcard_property,omitempty"` } -func (m MeetingPerson) Validate() error { - return runtime.ConvertValidatorError(typesValidator.Struct(m)) +func (o OrganizationNameBody) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(o.Name, "required"); err != nil { + errors = errors.Append("Name", err) + } + if v, ok := any(o.NameKind).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("NameKind", err) + } + } + if o.Ordinal != nil { + if err := typesValidator.Var(o.Ordinal, "omitempty,gte=0"); err != nil { + errors = errors.Append("Ordinal", err) + } + } + if v, ok := any(o.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if len(errors) == 0 { + return nil + } + return errors } -type MessageDetail struct { - Attachments []AttachmentInfo `json:"attachments,omitempty" validate:"required"` - Bcc []string `json:"bcc,omitempty"` - Body string `json:"body" validate:"required"` - BodyHTML *string `json:"body_html,omitempty"` - BodyOmitted *bool `json:"body_omitted,omitempty"` - Cc []string `json:"cc,omitempty"` - ConversationID *int64 `json:"conversation_id,omitempty"` - DeletedAt *string `json:"deleted_at,omitempty"` - From string `json:"from" validate:"required"` - FromEmail *string `json:"from_email,omitempty"` - FromName *string `json:"from_name,omitempty"` - FromPhone *string `json:"from_phone,omitempty"` - HasAttachments bool `json:"has_attachments"` - ID int64 `json:"id"` - Labels []string `json:"labels,omitempty" validate:"required"` - MessageType *string `json:"message_type,omitempty"` - SentAt string `json:"sent_at" validate:"required"` - SizeBytes int64 `json:"size_bytes"` - Snippet string `json:"snippet" validate:"required"` - SourceID *int64 `json:"source_id,omitempty"` - SourceMessageID *string `json:"source_message_id,omitempty"` - Subject string `json:"subject" validate:"required"` - To []string `json:"to,omitempty" validate:"required"` +type OrganizationProfile struct { + Addresses []OrganizationAddress `json:"addresses,omitempty" validate:"required"` + Categories []OrganizationCategory `json:"categories,omitempty" validate:"required"` + ContactPoints []OrganizationContactPoint `json:"contact_points,omitempty" validate:"required"` + Identifiers []OrganizationIdentifier `json:"identifiers,omitempty" validate:"required"` + Media []OrganizationMedia `json:"media,omitempty" validate:"required"` + Names []OrganizationName `json:"names,omitempty" validate:"required"` + Organization Organization `json:"organization"` } -func (m MessageDetail) Validate() error { +func (o OrganizationProfile) Validate() error { var errors runtime.ValidationErrors - for i, item := range m.Attachments { + for i, item := range o.Addresses { if v, ok := any(item).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append(fmt.Sprintf("Attachments[%d]", i), err) + errors = errors.Append(fmt.Sprintf("Addresses[%d]", i), err) } } } - if err := typesValidator.Var(m.Body, "required"); err != nil { - errors = errors.Append("Body", err) - } - if err := typesValidator.Var(m.From, "required"); err != nil { - errors = errors.Append("From", err) + for i, item := range o.Categories { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Categories[%d]", i), err) + } + } } - if err := typesValidator.Var(m.Labels, "required"); err != nil { - errors = errors.Append("Labels", err) + for i, item := range o.ContactPoints { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("ContactPoints[%d]", i), err) + } + } } - if err := typesValidator.Var(m.SentAt, "required"); err != nil { - errors = errors.Append("SentAt", err) + for i, item := range o.Identifiers { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Identifiers[%d]", i), err) + } + } } - if err := typesValidator.Var(m.Snippet, "required"); err != nil { - errors = errors.Append("Snippet", err) + for i, item := range o.Media { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Media[%d]", i), err) + } + } } - if err := typesValidator.Var(m.Subject, "required"); err != nil { - errors = errors.Append("Subject", err) + for i, item := range o.Names { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Names[%d]", i), err) + } + } } - if err := typesValidator.Var(m.To, "required"); err != nil { - errors = errors.Append("To", err) + if v, ok := any(o.Organization).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Organization", err) + } } if len(errors) == 0 { return nil @@ -3187,19 +4110,56 @@ func (m MessageDetail) Validate() error { return errors } -type MessageListResponse struct { - Messages []MessageSummary `json:"messages,omitempty" validate:"required"` - Page int64 `json:"page"` - PageSize int64 `json:"page_size"` - Total int64 `json:"total"` +type OrganizationProfileBody struct { + Addresses []OrganizationAddressBody `json:"addresses,omitempty"` + Categories []OrganizationCategoryBody `json:"categories,omitempty"` + ContactPoints []OrganizationContactPointBody `json:"contact_points,omitempty"` + Identifiers []OrganizationIdentifierBody `json:"identifiers,omitempty"` + Media []OrganizationMediaBody `json:"media,omitempty"` + Names []OrganizationNameBody `json:"names,omitempty"` } -func (m MessageListResponse) Validate() error { +func (o OrganizationProfileBody) Validate() error { var errors runtime.ValidationErrors - for i, item := range m.Messages { + for i, item := range o.Addresses { if v, ok := any(item).(runtime.Validator); ok { if err := v.Validate(); err != nil { - errors = errors.Append(fmt.Sprintf("Messages[%d]", i), err) + errors = errors.Append(fmt.Sprintf("Addresses[%d]", i), err) + } + } + } + for i, item := range o.Categories { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Categories[%d]", i), err) + } + } + } + for i, item := range o.ContactPoints { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("ContactPoints[%d]", i), err) + } + } + } + for i, item := range o.Identifiers { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Identifiers[%d]", i), err) + } + } + } + for i, item := range o.Media { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Media[%d]", i), err) + } + } + } + for i, item := range o.Names { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Names[%d]", i), err) } } } @@ -3209,45 +4169,26 @@ func (m MessageListResponse) Validate() error { return errors } -type MessageSummary struct { - Bcc []string `json:"bcc,omitempty"` - Cc []string `json:"cc,omitempty"` - ConversationID *int64 `json:"conversation_id,omitempty"` - DeletedAt *string `json:"deleted_at,omitempty"` - From string `json:"from" validate:"required"` - FromEmail *string `json:"from_email,omitempty"` - FromName *string `json:"from_name,omitempty"` - FromPhone *string `json:"from_phone,omitempty"` - HasAttachments bool `json:"has_attachments"` - ID int64 `json:"id"` - Labels []string `json:"labels,omitempty" validate:"required"` - MessageType *string `json:"message_type,omitempty"` - SentAt string `json:"sent_at" validate:"required"` - SizeBytes int64 `json:"size_bytes"` - Snippet string `json:"snippet" validate:"required"` - SourceID *int64 `json:"source_id,omitempty"` - SourceMessageID *string `json:"source_message_id,omitempty"` - Subject string `json:"subject" validate:"required"` - To []string `json:"to,omitempty" validate:"required"` -} - -func (m MessageSummary) Validate() error { - return runtime.ConvertValidatorError(typesValidator.Struct(m)) -} - -type MutationResult struct { - Name string `json:"name" validate:"required"` - SourceCount *int64 `json:"source_count,omitempty"` -} - -func (m MutationResult) Validate() error { - return runtime.ConvertValidatorError(typesValidator.Struct(m)) +type OrganizationsResponse struct { + Limit int64 `json:"limit"` + Offset int64 `json:"offset"` + Organizations []Organization `json:"organizations,omitempty" validate:"required"` + Total int64 `json:"total"` } -type OperationHealth struct { - Busy bool `json:"busy"` - Label *string `json:"label,omitempty"` - StartedAt *time.Time `json:"started_at,omitempty"` +func (o OrganizationsResponse) Validate() error { + var errors runtime.ValidationErrors + for i, item := range o.Organizations { + if v, ok := any(item).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append(fmt.Sprintf("Organizations[%d]", i), err) + } + } + } + if len(errors) == 0 { + return nil + } + return errors } type PartialDate struct { @@ -4335,6 +5276,10 @@ func (q QueryResult) Validate() error { return runtime.ConvertValidatorError(typesValidator.Struct(q)) } +type RefreshOrganizationDuplicateSuggestionsResponse struct { + Created int64 `json:"created"` +} + type RejectedCandidate struct { Identifier string `json:"identifier" validate:"required"` Reason string `json:"reason" validate:"required"` @@ -4544,6 +5489,24 @@ func (r RemoveResult) Validate() error { return errors } +type ResolveOrganizationDuplicateSuggestionBody struct { + Note *string `json:"note,omitempty"` + Status ResolveOrganizationDuplicateSuggestionBodyStatus `json:"status" validate:"required"` +} + +func (r ResolveOrganizationDuplicateSuggestionBody) Validate() error { + var errors runtime.ValidationErrors + if v, ok := any(r.Status).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Status", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + type SavedView struct { CanonicalState SavedViewStateEnvelope `json:"canonical_state"` CreatedAt time.Time `json:"created_at" validate:"required"` @@ -4873,6 +5836,39 @@ func (s SessionStatus) Validate() error { return errors } +type SetOrganizationAttributeBody struct { + Actor *string `json:"actor,omitempty"` + Confidence *float64 `json:"confidence,omitempty"` + DefinitionSlug string `json:"definition_slug" validate:"required"` + DryRun *bool `json:"dry_run,omitempty"` + ExpectedValueID *int64 `json:"expected_value_id,omitempty"` + Ordinal *int64 `json:"ordinal,omitempty"` + Source SetOrganizationAttributeBodySource `json:"source" validate:"required"` + SourceRef *string `json:"source_ref,omitempty"` + Value AttributeValue `json:"value"` +} + +func (s SetOrganizationAttributeBody) Validate() error { + var errors runtime.ValidationErrors + if err := typesValidator.Var(s.DefinitionSlug, "required"); err != nil { + errors = errors.Append("DefinitionSlug", err) + } + if v, ok := any(s.Source).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Source", err) + } + } + if v, ok := any(s.Value).(runtime.Validator); ok { + if err := v.Validate(); err != nil { + errors = errors.Append("Value", err) + } + } + if len(errors) == 0 { + return nil + } + return errors +} + type SetPersonAttributeRequest struct { ActiveFrom *time.Time `json:"active_from,omitempty"` ActiveUntil *time.Time `json:"active_until,omitempty"` diff --git a/pkg/client/openapi.yaml b/pkg/client/openapi.yaml index f4a135962..124ff0635 100644 --- a/pkg/client/openapi.yaml +++ b/pkg/client/openapi.yaml @@ -1802,6 +1802,183 @@ components: - last_at - cache_revision type: object + Employment: + properties: + address_id: + format: int64 + type: integer + confidence: + format: double + type: number + created_at: + format: date-time + type: string + department: + type: string + description: + type: string + end_date: + $ref: "#/components/schemas/PartialDate" + id: + format: int64 + type: integer + is_current: + type: boolean + is_primary: + type: boolean + location: + type: string + organization_id: + format: int64 + type: integer + person_id: + format: int64 + type: integer + revision: + format: int64 + type: integer + role: + type: string + source: + type: string + source_ref: + type: string + start_date: + $ref: "#/components/schemas/PartialDate" + title: + type: string + updated_at: + format: date-time + type: string + required: + - id + - person_id + - organization_id + - is_current + - is_primary + - source + - revision + - created_at + - updated_at + type: object + EmploymentBody: + additionalProperties: false + properties: + address_id: + format: int64 + nullable: true + type: integer + confidence: + format: double + nullable: true + type: number + department: + nullable: true + type: string + description: + nullable: true + type: string + end_date: + nullable: true + type: string + is_current: + nullable: true + type: boolean + is_primary: + nullable: true + type: boolean + location: + nullable: true + type: string + organization_id: + format: int64 + type: integer + person_id: + format: int64 + type: integer + role: + nullable: true + type: string + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + nullable: true + type: string + start_date: + nullable: true + type: string + title: + nullable: true + type: string + required: + - person_id + - organization_id + - source + type: object + EmploymentProjectionResponse: + properties: + department: + type: string + employment_id: + format: int64 + type: integer + organization_id: + format: int64 + type: integer + organization_name: + type: string + role: + type: string + title: + type: string + vcard: + $ref: "#/components/schemas/EmploymentVCard" + required: + - employment_id + - organization_id + - organization_name + - vcard + type: object + EmploymentVCard: + properties: + org: + items: + type: string + nullable: true + type: array + role: + type: string + title: + type: string + type: object + EmploymentsResponse: + properties: + employments: + items: + $ref: "#/components/schemas/Employment" + nullable: true + type: array + projection: + $ref: "#/components/schemas/EmploymentProjectionResponse" + required: + - employments + type: object + EndEmploymentBody: + additionalProperties: false + properties: + end_date: + type: string + required: + - end_date + type: object EntryRow: properties: anchor_message_id: @@ -3254,6 +3431,19 @@ components: required: - email type: object + MergeOrganizationBody: + additionalProperties: false + properties: + losing_organization_id: + format: int64 + type: integer + losing_revision: + format: int64 + type: integer + required: + - losing_organization_id + - losing_revision + type: object MessageDetail: properties: attachments: @@ -3445,132 +3635,43 @@ components: required: - busy type: object - PartialDate: - additionalProperties: false - properties: - day: - format: int64 - type: integer - month: - format: int64 - type: integer - year: - format: int64 - type: integer - type: object - ParticipantContactObservation: + Organization: properties: - address_kind: - type: string - envelope: - $ref: "#/components/schemas/ValueEnvelope" - normalization: - type: string - normalization_version: - format: int64 - type: integer - normalized_value: - type: string - observed_at: + created_at: format: date-time type: string - original_value: - type: string - participant_id: - format: int64 - type: integer - provider_user_id: - type: string - scope_kind: - type: string - scope_value: - type: string - service_slug: + description: type: string - source_id: + id: format: int64 type: integer - required: - - envelope - - participant_id - - address_kind - - original_value - - normalized_value - - normalization - - normalization_version - type: object - PatchAttributeDefinitionRequest: - additionalProperties: false - properties: - description: - nullable: true + kind: type: string - display_order: + merged_into_id: format: int64 type: integer - is_active: - type: boolean - label: - type: string - type: object - PatchPersonRequest: - additionalProperties: false - properties: - display_name: - nullable: true - type: string - x-oapi-codegen-extra-tags: - validate: omitempty - x-omitempty: false - required: - - display_name - type: object - PatchSavedViewRequest: - additionalProperties: false - properties: - canonical_state: - $ref: "#/components/schemas/SavedViewStateEnvelope" - description: - type: string name: type: string - schema_version: - format: int64 - type: integer - type: object - Person: - properties: - created_at: - format: date-time + primary_domain: type: string - display_name: + retired_at: + format: date-time type: string - id: - format: int64 - type: integer - participant_ids: - items: - format: int64 - type: integer - nullable: true - type: array revision: format: int64 type: integer updated_at: format: date-time type: string - vcard_uid: - type: string required: - id - - vcard_uid + - name + - kind - revision - - participant_ids - created_at - updated_at type: object - PersonAddress: + OrganizationAddress: properties: address_kind: type: string @@ -3592,11 +3693,11 @@ components: type: string locality: type: string - original_value: - type: string - person_id: + organization_id: format: int64 type: integer + original_value: + type: string place_uri: type: string post_office_box: @@ -3611,85 +3712,102 @@ components: type: string required: - envelope - - person_id + - organization_id - address_kind - original_value type: object - PersonAddressInputRequest: + OrganizationAddressBody: additionalProperties: false properties: address_kind: type: string country_code: + nullable: true type: string country_name: + nullable: true type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" extended_address: + nullable: true type: string extended_components: + nullable: true type: string free_text: + nullable: true type: string geo_uri: + nullable: true type: string label: + nullable: true type: string locality: + nullable: true type: string + ordinal: + format: int64 + minimum: 0 + type: integer original_value: type: string place_uri: + nullable: true type: string post_office_box: + nullable: true type: string postal_code: + nullable: true type: string + pref: + format: int64 + nullable: true + type: integer region: + nullable: true + type: string + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + nullable: true type: string street_address: + nullable: true type: string timezone: + nullable: true type: string - required: - - address_kind - - envelope - type: object - PersonAddressPatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonAddressInputRequest" + vcard_altid: nullable: true - type: array - supersede: - items: - format: int64 - type: integer + type: string + vcard_group: nullable: true - type: array - type: object - PersonAttributeGroup: - properties: - current: + type: string + vcard_pid: items: - $ref: "#/components/schemas/PersonAttributeValue" + type: string nullable: true type: array - definition: - $ref: "#/components/schemas/AttributeDefinition" - history: - items: - $ref: "#/components/schemas/PersonAttributeValue" + vcard_prop_id: nullable: true - type: array + type: string + vcard_property: + nullable: true + type: string required: - - definition - - current + - address_kind + - source type: object - PersonAttributeValue: + OrganizationAttributeValue: properties: active_from: format: date-time @@ -3716,7 +3834,7 @@ components: ordinal: format: int64 type: integer - person_id: + organization_id: format: int64 type: integer source: @@ -3730,7 +3848,7 @@ components: $ref: "#/components/schemas/AttributeValue" required: - id - - person_id + - organization_id - definition_id - definition_slug - ordinal @@ -3739,108 +3857,117 @@ components: - created_at - source type: object - PersonAttributeWrite: + OrganizationAttributeWrite: properties: dry_run: type: boolean superseded: - $ref: "#/components/schemas/PersonAttributeValue" + $ref: "#/components/schemas/OrganizationAttributeValue" value: - $ref: "#/components/schemas/PersonAttributeValue" + $ref: "#/components/schemas/OrganizationAttributeValue" required: - dry_run type: object - PersonAttributesResponse: + OrganizationAttributesResponse: properties: - attributes: + values: items: - $ref: "#/components/schemas/PersonAttributeGroup" + $ref: "#/components/schemas/OrganizationAttributeValue" nullable: true type: array - person_id: - format: int64 - type: integer required: - - person_id - - attributes + - values type: object - PersonCategory: + OrganizationBody: + additionalProperties: false properties: - envelope: - $ref: "#/components/schemas/ValueEnvelope" - normalized_value: + description: + nullable: true type: string - original_value: + kind: + enum: + - company + - nonprofit + - school + - government + - household + - other type: string - person_id: - format: int64 - type: integer + name: + type: string + primary_domain: + nullable: true + type: string + retired: + type: boolean required: - - envelope - - person_id - - original_value - - normalized_value + - name type: object - PersonCategoryInputRequest: - additionalProperties: false + OrganizationCategory: properties: - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - original_value: + category: type: string + category_normalized: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelope" + organization_id: + format: int64 + type: integer required: - - original_value - envelope + - organization_id + - category + - category_normalized type: object - PersonCategoryPatchRequest: + OrganizationCategoryBody: additionalProperties: false properties: - add: - items: - $ref: "#/components/schemas/PersonCategoryInputRequest" - nullable: true - type: array - supersede: - items: - format: int64 - type: integer - nullable: true - type: array - type: object - PersonCluster: - properties: - canonical_id: + category: + type: string + ordinal: format: int64 + minimum: 0 type: integer - edges: - items: - $ref: "#/components/schemas/PersonClusterEdge" + pref: + format: int64 nullable: true - type: array - member_ids: + type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + nullable: true + type: string + vcard_altid: + nullable: true + type: string + vcard_group: + nullable: true + type: string + vcard_pid: items: - format: int64 - type: integer + type: string nullable: true type: array + vcard_prop_id: + nullable: true + type: string + vcard_property: + nullable: true + type: string required: - - canonical_id - - member_ids - - edges - type: object - PersonClusterEdge: - properties: - participant_a: - format: int64 - type: integer - participant_b: - format: int64 - type: integer - required: - - participant_a - - participant_b + - category + - source type: object - PersonContactPoint: + OrganizationContactPoint: properties: address_kind: type: string @@ -3853,11 +3980,11 @@ components: type: integer normalized_value: type: string - original_value: - type: string - person_id: + organization_id: format: int64 type: integer + original_value: + type: string scope_kind: type: string scope_value: @@ -3868,150 +3995,244 @@ components: type: string required: - envelope - - person_id + - organization_id - address_kind - original_value - normalized_value - normalization - normalization_version type: object - PersonContactPointInputRequest: + OrganizationContactPointBody: additionalProperties: false properties: - address_kind: + contact_kind: + enum: + - email + - phone + - username + - impp + - url + - social + - calendar + - contact_uri + - org_directory + - language type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" + ordinal: + format: int64 + minimum: 0 + type: integer original_value: type: string + pref: + format: int64 + nullable: true + type: integer scope_kind: + nullable: true type: string scope_value: + nullable: true type: string service_slug: + nullable: true + type: string + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + nullable: true type: string uri: + nullable: true + type: string + vcard_altid: + nullable: true + type: string + vcard_group: + nullable: true + type: string + vcard_pid: + items: + type: string + nullable: true + type: array + vcard_prop_id: + nullable: true + type: string + vcard_property: + nullable: true type: string required: - - address_kind + - contact_kind - original_value - - envelope + - source type: object - PersonContactPointPatchRequest: + OrganizationCreateBody: additionalProperties: false properties: - add: - items: - $ref: "#/components/schemas/PersonContactPointInputRequest" + description: nullable: true - type: array - supersede: - items: - format: int64 - type: integer - nullable: true - type: array - type: object - PersonContextSummaryHTTPResponse: - properties: - cache_revision: type: string - candidate_snapshot_id: + kind: + enum: + - company + - nonprofit + - school + - government + - household + - other + type: string + name: + type: string + primary_domain: + nullable: true type: string - search_provenance: - $ref: "#/components/schemas/SearchProvenance" - summary: - $ref: "#/components/schemas/PersonSummary" required: - - summary - - cache_revision - - search_provenance + - name type: object - PersonDate: + OrganizationDuplicateSuggestion: properties: - calendar_scale: + confidence: + format: double + type: number + created_at: + format: date-time type: string - date: - $ref: "#/components/schemas/PartialDate" - date_kind: + criterion: type: string - date_text: + evidence: + type: string + id: + format: int64 + type: integer + organization_a_id: + format: int64 + type: integer + organization_b_id: + format: int64 + type: integer + resolution_note: + type: string + resolved_at: + format: date-time + type: string + resolved_by: + type: string + source: + type: string + status: + type: string + updated_at: + format: date-time type: string + required: + - id + - organization_a_id + - organization_b_id + - criterion + - evidence + - confidence + - status + - source + - created_at + - updated_at + type: object + OrganizationDuplicateSuggestionsResponse: + properties: + suggestions: + items: + $ref: "#/components/schemas/OrganizationDuplicateSuggestion" + nullable: true + type: array + required: + - suggestions + type: object + OrganizationIdentifier: + properties: envelope: $ref: "#/components/schemas/ValueEnvelope" - label: + identifier_kind: type: string - original_value: + identifier_value: type: string - person_id: + normalized_value: + type: string + organization_id: format: int64 type: integer required: - envelope - - person_id - - date_kind - - date - - original_value + - organization_id + - identifier_kind + - identifier_value + - normalized_value type: object - PersonDateInputRequest: + OrganizationIdentifierBody: additionalProperties: false properties: - calendar_scale: + identifier_kind: + enum: + - domain + - linkedin + - duns + - tax_id + - registry + - other type: string - date: - $ref: "#/components/schemas/PartialDate" - date_kind: + identifier_value: type: string - date_text: + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 + nullable: true + type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - label: + source_ref: + nullable: true type: string - original_value: + vcard_altid: + nullable: true type: string - required: - - date_kind - - envelope - type: object - PersonDatePatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonDateInputRequest" + vcard_group: nullable: true - type: array - supersede: + type: string + vcard_pid: items: - format: int64 - type: integer + type: string nullable: true type: array - type: object - PersonIdentifier: - properties: - display_value: - type: string - is_primary: - type: boolean - participant_id: - format: int64 - type: integer - provenance: - type: string - type: + vcard_prop_id: + nullable: true type: string - value: + vcard_property: + nullable: true type: string required: - - type - - value - - is_primary - - provenance - - participant_id + - identifier_kind + - identifier_value + - source type: object - PersonMedia: + OrganizationMedia: properties: byte_size: format: int64 @@ -4026,1327 +4247,1005 @@ components: type: string media_type: type: string - original_value: - type: string - person_id: + organization_id: format: int64 type: integer + original_value: + type: string uri: type: string required: - envelope - - person_id + - organization_id - media_kind - has_data - original_value type: object - PersonMediaInputRequest: + OrganizationMediaBody: additionalProperties: false properties: data: format: base64 type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" media_kind: + enum: + - photo + - logo + - sound + - key type: string media_type: + nullable: true type: string + ordinal: + format: int64 + minimum: 0 + type: integer original_value: type: string + pref: + format: int64 + nullable: true + type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + nullable: true + type: string uri: + nullable: true type: string - required: - - media_kind - - envelope - type: object - PersonMediaPatchRequest: - additionalProperties: false - properties: - add: - items: - $ref: "#/components/schemas/PersonMediaInputRequest" + vcard_altid: nullable: true - type: array - supersede: + type: string + vcard_group: + nullable: true + type: string + vcard_pid: items: - format: int64 - type: integer + type: string nullable: true type: array + vcard_prop_id: + nullable: true + type: string + vcard_property: + nullable: true + type: string + required: + - media_kind + - source type: object - PersonName: + OrganizationName: properties: - additional_names: - type: string envelope: $ref: "#/components/schemas/ValueEnvelope" - family_name: - type: string - formatted: - type: string - generation: - type: string - given_name: - type: string - honorific_prefixes: - type: string - honorific_suffixes: - type: string - is_derived: - type: boolean - language: + name: type: string name_kind: type: string - original_value: + name_normalized: type: string - person_id: + organization_id: format: int64 type: integer - phonetic_script: - type: string - phonetic_system: - type: string - script: - type: string - secondary_surname: - type: string - sort_as: - type: string required: - envelope - - person_id + - organization_id + - name + - name_normalized - name_kind - - is_derived - - original_value type: object - PersonNameInputRequest: + OrganizationNameBody: additionalProperties: false properties: - additional_names: - type: string - envelope: - $ref: "#/components/schemas/ValueEnvelopeInput" - family_name: - type: string - formatted: + name: type: string - generation: + name_kind: + enum: + - alias + - legal + - former + - abbreviation + - sort type: string - given_name: - type: string - honorific_prefixes: - type: string - honorific_suffixes: - type: string - is_derived: - type: boolean - language: - type: string - name_kind: - type: string - original_value: - type: string - phonetic_script: - type: string - phonetic_system: + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 + nullable: true + type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system type: string - script: + source_ref: + nullable: true type: string - secondary_surname: + vcard_altid: + nullable: true type: string - sort_as: + vcard_group: + nullable: true type: string - required: - - name_kind - - envelope - type: object - PersonNamePatchRequest: - additionalProperties: false - properties: - add: + vcard_pid: items: - $ref: "#/components/schemas/PersonNameInputRequest" + type: string nullable: true type: array - supersede: - items: - format: int64 - type: integer + vcard_prop_id: + nullable: true + type: string + vcard_property: nullable: true - type: array - type: object - PersonProfile: - properties: - display_name: type: string - id: - format: int64 - type: integer - revision: - format: int64 - type: integer required: - - id - - revision + - name + - name_kind + - source type: object - PersonProfileHistory: + OrganizationProfile: properties: addresses: items: - $ref: "#/components/schemas/PersonAddress" + $ref: "#/components/schemas/OrganizationAddress" nullable: true type: array categories: items: - $ref: "#/components/schemas/PersonCategory" + $ref: "#/components/schemas/OrganizationCategory" nullable: true type: array contact_points: items: - $ref: "#/components/schemas/PersonContactPoint" + $ref: "#/components/schemas/OrganizationContactPoint" nullable: true type: array - dates: + identifiers: items: - $ref: "#/components/schemas/PersonDate" + $ref: "#/components/schemas/OrganizationIdentifier" nullable: true type: array media: items: - $ref: "#/components/schemas/PersonMedia" + $ref: "#/components/schemas/OrganizationMedia" nullable: true type: array names: items: - $ref: "#/components/schemas/PersonName" - nullable: true - type: array - observations: - items: - $ref: "#/components/schemas/ParticipantContactObservation" + $ref: "#/components/schemas/OrganizationName" nullable: true type: array - person: - $ref: "#/components/schemas/Person" + organization: + $ref: "#/components/schemas/Organization" required: - - person + - organization - names - - contact_points + - identifiers - addresses - - dates - - categories + - contact_points - media - - observations + - categories type: object - PersonProfilePatchRequest: + OrganizationProfileBody: additionalProperties: false properties: addresses: - $ref: "#/components/schemas/PersonAddressPatchRequest" + items: + $ref: "#/components/schemas/OrganizationAddressBody" + nullable: true + type: array categories: - $ref: "#/components/schemas/PersonCategoryPatchRequest" + items: + $ref: "#/components/schemas/OrganizationCategoryBody" + nullable: true + type: array contact_points: - $ref: "#/components/schemas/PersonContactPointPatchRequest" - dates: - $ref: "#/components/schemas/PersonDatePatchRequest" - media: - $ref: "#/components/schemas/PersonMediaPatchRequest" - names: - $ref: "#/components/schemas/PersonNamePatchRequest" - type: object - PersonSearchHTTPResponse: - properties: - cache_revision: - type: string - candidate_snapshot_id: - type: string - next_cursor: - type: string - rows: items: - $ref: "#/components/schemas/PersonSummary" + $ref: "#/components/schemas/OrganizationContactPointBody" nullable: true type: array - search_provenance: - $ref: "#/components/schemas/SearchProvenance" - total_count: - format: int64 - type: integer - required: - - rows - - total_count - - cache_revision - - search_provenance - type: object - PersonSummary: - properties: - activity_count: - format: int64 - type: integer - cache_revision: - type: string - cluster: - $ref: "#/components/schemas/PersonCluster" - display_label: - type: string - display_name: - type: string - file_count: - format: int64 - type: integer - first_at: - format: date-time - type: string - id: - format: int64 - type: integer identifiers: items: - $ref: "#/components/schemas/PersonIdentifier" + $ref: "#/components/schemas/OrganizationIdentifierBody" nullable: true type: array - last_at: - format: date-time - type: string - partial_label: - type: boolean - profile: - $ref: "#/components/schemas/PersonProfile" - source_counts: + media: items: - $ref: "#/components/schemas/SourceCount" + $ref: "#/components/schemas/OrganizationMediaBody" nullable: true type: array - required: - - id - - display_label - - partial_label - - identifiers - - activity_count - - file_count - - source_counts - - first_at - - last_at - - cache_revision - type: object - PersonsResponse: - properties: - persons: + names: items: - $ref: "#/components/schemas/Person" + $ref: "#/components/schemas/OrganizationNameBody" nullable: true type: array - required: - - persons type: object - PingInfo: + OrganizationsResponse: properties: - ok: - type: boolean - pid: + limit: format: int64 type: integer - service: - type: string - version: - type: string - required: - - ok - type: object - Progress: - properties: - done: + offset: format: int64 type: integer + organizations: + items: + $ref: "#/components/schemas/Organization" + nullable: true + type: array total: format: int64 type: integer required: - - done + - organizations - total + - limit + - offset type: object - QueryRequest: + PartialDate: additionalProperties: false properties: - sql: - type: string - required: - - sql - type: object - QueryResult: - properties: - columns: - items: - type: string - nullable: true - type: array - row_count: + day: + format: int64 + type: integer + month: + format: int64 + type: integer + year: format: int64 type: integer - rows: - items: - items: - x-go-type: any - nullable: true - type: array - nullable: true - type: array - required: - - columns - - rows - - row_count type: object - RejectedCandidate: + ParticipantContactObservation: properties: - identifier: + address_kind: type: string - reason: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + normalization: type: string - required: - - identifier - - reason - type: object - RelationshipRow: - properties: - canonical_id: + normalization_version: format: int64 type: integer - display_label: + normalized_value: type: string - last_at: + observed_at: format: date-time type: string - member_ids: - items: - format: int64 - type: integer - nullable: true - type: array - score: - format: double - type: number - signals: - $ref: "#/components/schemas/RelationshipSignals" - required: - - canonical_id - - display_label - - member_ids - - score - - signals - - last_at - type: object - RelationshipSignals: - properties: - last_interaction_at: - format: date-time + original_value: type: string - meeting_count: - format: int64 - type: integer - meetings_together: - format: double - type: number - modalities: - format: int64 - type: integer - received_from_them: - format: double - type: number - sent_count: + participant_id: format: int64 type: integer - sent_to_them: - format: double - type: number - required: - - sent_to_them - - received_from_them - - meetings_together - - sent_count - - meeting_count - - modalities - - last_interaction_at - type: object - RelationshipTimelineHTTPRequest: - additionalProperties: false - properties: - cursor: + provider_user_id: type: string - filters: - items: - $ref: "#/components/schemas/ExploreFilter" - nullable: true - type: array - limit: - format: int64 - maximum: 500 - minimum: 0 - type: integer - timezone: + scope_kind: type: string - type: object - RelationshipTimelineHTTPResponse: - properties: - cache_revision: + scope_value: type: string - canonical_id: - format: int64 - type: integer - identity_revision: - format: int64 - type: integer - next_cursor: + service_slug: type: string - rows: - items: - $ref: "#/components/schemas/TimelineRow" - nullable: true - type: array - total_count: + source_id: format: int64 type: integer required: - - canonical_id - - rows - - total_count - - cache_revision - - identity_revision + - envelope + - participant_id + - address_kind + - original_value + - normalized_value + - normalization + - normalization_version type: object - RelationshipsHTTPRequest: + PatchAttributeDefinitionRequest: additionalProperties: false properties: - cursor: - type: string - filters: - items: - $ref: "#/components/schemas/ExploreFilter" + description: nullable: true - type: array - limit: - format: int64 - maximum: 500 - minimum: 0 - type: integer - show_all: - type: boolean - type: object - RelationshipsHTTPResponse: - properties: - cache_revision: type: string - identity_revision: + display_order: format: int64 type: integer - next_cursor: + is_active: + type: boolean + label: type: string - rows: - items: - $ref: "#/components/schemas/RelationshipRow" - nullable: true - type: array - total_count: - format: int64 - type: integer - required: - - rows - - total_count - - cache_revision - - identity_revision type: object - RemoteImageRequest: + PatchPersonRequest: additionalProperties: false properties: - url: - description: Absolute http(s) URL of the consented remote image + display_name: + nullable: true type: string + x-oapi-codegen-extra-tags: + validate: omitempty + x-omitempty: false required: - - url + - display_name type: object - RemoveRequest: + PatchSavedViewRequest: additionalProperties: false properties: - account: - type: string - identifier: - type: string - source_id: - format: int64 - type: integer - required: - - identifier - type: object - RemoveResult: - properties: - account: - type: string - cache_state: - enum: - - ready - - stale + canonical_state: + $ref: "#/components/schemas/SavedViewStateEnvelope" + description: type: string - identifier: + name: type: string - no_identity: - type: boolean - removed: + schema_version: format: int64 type: integer - required: - - account - - identifier - - removed type: object - SavedView: + Person: properties: - canonical_state: - $ref: "#/components/schemas/SavedViewStateEnvelope" created_at: format: date-time type: string - description: + display_name: type: string id: format: int64 type: integer - name: - type: string + participant_ids: + items: + format: int64 + type: integer + nullable: true + type: array revision: format: int64 type: integer - schema_version: - format: int64 - type: integer updated_at: format: date-time type: string + vcard_uid: + type: string required: - id - - name - - canonical_state - - schema_version + - vcard_uid - revision + - participant_ids - created_at - updated_at type: object - SavedViewFilter: - additionalProperties: false + PersonAddress: properties: - field: + address_kind: type: string - operator: + country_code: type: string - values: - description: Exact filter values; numeric identifiers use decimal strings - items: - type: string - type: array - required: - - field - - operator - - values - type: object - SavedViewSort: - additionalProperties: false - properties: - direction: - enum: - - asc - - desc + country_name: type: string - field: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + extended_address: type: string - required: - - field - - direction - type: object - SavedViewStateEnvelope: - additionalProperties: false - properties: - columns: - items: - type: string - type: array - filters: - items: - $ref: "#/components/schemas/SavedViewFilter" - type: array - grouping: - items: - type: string - type: array - inspector_pinned: - type: boolean - presentation: - enum: - - table - - timeline - - files + extended_components: type: string - query: + free_text: type: string - search_mode: + geo_uri: + type: string + label: + type: string + locality: + type: string + original_value: + type: string + person_id: + format: int64 + type: integer + place_uri: + type: string + post_office_box: + type: string + postal_code: + type: string + region: + type: string + street_address: + type: string + timezone: type: string - sort: - items: - $ref: "#/components/schemas/SavedViewSort" - type: array - type: object - SavedViewsResponse: - properties: - saved_views: - items: - $ref: "#/components/schemas/SavedView" - nullable: true - type: array required: - - saved_views + - envelope + - person_id + - address_kind + - original_value type: object - SchedulerStatusResponse: + PersonAddressInputRequest: + additionalProperties: false properties: - accounts: - items: - $ref: "#/components/schemas/AccountStatus" - nullable: true - type: array - running: - type: boolean + address_kind: + type: string + country_code: + type: string + country_name: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + extended_address: + type: string + extended_components: + type: string + free_text: + type: string + geo_uri: + type: string + label: + type: string + locality: + type: string + original_value: + type: string + place_uri: + type: string + post_office_box: + type: string + postal_code: + type: string + region: + type: string + street_address: + type: string + timezone: + type: string required: - - running - - accounts - type: object - ScoreBreakdown: - properties: - bm25: - format: double - type: number - rrf: - format: double - type: number - subject_boosted: - type: boolean - vector: - format: double - type: number + - address_kind + - envelope type: object - SearchCoverageRequest: + PersonAddressPatchRequest: additionalProperties: false properties: - filters: + add: items: - $ref: "#/components/schemas/ExploreFilter" + $ref: "#/components/schemas/PersonAddressInputRequest" nullable: true type: array - type: object - SearchCoverageResponse: - properties: - actions: + supersede: items: - enum: - - retry - - build_index - type: string + format: int64 + type: integer + nullable: true type: array - cache_revision: - type: string - detail: - type: string - eligible_count: - format: int64 - type: integer - embedded_count: - format: int64 - type: integer - percentage: - format: double - type: number - status: - enum: - - disabled - - initializing - - stale - - incomplete - - unavailable - - ready - type: string - vector_fingerprint: - type: string - vector_generation: - format: int64 - type: integer - required: - - eligible_count - - embedded_count - - percentage - - cache_revision - - status - - actions type: object - SearchFastResponse: + PersonAttributeGroup: properties: - applied_source_ids: + current: items: - format: int64 - type: integer + $ref: "#/components/schemas/PersonAttributeValue" nullable: true type: array - messages: + definition: + $ref: "#/components/schemas/AttributeDefinition" + history: items: - $ref: "#/components/schemas/MessageSummary" + $ref: "#/components/schemas/PersonAttributeValue" nullable: true type: array - query: - type: string - stats: - $ref: "#/components/schemas/TotalStatsResponse" - total_count: - format: int64 - type: integer required: - - query - - messages - - total_count + - definition + - current type: object - SearchProvenance: - additionalProperties: false + PersonAttributeValue: properties: - lexical_index_revision: + active_from: + format: date-time type: string - vector_generation: + active_until: + format: date-time + type: string + actor: + type: string + confidence: + format: double + type: number + created_at: + format: date-time + type: string + definition_id: format: int64 type: integer - type: object - SearchResult: - properties: - messages: - items: - $ref: "#/components/schemas/MessageSummary" - nullable: true - type: array - page: + definition_slug: + type: string + id: format: int64 type: integer - page_size: + ordinal: format: int64 type: integer - query: - type: string - total: + person_id: format: int64 type: integer + source: + type: string + source_ref: + type: string + superseded_at: + format: date-time + type: string + value: + $ref: "#/components/schemas/AttributeValue" required: - - query - - total - - page - - page_size - - messages + - id + - person_id + - definition_id + - definition_slug + - ordinal + - value + - active_from + - created_at + - source type: object - SecretSettingState: + PersonAttributeWrite: properties: - configured: + dry_run: type: boolean + superseded: + $ref: "#/components/schemas/PersonAttributeValue" + value: + $ref: "#/components/schemas/PersonAttributeValue" required: - - configured + - dry_run type: object - SecretSettingUpdate: - additionalProperties: false + PersonAttributesResponse: properties: - action: - enum: - - set - - clear + attributes: + items: + $ref: "#/components/schemas/PersonAttributeGroup" + nullable: true + type: array + person_id: + format: int64 + type: integer + required: + - person_id + - attributes + type: object + PersonCategory: + properties: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + normalized_value: type: string - value: + original_value: type: string + person_id: + format: int64 + type: integer required: - - action + - envelope + - person_id + - original_value + - normalized_value type: object - SenderCount: + PersonCategoryInputRequest: additionalProperties: false properties: - count: - format: int64 - type: integer - sender: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + original_value: type: string required: - - sender - - count + - original_value + - envelope type: object - SessionLoginRequest: + PersonCategoryPatchRequest: additionalProperties: false properties: - api_key: - type: string + add: + items: + $ref: "#/components/schemas/PersonCategoryInputRequest" + nullable: true + type: array + supersede: + items: + format: int64 + type: integer + nullable: true + type: array + type: object + PersonCluster: + properties: + canonical_id: + format: int64 + type: integer + edges: + items: + $ref: "#/components/schemas/PersonClusterEdge" + nullable: true + type: array + member_ids: + items: + format: int64 + type: integer + nullable: true + type: array required: - - api_key + - canonical_id + - member_ids + - edges type: object - SessionStatus: + PersonClusterEdge: properties: - auth_mode: - enum: - - loopback - - api_key - - session - - required - type: string - csrf_token: - type: string - https: - type: boolean - plain_http_warning: - type: boolean + participant_a: + format: int64 + type: integer + participant_b: + format: int64 + type: integer required: - - auth_mode - - https - - plain_http_warning + - participant_a + - participant_b type: object - SetPersonAttributeRequest: - additionalProperties: false + PersonContactPoint: properties: - active_from: - format: date-time - type: string - active_until: - format: date-time + address_kind: type: string - actor: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + normalization: type: string - confidence: - format: double - type: number - expected_value_id: + normalization_version: format: int64 type: integer - ordinal: + normalized_value: + type: string + original_value: + type: string + person_id: format: int64 type: integer - source: - enum: - - user - - carddav_import - - vcard_import - - archive_observation - - extraction - - enrichment - - system + scope_kind: type: string - source_ref: + scope_value: + type: string + service_slug: + type: string + uri: type: string - value: - $ref: "#/components/schemas/AttributeValue" required: - - value + - envelope + - person_id + - address_kind + - original_value + - normalized_value + - normalization + - normalization_version type: object - Setting: + PersonContactPointInputRequest: + additionalProperties: false properties: - group: - enum: - - browser - - server - - archive - - search - - sources - - integrations + address_kind: type: string - key: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + original_value: type: string - kind: - enum: - - string - - integer - - number - - boolean - - string_array - - secret + scope_kind: type: string - options: + scope_value: + type: string + service_slug: + type: string + uri: + type: string + required: + - address_kind + - original_value + - envelope + type: object + PersonContactPointPatchRequest: + additionalProperties: false + properties: + add: items: - type: string + $ref: "#/components/schemas/PersonContactPointInputRequest" + nullable: true + type: array + supersede: + items: + format: int64 + type: integer nullable: true type: array - read_only: - type: boolean - restart_required: - type: boolean - secret: - $ref: "#/components/schemas/SecretSettingState" - testable: - type: boolean - value: - $ref: "#/components/schemas/SettingValue" - required: - - key - - group - - kind - - restart_required type: object - SettingUpdate: - additionalProperties: false + PersonContextSummaryHTTPResponse: properties: - key: + cache_revision: type: string - secret: - $ref: "#/components/schemas/SecretSettingUpdate" - value: - $ref: "#/components/schemas/SettingValue" + candidate_snapshot_id: + type: string + search_provenance: + $ref: "#/components/schemas/SearchProvenance" + summary: + $ref: "#/components/schemas/PersonSummary" required: - - key + - summary + - cache_revision + - search_provenance type: object - SettingValue: - oneOf: - - additionalProperties: false - properties: - string: - type: string - required: - - string - type: object - - additionalProperties: false - properties: - integer: - format: int64 - type: integer - required: - - integer - type: object - - additionalProperties: false - properties: - number: - format: double - type: number - required: - - number - type: object - - additionalProperties: false - properties: - boolean: - type: boolean - required: - - boolean - type: object - - additionalProperties: false - properties: - strings: - items: - type: string - type: array - required: - - strings - type: object - SettingsPatchRequest: - additionalProperties: false + PersonDate: properties: - confirm_api_key_restart: - type: boolean - updates: - items: - $ref: "#/components/schemas/SettingUpdate" - minItems: 1 - type: array + calendar_scale: + type: string + date: + $ref: "#/components/schemas/PartialDate" + date_kind: + type: string + date_text: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelope" + label: + type: string + original_value: + type: string + person_id: + format: int64 + type: integer required: - - updates + - envelope + - person_id + - date_kind + - date + - original_value type: object - SettingsResponse: + PersonDateInputRequest: + additionalProperties: false properties: - pending_restart: - type: boolean - settings: - items: - $ref: "#/components/schemas/Setting" - type: array + calendar_scale: + type: string + date: + $ref: "#/components/schemas/PartialDate" + date_kind: + type: string + date_text: + type: string + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + label: + type: string + original_value: + type: string required: - - settings - - pending_restart + - date_kind + - envelope type: object - SimilarSearchResponse: + PersonDatePatchRequest: + additionalProperties: false properties: - generation: - $ref: "#/components/schemas/HybridGenerationSummary" - messages: + add: items: - $ref: "#/components/schemas/MessageSummary" + $ref: "#/components/schemas/PersonDateInputRequest" + nullable: true + type: array + supersede: + items: + format: int64 + type: integer nullable: true type: array - returned: - format: int64 - type: integer - seed_message_id: - format: int64 - type: integer - required: - - seed_message_id - - returned - - generation - - messages type: object - Source: - additionalProperties: false + PersonIdentifier: properties: - account_email: - format: email + display_value: type: string - x-go-type: string - display_name: - maxLength: 256 + is_primary: + type: boolean + participant_id: + format: int64 + type: integer + provenance: type: string - identifier: - maxLength: 128 + type: + type: string + value: type: string required: - - identifier - - account_email + - type + - value + - is_primary + - provenance + - participant_id type: object - SourceCount: + PersonMedia: properties: - count: + byte_size: format: int64 type: integer - source_type: + content_hash: type: string - required: - - source_type - - count - type: object - SourceIdentitiesResponse: - properties: - account: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + has_data: + type: boolean + media_kind: type: string - identities: - items: - $ref: "#/components/schemas/SourceIdentityResponse" - type: array - source_id: + media_type: + type: string + original_value: + type: string + person_id: format: int64 type: integer + uri: + type: string required: - - source_id - - account - - identities + - envelope + - person_id + - media_kind + - has_data + - original_value type: object - SourceIdentityResponse: + PersonMediaInputRequest: + additionalProperties: false properties: - confirmed_at: - format: date-time + data: + format: base64 type: string - identifier: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + media_kind: + type: string + media_type: + type: string + original_value: + type: string + uri: type: string - signals: - items: - type: string - type: array required: - - identifier - - signals - - confirmed_at + - media_kind + - envelope type: object - SourceStatus: + PersonMediaPatchRequest: + additionalProperties: false properties: - active_sync: - $ref: "#/components/schemas/SyncRunStatus" + add: + items: + $ref: "#/components/schemas/PersonMediaInputRequest" nullable: true - type: "" - can_sync: - type: boolean - display_name: + type: array + supersede: + items: + format: int64 + type: integer nullable: true + type: array + type: object + PersonName: + properties: + additional_names: type: string - id: - format: int64 - type: integer - identifier: + envelope: + $ref: "#/components/schemas/ValueEnvelope" + family_name: type: string - last_successful_sync: - $ref: "#/components/schemas/SyncRunStatus" - nullable: true - type: "" - last_sync_at: - nullable: true + formatted: type: string - latest_sync: - $ref: "#/components/schemas/SyncRunStatus" - nullable: true - type: "" - next_sync_at: - nullable: true + generation: type: string - schedule: + given_name: type: string - scheduled: + honorific_prefixes: + type: string + honorific_suffixes: + type: string + is_derived: type: boolean - scheduler_last_error: + language: type: string - source_type: + name_kind: type: string - sync_unavailable_reason: + original_value: type: string - updated_at: + person_id: + format: int64 + type: integer + phonetic_script: + type: string + phonetic_system: + type: string + script: + type: string + secondary_surname: + type: string + sort_as: type: string required: - - id - - source_type - - identifier - - display_name - - last_sync_at - - updated_at - - can_sync - - scheduled - - next_sync_at - type: object - SourceStatusResponse: - properties: - sources: - items: - $ref: "#/components/schemas/SourceStatus" - nullable: true - type: array - required: - - sources - type: object - SourcesRequest: - additionalProperties: false - properties: - accounts: - items: - type: string - nullable: true - type: array - required: - - accounts + - envelope + - person_id + - name_kind + - is_derived + - original_value type: object - StageDeletionFilter: + PersonNameInputRequest: additionalProperties: false properties: - after: + additional_names: type: string - before: + envelope: + $ref: "#/components/schemas/ValueEnvelopeInput" + family_name: type: string - domain: + formatted: type: string - label: + generation: type: string - recipient: + given_name: type: string - recipient_name: + honorific_prefixes: type: string - sender: + honorific_suffixes: type: string - sender_name: + is_derived: + type: boolean + language: type: string - source_id: - format: int64 - type: integer + name_kind: + type: string + original_value: + type: string + phonetic_script: + type: string + phonetic_system: + type: string + script: + type: string + secondary_surname: + type: string + sort_as: + type: string + required: + - name_kind + - envelope type: object - StageDeletionRequest: + PersonNamePatchRequest: additionalProperties: false properties: - description: - type: string - dry_run: - type: boolean - filter: - $ref: "#/components/schemas/StageDeletionFilter" - message_ids: + add: + items: + $ref: "#/components/schemas/PersonNameInputRequest" + nullable: true + type: array + supersede: items: format: int64 type: integer nullable: true type: array - operation_token: - type: string - selection: - $ref: "#/components/schemas/ExploreSelection" type: object - StageDeletionResponse: + PersonProfile: properties: - account: + display_name: type: string - dry_run: - type: boolean id: - type: string - message_count: - format: int64 - type: integer - sample_gmail_ids: - items: - type: string - nullable: true - type: array - status: - type: string - required: - - dry_run - - message_count - type: object - StatsResponse: - properties: - active_messages: - format: int64 - type: integer - database_size_bytes: - format: int64 - type: integer - source_deleted_messages: - format: int64 - type: integer - total_accounts: - format: int64 - type: integer - total_attachments: - format: int64 - type: integer - total_labels: - format: int64 - type: integer - total_messages: format: int64 type: integer - total_threads: - format: int64 - type: integer - vector_search: - $ref: "#/components/schemas/StatsView" - vector_status: - type: string - required: - - total_messages - - active_messages - - source_deleted_messages - - total_threads - - total_accounts - - total_labels - - total_attachments - - database_size_bytes - type: object - StatsView: - properties: - active_generation: - $ref: "#/components/schemas/GenerationSummary" - building_generation: - $ref: "#/components/schemas/BuildingSummary" - enabled: - type: boolean - missing_embeddings_total: + revision: format: int64 type: integer required: - - enabled - - active_generation - - missing_embeddings_total - type: object - StatusMessageResponse: - properties: - message: - type: string - status: - type: string - required: - - status - - message + - id + - revision type: object - StructuredPersonProfile: + PersonProfileHistory: properties: addresses: items: @@ -5378,6 +5277,11 @@ components: $ref: "#/components/schemas/PersonName" nullable: true type: array + observations: + items: + $ref: "#/components/schemas/ParticipantContactObservation" + nullable: true + type: array person: $ref: "#/components/schemas/Person" required: @@ -5388,659 +5292,2676 @@ components: - dates - categories - media + - observations type: object - Summary: + PersonProfilePatchRequest: additionalProperties: false properties: - accounts: - items: - type: string - nullable: true - type: array - date_range: + addresses: + $ref: "#/components/schemas/PersonAddressPatchRequest" + categories: + $ref: "#/components/schemas/PersonCategoryPatchRequest" + contact_points: + $ref: "#/components/schemas/PersonContactPointPatchRequest" + dates: + $ref: "#/components/schemas/PersonDatePatchRequest" + media: + $ref: "#/components/schemas/PersonMediaPatchRequest" + names: + $ref: "#/components/schemas/PersonNamePatchRequest" + type: object + PersonSearchHTTPResponse: + properties: + cache_revision: + type: string + candidate_snapshot_id: + type: string + next_cursor: + type: string + rows: items: - type: string - maxItems: 2 - minItems: 2 + $ref: "#/components/schemas/PersonSummary" nullable: true type: array - message_count: + search_provenance: + $ref: "#/components/schemas/SearchProvenance" + total_count: format: int64 type: integer - top_senders: - items: - $ref: "#/components/schemas/SenderCount" - nullable: true - type: array - total_size_bytes: - format: int64 - type: integer - required: - - message_count - - total_size_bytes - - date_range - - accounts - - top_senders - type: object - SyncRunItemStatus: - properties: - created_at: - type: string - error_kind: - type: string - error_message: - type: string - phase: - type: string - source_message_id: - type: string required: - - source_message_id - - phase - - error_kind - - error_message - - created_at + - rows + - total_count + - cache_revision + - search_provenance type: object - SyncRunStatus: + PersonSummary: properties: - completed_at: - nullable: true - type: string - cursor_after: - nullable: true + activity_count: + format: int64 + type: integer + cache_revision: type: string - cursor_before: - nullable: true + cluster: + $ref: "#/components/schemas/PersonCluster" + display_label: type: string - error_message: - nullable: true + display_name: type: string - errors_count: + file_count: format: int64 type: integer + first_at: + format: date-time + type: string id: format: int64 type: integer - item_errors: + identifiers: items: - $ref: "#/components/schemas/SyncRunItemStatus" + $ref: "#/components/schemas/PersonIdentifier" nullable: true type: array - messages_added: - format: int64 - type: integer - messages_processed: - format: int64 - type: integer - messages_updated: - format: int64 - type: integer - skipped_count: - format: int64 - type: integer - source_id: - format: int64 - type: integer - started_at: - type: string - status: + last_at: + format: date-time type: string + partial_label: + type: boolean + profile: + $ref: "#/components/schemas/PersonProfile" + source_counts: + items: + $ref: "#/components/schemas/SourceCount" + nullable: true + type: array required: - id - - source_id - - started_at - - completed_at - - status - - messages_processed - - messages_added - - messages_updated - - errors_count - - error_message - - cursor_before - - cursor_after + - display_label + - partial_label + - identifiers + - activity_count + - file_count + - source_counts + - first_at + - last_at + - cache_revision type: object - TaskIntegrationStatusResponse: + PersonsResponse: properties: - message: - description: Credential-free status explanation - type: string - project: - description: Configured project when available - type: string - security_note: - description: Platform-specific local socket security limitation - type: string - state: - description: Current task integration state - enum: - - disabled - - not_found - - authentication_required - - unreachable - - incompatible - - wrong_project - - ready - type: string + persons: + items: + $ref: "#/components/schemas/Person" + nullable: true + type: array required: - - state - - message + - persons type: object - TaskLinkLookupResponse: + PingInfo: properties: - complete: + ok: type: boolean - last_scan: - format: date-time - type: string - outbound_metadata: - $ref: "#/components/schemas/TaskLinkOutboundMetadata" - reason: - type: string - remote_revision: + pid: + format: int64 + type: integer + service: type: string - state: + version: type: string - tasks: - items: - $ref: "#/components/schemas/TaskSummary" - type: array required: - - tasks - - outbound_metadata - - state - - complete - - last_scan + - ok type: object - TaskLinkMutationRequest: + Progress: + properties: + done: + format: int64 + type: integer + total: + format: int64 + type: integer + required: + - done + - total + type: object + QueryRequest: additionalProperties: false properties: - added_at: - description: Browser-generated retry-stable RFC3339 timestamp for the outbound mail link - type: string - description: + sql: type: string - labels: + required: + - sql + type: object + QueryResult: + properties: + columns: items: type: string nullable: true type: array - priority: - type: string - task_id: - description: Existing task to link; omit to create a task - type: string - title: - type: string + row_count: + format: int64 + type: integer + rows: + items: + items: + x-go-type: any + nullable: true + type: array + nullable: true + type: array required: - - added_at + - columns + - rows + - row_count type: object - TaskLinkMutationResponse: + RefreshOrganizationDuplicateSuggestionsResponse: properties: - task: - $ref: "#/components/schemas/TaskLinkTask" + created: + format: int64 + type: integer required: - - task + - created type: object - TaskLinkOutboundMetadata: + RejectedCandidate: properties: - archive_uid: + identifier: type: string - conversation_id: - format: int64 - type: integer - from: + reason: type: string - message_id: + required: + - identifier + - reason + type: object + RelationshipRow: + properties: + canonical_id: format: int64 type: integer - sent_at: - type: string - source_identifier: - type: string - source_message_id: - type: string - source_type: + display_label: type: string - subject: + last_at: + format: date-time type: string - required: - - archive_uid - - message_id - - conversation_id - - subject - - from - - sent_at - - source_type - - source_identifier - - source_message_id - type: object - TaskLinkTask: - properties: - id: - type: string - project: - type: string - revision: - type: string - title: - type: string - required: - - id - - project - - title - type: object - TaskSearchResponse: - properties: - tasks: + member_ids: items: - $ref: "#/components/schemas/TaskSummary" + format: int64 + type: integer nullable: true type: array + score: + format: double + type: number + signals: + $ref: "#/components/schemas/RelationshipSignals" required: - - tasks + - canonical_id + - display_label + - member_ids + - score + - signals + - last_at type: object - TaskSummary: + RelationshipSignals: properties: - id: - type: string - revision: - type: string - title: + last_interaction_at: + format: date-time type: string - required: - - id - - title - type: object - TextConversationRow: - properties: - conversation_id: + meeting_count: format: int64 type: integer - last_message_at: - type: string - last_preview: - type: string - message_count: + meetings_together: + format: double + type: number + modalities: format: int64 type: integer - participant_count: + received_from_them: + format: double + type: number + sent_count: format: int64 type: integer - source_type: - type: string - title: - type: string + sent_to_them: + format: double + type: number required: - - conversation_id - - title - - source_type - - message_count - - participant_count - - last_preview + - sent_to_them + - received_from_them + - meetings_together + - sent_count + - meeting_count + - modalities + - last_interaction_at type: object - TextConversationsResponse: + RelationshipTimelineHTTPRequest: + additionalProperties: false properties: - conversations: + cursor: + type: string + filters: items: - $ref: "#/components/schemas/TextConversationRow" + $ref: "#/components/schemas/ExploreFilter" nullable: true type: array - count: - format: int64 - type: integer - has_more: - type: boolean limit: format: int64 + maximum: 500 + minimum: 0 type: integer - offset: - format: int64 - type: integer - required: - - count - - has_more - - offset - - limit - - conversations + timezone: + type: string type: object - TextMessagesResponse: + RelationshipTimelineHTTPResponse: properties: - count: + cache_revision: + type: string + canonical_id: format: int64 type: integer - has_more: - type: boolean - limit: + identity_revision: format: int64 type: integer - messages: + next_cursor: + type: string + rows: items: - $ref: "#/components/schemas/CLIQueryMessageSummary" + $ref: "#/components/schemas/TimelineRow" nullable: true type: array - offset: + total_count: format: int64 type: integer required: - - count - - has_more - - offset - - limit - - messages + - canonical_id + - rows + - total_count + - cache_revision + - identity_revision type: object - TimelineRow: + RelationshipsHTTPRequest: + additionalProperties: false properties: - anchor_message_id: - format: int64 - type: integer - conversation_id: + cursor: + type: string + filters: + items: + $ref: "#/components/schemas/ExploreFilter" + nullable: true + type: array + limit: format: int64 + maximum: 500 + minimum: 0 type: integer - first_at: - format: date-time - type: string - has_attachments: + show_all: type: boolean - key: - type: string - kind: + type: object + RelationshipsHTTPResponse: + properties: + cache_revision: type: string - message_count: + identity_revision: format: int64 type: integer - occurred_at: - format: date-time - type: string - preview: + next_cursor: type: string - source_id: + rows: + items: + $ref: "#/components/schemas/RelationshipRow" + nullable: true + type: array + total_count: format: int64 type: integer - title: + required: + - rows + - total_count + - cache_revision + - identity_revision + type: object + RemoteImageRequest: + additionalProperties: false + properties: + url: + description: Absolute http(s) URL of the consented remote image type: string required: - - key - - kind - - title - - preview - - occurred_at - - message_count - - source_id - - has_attachments + - url type: object - TokenUploadRequest: + RemoveRequest: additionalProperties: false properties: - access_token: + account: type: string - client_id: + identifier: type: string - expiry: - format: date-time + source_id: + format: int64 + type: integer + required: + - identifier + type: object + RemoveResult: + properties: + account: type: string - refresh_token: + cache_state: + enum: + - ready + - stale type: string - scopes: - items: - type: string + identifier: + type: string + no_identity: + type: boolean + removed: + format: int64 + type: integer + required: + - account + - identifier + - removed + type: object + ResolveOrganizationDuplicateSuggestionBody: + additionalProperties: false + properties: + note: nullable: true - type: array - tenant_id: type: string - token_type: + status: + enum: + - accepted + - rejected type: string required: - - refresh_token + - status type: object - TotalStatsResponse: + SavedView: properties: - account_count: - format: int64 - type: integer - active_messages: - format: int64 - type: integer - applied_search_scope: - type: boolean - applied_source_ids: - items: - format: int64 - type: integer - nullable: true - type: array - attachment_count: - format: int64 - type: integer - attachment_size: - format: int64 - type: integer - label_count: - format: int64 - type: integer - message_count: + canonical_state: + $ref: "#/components/schemas/SavedViewStateEnvelope" + created_at: + format: date-time + type: string + description: + type: string + id: format: int64 type: integer - source_deleted_messages: + name: + type: string + revision: format: int64 type: integer - total_size: + schema_version: format: int64 type: integer + updated_at: + format: date-time + type: string required: - - message_count - - active_messages - - source_deleted_messages - - total_size - - attachment_count - - attachment_size - - label_count - - account_count + - id + - name + - canonical_state + - schema_version + - revision + - created_at + - updated_at type: object - TranscriptSegment: + SavedViewFilter: additionalProperties: false properties: - offset_seconds: - format: double - minimum: 0 - type: number - speaker: + field: type: string - text: + operator: type: string + values: + description: Exact filter values; numeric identifiers use decimal strings + items: + type: string + type: array required: - - speaker - - text + - field + - operator + - values type: object - UpdateRequest: + SavedViewSort: additionalProperties: false properties: - display_name: + direction: + enum: + - asc + - desc type: string - email: + field: type: string required: - - email - - display_name + - field + - direction type: object - UpdateResult: + SavedViewStateEnvelope: + additionalProperties: false properties: - display_name: + columns: + items: + type: string + type: array + filters: + items: + $ref: "#/components/schemas/SavedViewFilter" + type: array + grouping: + items: + type: string + type: array + inspector_pinned: + type: boolean + presentation: + enum: + - table + - timeline + - files type: string - email: + query: + type: string + search_mode: type: string + sort: + items: + $ref: "#/components/schemas/SavedViewSort" + type: array + type: object + SavedViewsResponse: + properties: + saved_views: + items: + $ref: "#/components/schemas/SavedView" + nullable: true + type: array required: - - email - - display_name + - saved_views type: object - VCardIdentity: - additionalProperties: false + SchedulerStatusResponse: properties: - altid: - type: string - group: - type: string - pid: + accounts: items: - type: string + $ref: "#/components/schemas/AccountStatus" nullable: true type: array - prop_id: - type: string - property: - type: string + running: + type: boolean + required: + - running + - accounts type: object - ValueEnvelope: + ScoreBreakdown: properties: - active_from: - format: date-time - type: string - active_until: - format: date-time - type: string - confidence: + bm25: format: double type: number - created_at: - format: date-time - type: string - id: - format: int64 - type: integer - ordinal: - format: int64 - type: integer - pref: - format: int64 - type: integer - source: - type: string - source_ref: - type: string - superseded_at: - format: date-time - type: string - type_label: - type: string - type_tokens: + rrf: + format: double + type: number + subject_boosted: + type: boolean + vector: + format: double + type: number + type: object + SearchCoverageRequest: + additionalProperties: false + properties: + filters: items: - type: string + $ref: "#/components/schemas/ExploreFilter" nullable: true type: array - updated_at: - format: date-time - type: string - vcard: - $ref: "#/components/schemas/VCardIdentity" - required: - - id - - ordinal - - vcard - - source - - created_at - - updated_at type: object - ValueEnvelopeInput: - additionalProperties: false + SearchCoverageResponse: properties: - active_from: - format: date-time + actions: + items: + enum: + - retry + - build_index + type: string + type: array + cache_revision: type: string - active_until: - format: date-time + detail: type: string - confidence: - format: double - type: number - ordinal: + eligible_count: format: int64 - minimum: 0 type: integer - pref: + embedded_count: format: int64 type: integer - source: - type: string - source_ref: + percentage: + format: double + type: number + status: + enum: + - disabled + - initializing + - stale + - incomplete + - unavailable + - ready type: string - type_label: + vector_fingerprint: type: string - type_tokens: - items: - type: string - nullable: true - type: array - vcard: - $ref: "#/components/schemas/VCardIdentity" + vector_generation: + format: int64 + type: integer + required: + - eligible_count + - embedded_count + - percentage + - cache_revision + - status + - actions + type: object + SearchFastResponse: + properties: + applied_source_ids: + items: + format: int64 + type: integer + nullable: true + type: array + messages: + items: + $ref: "#/components/schemas/MessageSummary" + nullable: true + type: array + query: + type: string + stats: + $ref: "#/components/schemas/TotalStatsResponse" + total_count: + format: int64 + type: integer + required: + - query + - messages + - total_count + type: object + SearchProvenance: + additionalProperties: false + properties: + lexical_index_revision: + type: string + vector_generation: + format: int64 + type: integer + type: object + SearchResult: + properties: + messages: + items: + $ref: "#/components/schemas/MessageSummary" + nullable: true + type: array + page: + format: int64 + type: integer + page_size: + format: int64 + type: integer + query: + type: string + total: + format: int64 + type: integer + required: + - query + - total + - page + - page_size + - messages + type: object + SecretSettingState: + properties: + configured: + type: boolean + required: + - configured + type: object + SecretSettingUpdate: + additionalProperties: false + properties: + action: + enum: + - set + - clear + type: string + value: + type: string + required: + - action + type: object + SenderCount: + additionalProperties: false + properties: + count: + format: int64 + type: integer + sender: + type: string + required: + - sender + - count + type: object + SessionLoginRequest: + additionalProperties: false + properties: + api_key: + type: string + required: + - api_key + type: object + SessionStatus: + properties: + auth_mode: + enum: + - loopback + - api_key + - session + - required + type: string + csrf_token: + type: string + https: + type: boolean + plain_http_warning: + type: boolean + required: + - auth_mode + - https + - plain_http_warning + type: object + SetOrganizationAttributeBody: + additionalProperties: false + properties: + actor: + nullable: true + type: string + confidence: + format: double + nullable: true + type: number + definition_slug: + type: string + dry_run: + type: boolean + expected_value_id: + format: int64 + nullable: true + type: integer + ordinal: + format: int64 + nullable: true + type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: + nullable: true + type: string + value: + $ref: "#/components/schemas/AttributeValue" required: + - definition_slug + - value - source type: object - VectorHealth: + SetPersonAttributeRequest: + additionalProperties: false properties: - error: + active_from: + format: date-time type: string - status: + active_until: + format: date-time + type: string + actor: + type: string + confidence: + format: double + type: number + expected_value_id: + format: int64 + type: integer + ordinal: + format: int64 + type: integer + source: + enum: + - user + - carddav_import + - vcard_import + - archive_observation + - extraction + - enrichment + - system + type: string + source_ref: type: string + value: + $ref: "#/components/schemas/AttributeValue" required: - - status + - value type: object - securitySchemes: - apiKey: - in: header - name: X-Api-Key - type: apiKey -info: - title: msgvault API - version: 1.38.0 -openapi: 3.0.3 -paths: - /api/ping: - get: - operationId: daemonPing + Setting: + properties: + group: + enum: + - browser + - server + - archive + - search + - sources + - integrations + type: string + key: + type: string + kind: + enum: + - string + - integer + - number + - boolean + - string_array + - secret + type: string + options: + items: + type: string + nullable: true + type: array + read_only: + type: boolean + restart_required: + type: boolean + secret: + $ref: "#/components/schemas/SecretSettingState" + testable: + type: boolean + value: + $ref: "#/components/schemas/SettingValue" + required: + - key + - group + - kind + - restart_required + type: object + SettingUpdate: + additionalProperties: false + properties: + key: + type: string + secret: + $ref: "#/components/schemas/SecretSettingUpdate" + value: + $ref: "#/components/schemas/SettingValue" + required: + - key + type: object + SettingValue: + oneOf: + - additionalProperties: false + properties: + string: + type: string + required: + - string + type: object + - additionalProperties: false + properties: + integer: + format: int64 + type: integer + required: + - integer + type: object + - additionalProperties: false + properties: + number: + format: double + type: number + required: + - number + type: object + - additionalProperties: false + properties: + boolean: + type: boolean + required: + - boolean + type: object + - additionalProperties: false + properties: + strings: + items: + type: string + type: array + required: + - strings + type: object + SettingsPatchRequest: + additionalProperties: false + properties: + confirm_api_key_restart: + type: boolean + updates: + items: + $ref: "#/components/schemas/SettingUpdate" + minItems: 1 + type: array + required: + - updates + type: object + SettingsResponse: + properties: + pending_restart: + type: boolean + settings: + items: + $ref: "#/components/schemas/Setting" + type: array + required: + - settings + - pending_restart + type: object + SimilarSearchResponse: + properties: + generation: + $ref: "#/components/schemas/HybridGenerationSummary" + messages: + items: + $ref: "#/components/schemas/MessageSummary" + nullable: true + type: array + returned: + format: int64 + type: integer + seed_message_id: + format: int64 + type: integer + required: + - seed_message_id + - returned + - generation + - messages + type: object + Source: + additionalProperties: false + properties: + account_email: + format: email + type: string + x-go-type: string + display_name: + maxLength: 256 + type: string + identifier: + maxLength: 128 + type: string + required: + - identifier + - account_email + type: object + SourceCount: + properties: + count: + format: int64 + type: integer + source_type: + type: string + required: + - source_type + - count + type: object + SourceIdentitiesResponse: + properties: + account: + type: string + identities: + items: + $ref: "#/components/schemas/SourceIdentityResponse" + type: array + source_id: + format: int64 + type: integer + required: + - source_id + - account + - identities + type: object + SourceIdentityResponse: + properties: + confirmed_at: + format: date-time + type: string + identifier: + type: string + signals: + items: + type: string + type: array + required: + - identifier + - signals + - confirmed_at + type: object + SourceStatus: + properties: + active_sync: + $ref: "#/components/schemas/SyncRunStatus" + nullable: true + type: "" + can_sync: + type: boolean + display_name: + nullable: true + type: string + id: + format: int64 + type: integer + identifier: + type: string + last_successful_sync: + $ref: "#/components/schemas/SyncRunStatus" + nullable: true + type: "" + last_sync_at: + nullable: true + type: string + latest_sync: + $ref: "#/components/schemas/SyncRunStatus" + nullable: true + type: "" + next_sync_at: + nullable: true + type: string + schedule: + type: string + scheduled: + type: boolean + scheduler_last_error: + type: string + source_type: + type: string + sync_unavailable_reason: + type: string + updated_at: + type: string + required: + - id + - source_type + - identifier + - display_name + - last_sync_at + - updated_at + - can_sync + - scheduled + - next_sync_at + type: object + SourceStatusResponse: + properties: + sources: + items: + $ref: "#/components/schemas/SourceStatus" + nullable: true + type: array + required: + - sources + type: object + SourcesRequest: + additionalProperties: false + properties: + accounts: + items: + type: string + nullable: true + type: array + required: + - accounts + type: object + StageDeletionFilter: + additionalProperties: false + properties: + after: + type: string + before: + type: string + domain: + type: string + label: + type: string + recipient: + type: string + recipient_name: + type: string + sender: + type: string + sender_name: + type: string + source_id: + format: int64 + type: integer + type: object + StageDeletionRequest: + additionalProperties: false + properties: + description: + type: string + dry_run: + type: boolean + filter: + $ref: "#/components/schemas/StageDeletionFilter" + message_ids: + items: + format: int64 + type: integer + nullable: true + type: array + operation_token: + type: string + selection: + $ref: "#/components/schemas/ExploreSelection" + type: object + StageDeletionResponse: + properties: + account: + type: string + dry_run: + type: boolean + id: + type: string + message_count: + format: int64 + type: integer + sample_gmail_ids: + items: + type: string + nullable: true + type: array + status: + type: string + required: + - dry_run + - message_count + type: object + StatsResponse: + properties: + active_messages: + format: int64 + type: integer + database_size_bytes: + format: int64 + type: integer + source_deleted_messages: + format: int64 + type: integer + total_accounts: + format: int64 + type: integer + total_attachments: + format: int64 + type: integer + total_labels: + format: int64 + type: integer + total_messages: + format: int64 + type: integer + total_threads: + format: int64 + type: integer + vector_search: + $ref: "#/components/schemas/StatsView" + vector_status: + type: string + required: + - total_messages + - active_messages + - source_deleted_messages + - total_threads + - total_accounts + - total_labels + - total_attachments + - database_size_bytes + type: object + StatsView: + properties: + active_generation: + $ref: "#/components/schemas/GenerationSummary" + building_generation: + $ref: "#/components/schemas/BuildingSummary" + enabled: + type: boolean + missing_embeddings_total: + format: int64 + type: integer + required: + - enabled + - active_generation + - missing_embeddings_total + type: object + StatusMessageResponse: + properties: + message: + type: string + status: + type: string + required: + - status + - message + type: object + StructuredPersonProfile: + properties: + addresses: + items: + $ref: "#/components/schemas/PersonAddress" + nullable: true + type: array + categories: + items: + $ref: "#/components/schemas/PersonCategory" + nullable: true + type: array + contact_points: + items: + $ref: "#/components/schemas/PersonContactPoint" + nullable: true + type: array + dates: + items: + $ref: "#/components/schemas/PersonDate" + nullable: true + type: array + media: + items: + $ref: "#/components/schemas/PersonMedia" + nullable: true + type: array + names: + items: + $ref: "#/components/schemas/PersonName" + nullable: true + type: array + person: + $ref: "#/components/schemas/Person" + required: + - person + - names + - contact_points + - addresses + - dates + - categories + - media + type: object + Summary: + additionalProperties: false + properties: + accounts: + items: + type: string + nullable: true + type: array + date_range: + items: + type: string + maxItems: 2 + minItems: 2 + nullable: true + type: array + message_count: + format: int64 + type: integer + top_senders: + items: + $ref: "#/components/schemas/SenderCount" + nullable: true + type: array + total_size_bytes: + format: int64 + type: integer + required: + - message_count + - total_size_bytes + - date_range + - accounts + - top_senders + type: object + SyncRunItemStatus: + properties: + created_at: + type: string + error_kind: + type: string + error_message: + type: string + phase: + type: string + source_message_id: + type: string + required: + - source_message_id + - phase + - error_kind + - error_message + - created_at + type: object + SyncRunStatus: + properties: + completed_at: + nullable: true + type: string + cursor_after: + nullable: true + type: string + cursor_before: + nullable: true + type: string + error_message: + nullable: true + type: string + errors_count: + format: int64 + type: integer + id: + format: int64 + type: integer + item_errors: + items: + $ref: "#/components/schemas/SyncRunItemStatus" + nullable: true + type: array + messages_added: + format: int64 + type: integer + messages_processed: + format: int64 + type: integer + messages_updated: + format: int64 + type: integer + skipped_count: + format: int64 + type: integer + source_id: + format: int64 + type: integer + started_at: + type: string + status: + type: string + required: + - id + - source_id + - started_at + - completed_at + - status + - messages_processed + - messages_added + - messages_updated + - errors_count + - error_message + - cursor_before + - cursor_after + type: object + TaskIntegrationStatusResponse: + properties: + message: + description: Credential-free status explanation + type: string + project: + description: Configured project when available + type: string + security_note: + description: Platform-specific local socket security limitation + type: string + state: + description: Current task integration state + enum: + - disabled + - not_found + - authentication_required + - unreachable + - incompatible + - wrong_project + - ready + type: string + required: + - state + - message + type: object + TaskLinkLookupResponse: + properties: + complete: + type: boolean + last_scan: + format: date-time + type: string + outbound_metadata: + $ref: "#/components/schemas/TaskLinkOutboundMetadata" + reason: + type: string + remote_revision: + type: string + state: + type: string + tasks: + items: + $ref: "#/components/schemas/TaskSummary" + type: array + required: + - tasks + - outbound_metadata + - state + - complete + - last_scan + type: object + TaskLinkMutationRequest: + additionalProperties: false + properties: + added_at: + description: Browser-generated retry-stable RFC3339 timestamp for the outbound mail link + type: string + description: + type: string + labels: + items: + type: string + nullable: true + type: array + priority: + type: string + task_id: + description: Existing task to link; omit to create a task + type: string + title: + type: string + required: + - added_at + type: object + TaskLinkMutationResponse: + properties: + task: + $ref: "#/components/schemas/TaskLinkTask" + required: + - task + type: object + TaskLinkOutboundMetadata: + properties: + archive_uid: + type: string + conversation_id: + format: int64 + type: integer + from: + type: string + message_id: + format: int64 + type: integer + sent_at: + type: string + source_identifier: + type: string + source_message_id: + type: string + source_type: + type: string + subject: + type: string + required: + - archive_uid + - message_id + - conversation_id + - subject + - from + - sent_at + - source_type + - source_identifier + - source_message_id + type: object + TaskLinkTask: + properties: + id: + type: string + project: + type: string + revision: + type: string + title: + type: string + required: + - id + - project + - title + type: object + TaskSearchResponse: + properties: + tasks: + items: + $ref: "#/components/schemas/TaskSummary" + nullable: true + type: array + required: + - tasks + type: object + TaskSummary: + properties: + id: + type: string + revision: + type: string + title: + type: string + required: + - id + - title + type: object + TextConversationRow: + properties: + conversation_id: + format: int64 + type: integer + last_message_at: + type: string + last_preview: + type: string + message_count: + format: int64 + type: integer + participant_count: + format: int64 + type: integer + source_type: + type: string + title: + type: string + required: + - conversation_id + - title + - source_type + - message_count + - participant_count + - last_preview + type: object + TextConversationsResponse: + properties: + conversations: + items: + $ref: "#/components/schemas/TextConversationRow" + nullable: true + type: array + count: + format: int64 + type: integer + has_more: + type: boolean + limit: + format: int64 + type: integer + offset: + format: int64 + type: integer + required: + - count + - has_more + - offset + - limit + - conversations + type: object + TextMessagesResponse: + properties: + count: + format: int64 + type: integer + has_more: + type: boolean + limit: + format: int64 + type: integer + messages: + items: + $ref: "#/components/schemas/CLIQueryMessageSummary" + nullable: true + type: array + offset: + format: int64 + type: integer + required: + - count + - has_more + - offset + - limit + - messages + type: object + TimelineRow: + properties: + anchor_message_id: + format: int64 + type: integer + conversation_id: + format: int64 + type: integer + first_at: + format: date-time + type: string + has_attachments: + type: boolean + key: + type: string + kind: + type: string + message_count: + format: int64 + type: integer + occurred_at: + format: date-time + type: string + preview: + type: string + source_id: + format: int64 + type: integer + title: + type: string + required: + - key + - kind + - title + - preview + - occurred_at + - message_count + - source_id + - has_attachments + type: object + TokenUploadRequest: + additionalProperties: false + properties: + access_token: + type: string + client_id: + type: string + expiry: + format: date-time + type: string + refresh_token: + type: string + scopes: + items: + type: string + nullable: true + type: array + tenant_id: + type: string + token_type: + type: string + required: + - refresh_token + type: object + TotalStatsResponse: + properties: + account_count: + format: int64 + type: integer + active_messages: + format: int64 + type: integer + applied_search_scope: + type: boolean + applied_source_ids: + items: + format: int64 + type: integer + nullable: true + type: array + attachment_count: + format: int64 + type: integer + attachment_size: + format: int64 + type: integer + label_count: + format: int64 + type: integer + message_count: + format: int64 + type: integer + source_deleted_messages: + format: int64 + type: integer + total_size: + format: int64 + type: integer + required: + - message_count + - active_messages + - source_deleted_messages + - total_size + - attachment_count + - attachment_size + - label_count + - account_count + type: object + TranscriptSegment: + additionalProperties: false + properties: + offset_seconds: + format: double + minimum: 0 + type: number + speaker: + type: string + text: + type: string + required: + - speaker + - text + type: object + UpdateRequest: + additionalProperties: false + properties: + display_name: + type: string + email: + type: string + required: + - email + - display_name + type: object + UpdateResult: + properties: + display_name: + type: string + email: + type: string + required: + - email + - display_name + type: object + VCardIdentity: + additionalProperties: false + properties: + altid: + type: string + group: + type: string + pid: + items: + type: string + nullable: true + type: array + prop_id: + type: string + property: + type: string + type: object + ValueEnvelope: + properties: + active_from: + format: date-time + type: string + active_until: + format: date-time + type: string + confidence: + format: double + type: number + created_at: + format: date-time + type: string + id: + format: int64 + type: integer + ordinal: + format: int64 + type: integer + pref: + format: int64 + type: integer + source: + type: string + source_ref: + type: string + superseded_at: + format: date-time + type: string + type_label: + type: string + type_tokens: + items: + type: string + nullable: true + type: array + updated_at: + format: date-time + type: string + vcard: + $ref: "#/components/schemas/VCardIdentity" + required: + - id + - ordinal + - vcard + - source + - created_at + - updated_at + type: object + ValueEnvelopeInput: + additionalProperties: false + properties: + active_from: + format: date-time + type: string + active_until: + format: date-time + type: string + confidence: + format: double + type: number + ordinal: + format: int64 + minimum: 0 + type: integer + pref: + format: int64 + type: integer + source: + type: string + source_ref: + type: string + type_label: + type: string + type_tokens: + items: + type: string + nullable: true + type: array + vcard: + $ref: "#/components/schemas/VCardIdentity" + required: + - source + type: object + VectorHealth: + properties: + error: + type: string + status: + type: string + required: + - status + type: object + securitySchemes: + apiKey: + in: header + name: X-Api-Key + type: apiKey +info: + title: msgvault API + version: 1.39.0 +openapi: 3.0.3 +paths: + /api/ping: + get: + operationId: daemonPing + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PingInfo" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Daemon discovery ping + tags: + - Daemon + /api/session: + delete: + operationId: logoutSession + responses: + "204": + description: No Content + "429": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Delete the current browser session + tags: + - Session + get: + operationId: getSession + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionStatus" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Get browser authentication status + tags: + - Session + /api/session/login: + post: + operationId: loginSession + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SessionLoginRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/SessionStatus" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + summary: Create an in-memory browser session + tags: + - Session + /api/v1/accounts: + get: + operationId: listAccounts + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AccountListResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List scheduler-configured accounts (with sync schedules); use /cli/accounts for all archived sources + tags: + - API + post: + operationId: addAccount + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AddAccountRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/StatusMessageResponse" + description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/StatusMessageResponse" + description: Created + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Add an account + tags: + - API + /api/v1/aggregates: + get: + operationId: getAggregates + parameters: + - description: Aggregate view type + in: query + name: view_type + schema: + type: string + - description: "Sort field: count, size, attachment_size, or name" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string + - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) + in: query + name: limit + schema: + format: int64 + type: integer + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Search query + in: query + name: search_query + schema: + type: string + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AggregateResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get aggregate rows + tags: + - API + /api/v1/aggregates/sub: + get: + operationId: getSubAggregates + parameters: + - description: Aggregate view type + in: query + name: view_type + required: true + schema: + type: string + - description: "Sort field: count, size, attachment_size, or name" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string + - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) + in: query + name: limit + schema: + format: int64 + type: integer + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Search query + in: query + name: search_query + schema: + type: string + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + - description: Sender email/address filter + in: query + name: sender + schema: + type: string + - description: Sender display-name filter + in: query + name: sender_name + schema: + type: string + - description: Recipient email/address filter + in: query + name: recipient + schema: + type: string + - description: Recipient display-name filter + in: query + name: recipient_name + schema: + type: string + - description: Domain filter + in: query + name: domain + schema: + type: string + - description: Label filter + in: query + name: label + schema: + type: string + - description: Message type filter + in: query + name: message_type + schema: + type: string + - description: Named time period + in: query + name: time_period + schema: + type: string + - description: Conversation ID + in: query + name: conversation_id + schema: + format: int64 + type: integer + - description: Comma-separated aggregate view names to match empty values + in: query + name: empty_targets + schema: + type: string + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AggregateResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get nested aggregate rows + tags: + - API + /api/v1/attachments/{hash}/content: + get: + operationId: getAttachmentContent + parameters: + - description: Attachment SHA-256 content hash + in: path + name: hash + required: true + schema: + type: string + responses: + "200": + content: + "*/*": + schema: + format: binary + type: string + x-contentMediaType: application/octet-stream + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Download attachment content by SHA-256 hash + tags: + - API + /api/v1/attachments/{id}: + get: + operationId: getAttachment + parameters: + - description: Attachment ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttachmentInfo" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get attachment metadata + tags: + - API + /api/v1/attribute-definitions: + get: + operationId: listAttributeDefinitions + parameters: + - description: Filter by object type + in: query + name: object_type + schema: + type: string + - description: Include deactivated definitions + in: query + name: include_hidden + schema: + type: boolean + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinitionsResponse" + description: OK + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List portable attribute definitions + tags: + - API + post: + operationId: createAttributeDefinition + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateAttributeDefinitionRequest" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinition" + description: Created + headers: + ETag: + description: Strong attribute definition revision tag + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Create a user attribute definition + tags: + - API + /api/v1/attribute-definitions/{id}: + delete: + operationId: deleteAttributeDefinition + parameters: + - description: Attribute definition ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest definition read + in: header + name: If-Match + required: true + schema: + type: string + responses: + "204": + description: No Content + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Delete a user attribute definition + tags: + - API + get: + operationId: getAttributeDefinition + parameters: + - description: Attribute definition ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinition" + description: OK + headers: + ETag: + description: Strong attribute definition revision tag + schema: + type: string + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get one attribute definition + tags: + - API + patch: + operationId: patchAttributeDefinition + parameters: + - description: Attribute definition ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest definition read + in: header + name: If-Match + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PatchAttributeDefinitionRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AttributeDefinition" + description: OK + headers: + ETag: + description: Strong attribute definition revision tag + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Update mutable attribute definition fields + tags: + - API + /api/v1/auth/token/{email}: + post: + operationId: uploadToken + parameters: + - description: Account email address + in: path + name: email + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TokenUploadRequest" + required: true responses: - "200": + "201": content: application/json: schema: - $ref: "#/components/schemas/PingInfo" - description: OK + $ref: "#/components/schemas/StatusMessageResponse" + description: Created default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Daemon discovery ping + security: + - apiKey: [] + summary: Upload an OAuth token tags: - - Daemon - /api/session: - delete: - operationId: logoutSession + - API + /api/v1/backup/freeze/begin: + post: + operationId: beginBackupFreeze responses: - "204": - description: No Content - "429": + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BackupFreezeBeginResponse" + description: OK + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error + security: + - apiKey: [] + summary: Begin a backup freeze window + tags: + - API + /api/v1/backup/freeze/end: + post: + operationId: endBackupFreeze + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/BackupFreezeEndRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/BackupFreezeEndResponse" + description: OK default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Delete the current browser session + security: + - apiKey: [] + summary: End a backup freeze window tags: - - Session + - API + /api/v1/cli/account: + post: + operationId: updateCLIAccount + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateRequest" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateResult" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable + security: + - apiKey: [] + summary: Update an account for CLI use + tags: + - CLI + /api/v1/cli/accounts: get: - operationId: getSession + operationId: listCLIAccounts responses: "200": content: application/json: schema: - $ref: "#/components/schemas/SessionStatus" + $ref: "#/components/schemas/CliAccountsResponse" description: OK default: content: @@ -6048,24 +7969,162 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Get browser authentication status + security: + - apiKey: [] + summary: List accounts for CLI output tags: - - Session - /api/session/login: + - API + /api/v1/cli/add-calendar/plan: post: - operationId: loginSession + operationId: planCLIAddCalendar requestBody: content: application/json: schema: - $ref: "#/components/schemas/SessionLoginRequest" + $ref: "#/components/schemas/CLIAddCalendarPlanRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/SessionStatus" + $ref: "#/components/schemas/CLIAddCalendarPlanResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Plan CLI Calendar account setup + tags: + - API + /api/v1/cli/attachment: + get: + operationId: getCLIAttachment + parameters: + - description: Attachment SHA-256 content hash + in: query + name: content_hash + required: true + schema: + type: string + responses: + "200": + content: + application/octet-stream: + schema: + format: binary + type: string + x-contentMediaType: application/octet-stream + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get one attachment for CLI export + tags: + - API + /api/v1/cli/build-cache: + post: + operationId: buildCLICache + parameters: + - description: Rebuild all cache files from scratch + in: query + name: full_rebuild + schema: + type: boolean + responses: + "200": + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/CLICacheBuildEvent" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Build the CLI analytics cache + tags: + - API + /api/v1/cli/cache-stats: + get: + operationId: getCLICacheStats + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CacheStats" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Get CLI-compatible analytics cache statistics + tags: + - API + /api/v1/cli/collection: + get: + operationId: getCLICollection + parameters: + - description: Collection name + in: query + name: name + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CliCollectionEnvelope" description: OK default: content: @@ -6073,18 +8132,20 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - summary: Create an in-memory browser session + security: + - apiKey: [] + summary: Get one collection for CLI output tags: - - Session - /api/v1/accounts: + - API + /api/v1/cli/collections: get: - operationId: listAccounts + operationId: listCLICollections responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AccountListResponse" + $ref: "#/components/schemas/CliCollectionsResponse" description: OK default: content: @@ -6094,483 +8155,440 @@ paths: description: Error security: - apiKey: [] - summary: List scheduler-configured accounts (with sync schedules); use /cli/accounts for all archived sources + summary: List collections for CLI output tags: - API post: - operationId: addAccount + operationId: createCLICollection requestBody: content: application/json: schema: - $ref: "#/components/schemas/AddAccountRequest" + $ref: "#/components/schemas/CreateRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/StatusMessageResponse" + $ref: "#/components/schemas/MutationResult" description: OK - "201": + "400": content: application/json: schema: - $ref: "#/components/schemas/StatusMessageResponse" - description: Created - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - security: - - apiKey: [] - summary: Add an account - tags: - - API - /api/v1/aggregates: - get: - operationId: getAggregates - parameters: - - description: Aggregate view type - in: query - name: view_type - schema: - type: string - - description: "Sort field: count, size, attachment_size, or name" - in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction - schema: - type: string - - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) - in: query - name: limit - schema: - format: int64 - type: integer - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Source ID - in: query - name: source_id - schema: - format: int64 - type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages - in: query - name: hide_deleted - schema: - type: boolean - - description: Search query - in: query - name: search_query - schema: - type: string - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - responses: - "200": + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": content: application/json: schema: - $ref: "#/components/schemas/AggregateResponse" - description: OK - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get aggregate rows + summary: Create a collection for CLI use tags: - - API - /api/v1/aggregates/sub: - get: - operationId: getSubAggregates + - CLI + /api/v1/cli/collections/{name}: + delete: + operationId: deleteCLICollection parameters: - - description: Aggregate view type - in: query - name: view_type + - description: Collection name + in: path + name: name required: true schema: + description: Collection name type: string - - description: "Sort field: count, size, attachment_size, or name" - in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction - schema: - type: string - - description: Maximum number of rows to return (default 100; values below 1 fall back to the default) - in: query - name: limit - schema: - format: int64 - type: integer - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Source ID - in: query - name: source_id - schema: - format: int64 - type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages - in: query - name: hide_deleted - schema: - type: boolean - - description: Search query - in: query - name: search_query - schema: - type: string - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - - description: Sender email/address filter - in: query - name: sender - schema: - type: string - - description: Sender display-name filter - in: query - name: sender_name - schema: - type: string - - description: Recipient email/address filter - in: query - name: recipient - schema: - type: string - - description: Recipient display-name filter - in: query - name: recipient_name - schema: - type: string - - description: Domain filter - in: query - name: domain - schema: - type: string - - description: Label filter - in: query - name: label - schema: - type: string - - description: Message type filter - in: query - name: message_type - schema: - type: string - - description: Named time period - in: query - name: time_period - schema: - type: string - - description: Conversation ID - in: query - name: conversation_id - schema: - format: int64 - type: integer - - description: Comma-separated aggregate view names to match empty values - in: query - name: empty_targets - schema: - type: string - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AggregateResponse" + $ref: "#/components/schemas/MutationResult" description: OK - default: + "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get nested aggregate rows + summary: Delete a CLI collection tags: - - API - /api/v1/attachments/{hash}/content: - get: - operationId: getAttachmentContent + - CLI + /api/v1/cli/collections/{name}/sources: + delete: + operationId: removeCLICollectionSources parameters: - - description: Attachment SHA-256 content hash + - description: Collection name in: path - name: hash + name: name required: true schema: + description: Collection name type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SourcesRequest" + required: true responses: "200": content: - "*/*": + application/json: schema: - format: binary - type: string - x-contentMediaType: application/octet-stream + $ref: "#/components/schemas/MutationResult" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "401": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "404": + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Download attachment content by SHA-256 hash + summary: Remove accounts from a CLI collection tags: - - API - /api/v1/attachments/{id}: - get: - operationId: getAttachment + - CLI + patch: + operationId: addCLICollectionSources parameters: - - description: Attachment ID + - description: Collection name in: path - name: id + name: name required: true schema: - format: int64 - type: integer + description: Collection name + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SourcesRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttachmentInfo" + $ref: "#/components/schemas/MutationResult" description: OK - default: + "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Not Found + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get attachment metadata + summary: Add accounts to a CLI collection tags: - - API - /api/v1/attribute-definitions: - get: - operationId: listAttributeDefinitions - parameters: - - description: Filter by object type - in: query - name: object_type - schema: - type: string - - description: Include deactivated definitions - in: query - name: include_hidden - schema: - type: boolean + - CLI + /api/v1/cli/deduplicate/plan: + post: + operationId: planCLIDeduplicate + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIDeduplicatePlanRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinitionsResponse" + $ref: "#/components/schemas/CLIDeduplicatePlanResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable + security: + - apiKey: [] + summary: Plan interactive deduplication for CLI use + tags: + - CLI + /api/v1/cli/delete-deduped: + post: + operationId: executeCLIDeleteDeduped + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CliDeleteDedupedExecuteRequest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/CliDeleteDedupedExecuteResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Conflict + "422": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: List portable attribute definitions + summary: Delete dedup-hidden messages for CLI use tags: - - API + - CLI + /api/v1/cli/delete-deduped/plan: post: - operationId: createAttributeDefinition + operationId: planCLIDeleteDeduped requestBody: content: application/json: schema: - $ref: "#/components/schemas/CreateAttributeDefinitionRequest" + $ref: "#/components/schemas/CliDeleteDedupedPlanRequest" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinition" - description: Created - headers: - ETag: - description: Strong attribute definition revision tag - schema: - type: string + $ref: "#/components/schemas/CliDeleteDedupedPlanResponse" + description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Create a user attribute definition + summary: Plan deletion of dedup-hidden messages for CLI use tags: - - API - /api/v1/attribute-definitions/{id}: - delete: - operationId: deleteAttributeDefinition - parameters: - - description: Attribute definition ID - in: path - name: id - required: true - schema: - format: int64 - type: integer - - description: Strong ETag returned by the latest definition read - in: header - name: If-Match - required: true - schema: - type: string + - CLI + /api/v1/cli/delete-staged/plan: + post: + operationId: planCLIDeleteStaged + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIDeleteStagedPlanRequest" + required: true responses: - "204": - description: No Content - "400": + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "404": + $ref: "#/components/schemas/CLIDeleteStagedPlanResponse" + description: OK + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + security: + - apiKey: [] + summary: Plan CLI staged deletion execution + tags: + - API + /api/v1/cli/deletion-manifests: + post: + operationId: createCLIDeletionManifest + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Manifest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "428": + $ref: "#/components/schemas/CLIDeletionManifestResponse" + description: OK + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: Create a staged deletion manifest + tags: + - API + /api/v1/cli/embeddings/plan: + post: + operationId: planCLIEmbeddings + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIEmbeddingsPlanRequest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/CLIEmbeddingsPlanResponse" + description: OK default: content: application/json: @@ -6579,199 +8597,181 @@ paths: description: Error security: - apiKey: [] - summary: Delete a user attribute definition + summary: Plan CLI embeddings management tags: - API - get: - operationId: getAttributeDefinition - parameters: - - description: Attribute definition ID - in: path - name: id - required: true - schema: - format: int64 - type: integer + /api/v1/cli/identities: + delete: + operationId: removeCLIIdentity + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemoveRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinition" + $ref: "#/components/schemas/RemoveResult" description: OK - headers: - ETag: - description: Strong attribute definition revision tag + "400": + content: + application/json: schema: - type: string - "404": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Get one attribute definition + summary: Remove a confirmed identifier from an account identity tags: - - API - patch: - operationId: patchAttributeDefinition + - CLI + get: + operationId: listCLIIdentities parameters: - - description: Attribute definition ID - in: path - name: id - required: true + - description: Restrict to a single account + explode: false + in: query + name: account + schema: + description: Restrict to a single account + type: string + - description: Restrict to all member accounts of one collection + explode: false + in: query + name: collection + schema: + description: Restrict to all member accounts of one collection + type: string + - description: Restrict to one source by numeric ID + explode: false + in: query + name: source_id schema: + description: Restrict to one source by numeric ID format: int64 type: integer - - description: Strong ETag returned by the latest definition read - in: header - name: If-Match - required: true + - description: For account scope, return only the primary source instead of related sources + explode: false + in: query + name: primary_only schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/PatchAttributeDefinitionRequest" - required: true + description: For account scope, return only the primary source instead of related sources + type: boolean responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AttributeDefinition" + $ref: "#/components/schemas/CliIdentitiesResponse" description: OK - headers: - ETag: - description: Strong attribute definition revision tag - schema: - type: string "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "428": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - default: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Update mutable attribute definition fields + summary: List confirmed account identities tags: - - API - /api/v1/auth/token/{email}: + - CLI post: - operationId: uploadToken - parameters: - - description: Account email address - in: path - name: email - required: true - schema: - type: string + operationId: addCLIIdentity requestBody: content: application/json: schema: - $ref: "#/components/schemas/TokenUploadRequest" + $ref: "#/components/schemas/AddRequest" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/StatusMessageResponse" - description: Created - default: + $ref: "#/components/schemas/AddResult" + description: OK + "400": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - security: - - apiKey: [] - summary: Upload an OAuth token - tags: - - API - /api/v1/backup/freeze/begin: - post: - operationId: beginBackupFreeze - responses: - "200": + $ref: "#/components/schemas/ApiHTTPError" + description: Bad Request + "422": content: application/json: schema: - $ref: "#/components/schemas/BackupFreezeBeginResponse" - description: OK - default: + $ref: "#/components/schemas/ApiHTTPError" + description: Unprocessable Entity + "500": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + $ref: "#/components/schemas/ApiHTTPError" + description: Internal Server Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ApiHTTPError" + description: Service Unavailable security: - apiKey: [] - summary: Begin a backup freeze window + summary: Add a confirmed identifier to an account identity tags: - - API - /api/v1/backup/freeze/end: + - CLI + /api/v1/cli/identities/discover: post: - operationId: endBackupFreeze + operationId: discoverCLIIdentities requestBody: content: application/json: schema: - $ref: "#/components/schemas/BackupFreezeEndRequest" + $ref: "#/components/schemas/DiscoverRequest" required: true responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/BackupFreezeEndResponse" + $ref: "#/components/schemas/DiscoverEvent" description: OK default: content: @@ -6781,24 +8781,24 @@ paths: description: Error security: - apiKey: [] - summary: End a backup freeze window + summary: Discover source-scoped account identities tags: - API - /api/v1/cli/account: + /api/v1/cli/identities/import: post: - operationId: updateCLIAccount + operationId: importCLIIdentities requestBody: content: application/json: schema: - $ref: "#/components/schemas/UpdateRequest" + $ref: "#/components/schemas/ImportRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/UpdateResult" + $ref: "#/components/schemas/ImportResult" description: OK "400": content: @@ -6806,12 +8806,6 @@ paths: schema: $ref: "#/components/schemas/ApiHTTPError" description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found "422": content: application/json: @@ -6832,18 +8826,18 @@ paths: description: Service Unavailable security: - apiKey: [] - summary: Update an account for CLI use + summary: Preview or apply parsed source-scoped identities tags: - CLI - /api/v1/cli/accounts: - get: - operationId: listCLIAccounts + /api/v1/cli/init-db: + post: + operationId: initCLIArchive responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliAccountsResponse" + $ref: "#/components/schemas/CliInitDBResponse" description: OK default: content: @@ -6853,24 +8847,25 @@ paths: description: Error security: - apiKey: [] - summary: List accounts for CLI output + summary: Initialize the archive for CLI use tags: - API - /api/v1/cli/add-calendar/plan: - post: - operationId: planCLIAddCalendar - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CLIAddCalendarPlanRequest" - required: true + /api/v1/cli/message: + get: + operationId: getCLIMessage + parameters: + - description: Message numeric ID or source message ID + in: query + name: id + required: true + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIAddCalendarPlanResponse" + $ref: "#/components/schemas/CliMessageResponse" description: OK default: content: @@ -6880,23 +8875,23 @@ paths: description: Error security: - apiKey: [] - summary: Plan CLI Calendar account setup + summary: Get one message for CLI output tags: - API - /api/v1/cli/attachment: + /api/v1/cli/message/raw: get: - operationId: getCLIAttachment + operationId: getCLIMessageRaw parameters: - - description: Attachment SHA-256 content hash + - description: Message numeric ID or source message ID in: query - name: content_hash + name: id required: true schema: type: string responses: "200": content: - application/octet-stream: + message/rfc822: schema: format: binary type: string @@ -6940,24 +8935,18 @@ paths: description: Error security: - apiKey: [] - summary: Get one attachment for CLI export + summary: Get one raw message for CLI export tags: - API - /api/v1/cli/build-cache: + /api/v1/cli/rebuild-fts: post: - operationId: buildCLICache - parameters: - - description: Rebuild all cache files from scratch - in: query - name: full_rebuild - schema: - type: boolean + operationId: rebuildCLIFTS responses: "200": content: application/x-ndjson: schema: - $ref: "#/components/schemas/CLICacheBuildEvent" + $ref: "#/components/schemas/CliRebuildFTSEvent" description: OK default: content: @@ -6967,18 +8956,45 @@ paths: description: Error security: - apiKey: [] - summary: Build the CLI analytics cache + summary: Rebuild the CLI full-text search index tags: - API - /api/v1/cli/cache-stats: - get: - operationId: getCLICacheStats + /api/v1/cli/repair-encoding: + post: + operationId: repairEncodingCLI responses: "200": + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/CLIRepairEncodingEvent" + description: OK + default: content: application/json: schema: - $ref: "#/components/schemas/CacheStats" + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Repair CLI archive encoding + tags: + - API + /api/v1/cli/run: + post: + operationId: runCLI + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CLIRunRequest" + required: true + responses: + "200": + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/CLIRunEvent" description: OK default: content: @@ -6988,25 +9004,84 @@ paths: description: Error security: - apiKey: [] - summary: Get CLI-compatible analytics cache statistics + summary: Run an allowlisted CLI command tags: - API - /api/v1/cli/collection: + /api/v1/cli/search: get: - operationId: getCLICollection + operationId: searchCLI parameters: - - description: Collection name + - description: Search query in: query - name: name + name: q required: true schema: type: string + - description: Maximum number of rows to return + in: query + name: limit + schema: + format: int64 + type: integer + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + - description: Message type filter; repeat or comma-separate for multiple values + in: query + name: message_type + schema: + type: string + - description: Restrict to one account/source + in: query + name: account + schema: + type: string + - description: Restrict to one collection + in: query + name: collection + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CliSearchResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Search messages for CLI output + tags: + - API + /api/v1/cli/stats: + get: + operationId: getCLIStats + parameters: + - description: Restrict to one account/source + in: query + name: account + schema: + type: string + - description: Restrict to one collection + in: query + name: collection + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliCollectionEnvelope" + $ref: "#/components/schemas/CliStatsResponse" description: OK default: content: @@ -7016,18 +9091,38 @@ paths: description: Error security: - apiKey: [] - summary: Get one collection for CLI output + summary: Get CLI-compatible archive statistics tags: - API - /api/v1/cli/collections: - get: - operationId: listCLICollections + /api/v1/cli/sync: + post: + operationId: syncCLI + parameters: + - description: Account email or display name to sync + in: query + name: email + schema: + type: string + - description: IMAP folder names to include (repeatable) + in: query + name: folder + schema: + items: + type: string + type: array + - description: IMAP folder names to exclude (repeatable) + in: query + name: skip-folder + schema: + items: + type: string + type: array responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/CliCollectionsResponse" + $ref: "#/components/schemas/CLISyncEvent" description: OK default: content: @@ -7037,385 +9132,338 @@ paths: description: Error security: - apiKey: [] - summary: List collections for CLI output + summary: Run CLI incremental sync tags: - API + /api/v1/cli/sync-full: post: - operationId: createCLICollection - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CreateRequest" - required: true - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/MutationResult" - description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable - security: - - apiKey: [] - summary: Create a collection for CLI use - tags: - - CLI - /api/v1/cli/collections/{name}: - delete: - operationId: deleteCLICollection + operationId: syncFullCLI parameters: - - description: Collection name - in: path - name: name - required: true + - description: Account email or display name to sync + in: query + name: email + schema: + type: string + - description: Gmail search query + in: query + name: query + schema: + type: string + - description: Only messages on or after this YYYY-MM-DD date + in: query + name: after + schema: + type: string + - description: Only messages before this YYYY-MM-DD date + in: query + name: before schema: - description: Collection name type: string + - description: Maximum messages to sync + in: query + name: limit + schema: + format: int64 + type: integer + - description: Ignore checkpoints and start fresh + in: query + name: noresume + schema: + type: boolean + - description: IMAP folder names to include (repeatable) + in: query + name: folder + schema: + items: + type: string + type: array + - description: IMAP folder names to exclude (repeatable) + in: query + name: skip-folder + schema: + items: + type: string + type: array responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/MutationResult" + $ref: "#/components/schemas/CLISyncEvent" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Delete a CLI collection + summary: Run CLI full sync tags: - - CLI - /api/v1/cli/collections/{name}/sources: - delete: - operationId: removeCLICollectionSources + - API + /api/v1/cli/verify: + post: + operationId: verifyCLI parameters: - - description: Collection name - in: path - name: name + - description: Account email to verify + in: query + name: email required: true schema: - description: Collection name type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/SourcesRequest" - required: true + - description: Number of messages to sample for MIME verification + in: query + name: sample + schema: + format: int64 + type: integer + - description: Skip SQLite integrity check + in: query + name: skip_db_check + schema: + type: boolean + - description: Emit JSON summary output + in: query + name: json + schema: + type: boolean responses: "200": content: - application/json: + application/x-ndjson: schema: - $ref: "#/components/schemas/MutationResult" + $ref: "#/components/schemas/CLIVerifyEvent" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Remove accounts from a CLI collection + summary: Verify the CLI archive against Gmail tags: - - CLI - patch: - operationId: addCLICollectionSources + - API + /api/v1/communication-services: + get: + description: Lists the small open service catalog without pagination, including aliases and normalization policy. + operationId: listCommunicationServices parameters: - - description: Collection name - in: path - name: name - required: true + - description: Include inactive catalog entries + in: query + name: include_inactive schema: - description: Collection name - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/SourcesRequest" - required: true + default: false + type: boolean responses: "200": content: application/json: schema: - $ref: "#/components/schemas/MutationResult" + $ref: "#/components/schemas/CommunicationServicesResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Not Found - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Add accounts to a CLI collection + summary: List communication services tags: - - CLI - /api/v1/cli/deduplicate/plan: + - API post: - operationId: planCLIDeduplicate + description: Registers an unknown or custom service without a schema migration. Re-registering a slug is idempotent. + operationId: createCommunicationService requestBody: content: application/json: schema: - $ref: "#/components/schemas/CLIDeduplicatePlanRequest" + $ref: "#/components/schemas/CreateCommunicationServiceRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIDeduplicatePlanResponse" + $ref: "#/components/schemas/CommunicationService" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error + $ref: "#/components/schemas/ErrorResponse" + description: Error "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Plan interactive deduplication for CLI use + summary: Register a communication service tags: - - CLI - /api/v1/cli/delete-deduped: + - API + /api/v1/content/remote-image: post: - operationId: executeCLIDeleteDeduped + operationId: getRemoteImage requestBody: content: application/json: schema: - $ref: "#/components/schemas/CliDeleteDedupedExecuteRequest" + $ref: "#/components/schemas/RemoteImageRequest" required: true responses: "200": content: - application/json: + image/*: schema: - $ref: "#/components/schemas/CliDeleteDedupedExecuteResponse" + format: binary + type: string + x-contentMediaType: application/octet-stream description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "409": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Conflict - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "415": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "502": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Delete dedup-hidden messages for CLI use + summary: Fetch a consented remote mail image through the SSRF-hardened daemon proxy tags: - - CLI - /api/v1/cli/delete-deduped/plan: - post: - operationId: planCLIDeleteDeduped - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CliDeleteDedupedPlanRequest" - required: true + - API + /api/v1/conversations/{id}: + get: + operationId: getConversation + parameters: + - description: Conversation ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Selected message ID anchoring the chronological window + in: query + name: anchor + required: true + schema: + format: int64 + type: integer + - description: Messages before the anchor (default 25, max 50) + in: query + name: before + schema: + format: int64 + type: integer + - description: Messages after the anchor (default 25, max 50) + in: query + name: after + schema: + format: int64 + type: integer + - description: Lower UTC bound, inclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) + in: query + name: start + schema: + type: string + - description: Upper UTC bound, exclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) + in: query + name: end + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliDeleteDedupedPlanResponse" + $ref: "#/components/schemas/ConversationResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Plan deletion of dedup-hidden messages for CLI use + summary: Get a bounded containing conversation tags: - - CLI - /api/v1/cli/delete-staged/plan: - post: - operationId: planCLIDeleteStaged - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CLIDeleteStagedPlanRequest" - required: true + - API + /api/v1/deletions: + get: + operationId: listDeletions + parameters: + - description: Filter manifests by status (pending, in_progress, completed, failed, cancelled) + in: query + name: status + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIDeleteStagedPlanResponse" + $ref: "#/components/schemas/ListDeletionsResponse" description: OK default: content: @@ -7425,24 +9473,57 @@ paths: description: Error security: - apiKey: [] - summary: Plan CLI staged deletion execution + summary: List staged deletion manifests tags: - API - /api/v1/cli/deletion-manifests: post: - operationId: createCLIDeletionManifest + operationId: stageDeletion requestBody: content: application/json: schema: - $ref: "#/components/schemas/Manifest" + $ref: "#/components/schemas/StageDeletionRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIDeletionManifestResponse" + $ref: "#/components/schemas/StageDeletionResponse" + description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/StageDeletionResponse" + description: Created + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: Stage messages for deletion + tags: + - API + /api/v1/deletions/{id}: + delete: + operationId: cancelDeletion + parameters: + - description: Deletion manifest ID + in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/CancelDeletionResponse" description: OK default: content: @@ -7452,24 +9533,24 @@ paths: description: Error security: - apiKey: [] - summary: Create a staged deletion manifest + summary: Cancel a staged deletion manifest tags: - API - /api/v1/cli/embeddings/plan: - post: - operationId: planCLIEmbeddings - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/CLIEmbeddingsPlanRequest" - required: true + get: + operationId: getDeletion + parameters: + - description: Deletion manifest ID + in: path + name: id + required: true + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CLIEmbeddingsPlanResponse" + $ref: "#/components/schemas/DeletionManifestDetail" description: OK default: content: @@ -7479,182 +9560,181 @@ paths: description: Error security: - apiKey: [] - summary: Plan CLI embeddings management + summary: Inspect a staged deletion manifest tags: - API - /api/v1/cli/identities: - delete: - operationId: removeCLIIdentity + /api/v1/domains/search: + post: + operationId: searchDomains requestBody: content: application/json: schema: - $ref: "#/components/schemas/RemoveRequest" + $ref: "#/components/schemas/IdentitySearchHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/RemoveResult" + $ref: "#/components/schemas/DomainSearchHTTPResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Remove a confirmed identifier from an account identity + summary: Search analytical domains tags: - - CLI + - Exploration + /api/v1/domains/{domain}: get: - operationId: listCLIIdentities + operationId: getDomain parameters: - - description: Restrict to a single account - explode: false - in: query - name: account - schema: - description: Restrict to a single account - type: string - - description: Restrict to all member accounts of one collection - explode: false - in: query - name: collection + - description: Exact normalized domain fact + in: path + name: domain + required: true schema: - description: Restrict to all member accounts of one collection type: string - - description: Restrict to one source by numeric ID - explode: false - in: query - name: source_id - schema: - description: Restrict to one source by numeric ID - format: int64 - type: integer - - description: For account scope, return only the primary source instead of related sources - explode: false - in: query - name: primary_only - schema: - description: For account scope, return only the primary source instead of related sources - type: boolean responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliIdentitiesResponse" + $ref: "#/components/schemas/DomainSummary" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": - content: - application/json: - schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: List confirmed account identities + summary: Get one analytical domain tags: - - CLI + - API + /api/v1/domains/{domain}/files/search: post: - operationId: addCLIIdentity + operationId: searchDomainFiles + parameters: + - description: Exact normalized domain fact + in: path + name: domain + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/AddRequest" + $ref: "#/components/schemas/FileSearchHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/AddResult" + $ref: "#/components/schemas/FileSearchHTTPResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Add a confirmed identifier to an account identity + summary: Search one domain's analytical files tags: - - CLI - /api/v1/cli/identities/discover: + - Exploration + /api/v1/domains/{domain}/summary: post: - operationId: discoverCLIIdentities + operationId: getDomainContextSummary + parameters: + - description: Exact normalized domain fact + in: path + name: domain + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/DiscoverRequest" + $ref: "#/components/schemas/ExploreHTTPRequest" required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/DiscoverEvent" + $ref: "#/components/schemas/DomainContextSummaryHTTPResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -7663,92 +9743,106 @@ paths: description: Error security: - apiKey: [] - summary: Discover source-scoped account identities + summary: Get one domain's contextual analytical summary tags: - - API - /api/v1/cli/identities/import: + - Exploration + /api/v1/domains/{domain}/timeline: post: - operationId: importCLIIdentities + operationId: getDomainTimeline + parameters: + - description: Exact normalized domain fact + in: path + name: domain + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/ImportRequest" + $ref: "#/components/schemas/ExploreHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ImportResult" + $ref: "#/components/schemas/ExploreHTTPResponse" description: OK "400": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Bad Request - "422": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Unprocessable Entity - "500": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Internal Server Error - "503": + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable + default: content: application/json: schema: - $ref: "#/components/schemas/ApiHTTPError" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error security: - apiKey: [] - summary: Preview or apply parsed source-scoped identities + summary: Get one domain's canonical activity timeline tags: - - CLI - /api/v1/cli/init-db: + - Exploration + /api/v1/employments: post: - operationId: initCLIArchive + operationId: createEmployment + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EmploymentBody" + required: true responses: - "200": + "201": content: application/json: schema: - $ref: "#/components/schemas/CliInitDBResponse" - description: OK - default: + $ref: "#/components/schemas/Employment" + description: Created + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + Location: + description: Canonical URL of the created employment record + schema: + type: string + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Initialize the archive for CLI use - tags: - - API - /api/v1/cli/message: - get: - operationId: getCLIMessage - parameters: - - description: Message numeric ID or source message ID - in: query - name: id - required: true - schema: - type: string - responses: - "200": + "409": content: application/json: schema: - $ref: "#/components/schemas/CliMessageResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -7757,47 +9851,48 @@ paths: description: Error security: - apiKey: [] - summary: Get one message for CLI output + summary: Create an employment record tags: - API - /api/v1/cli/message/raw: - get: - operationId: getCLIMessageRaw + /api/v1/employments/{id}: + delete: + operationId: deleteEmployment parameters: - - description: Message numeric ID or source message ID - in: query + - description: Employment ID + in: path name: id required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true schema: type: string responses: - "200": - content: - message/rfc822: - schema: - format: binary - type: string - x-contentMediaType: application/octet-stream - description: OK + "204": + description: No Content "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + "428": content: application/json: schema: @@ -7817,40 +9912,49 @@ paths: description: Error security: - apiKey: [] - summary: Get one raw message for CLI export + summary: Delete an employment record tags: - API - /api/v1/cli/rebuild-fts: - post: - operationId: rebuildCLIFTS + get: + operationId: getEmployment + parameters: + - description: Employment ID + in: path + name: id + required: true + schema: + format: int64 + type: integer responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CliRebuildFTSEvent" + $ref: "#/components/schemas/Employment" description: OK - default: + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Rebuild the CLI full-text search index - tags: - - API - /api/v1/cli/repair-encoding: - post: - operationId: repairEncodingCLI - responses: - "200": + "404": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLIRepairEncodingEvent" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -7859,25 +9963,73 @@ paths: description: Error security: - apiKey: [] - summary: Repair CLI archive encoding + summary: Get an employment record tags: - API - /api/v1/cli/run: - post: - operationId: runCLI + patch: + operationId: patchEmployment + parameters: + - description: Employment ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/CLIRunRequest" + $ref: "#/components/schemas/EmploymentBody" required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLIRunEvent" + $ref: "#/components/schemas/Employment" description: OK + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -7886,85 +10038,74 @@ paths: description: Error security: - apiKey: [] - summary: Run an allowlisted CLI command + summary: Replace an employment record's mutable fields tags: - API - /api/v1/cli/search: - get: - operationId: searchCLI + /api/v1/employments/{id}/end: + post: + operationId: endEmployment parameters: - - description: Search query - in: query - name: q + - description: Employment ID + in: path + name: id required: true - schema: - type: string - - description: Maximum number of rows to return - in: query - name: limit schema: format: int64 type: integer - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer - - description: Message type filter; repeat or comma-separate for multiple values - in: query - name: message_type - schema: - type: string - - description: Restrict to one account/source - in: query - name: account - schema: - type: string - - description: Restrict to one collection - in: query - name: collection + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EndEmploymentBody" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CliSearchResponse" - description: OK - default: + $ref: "#/components/schemas/Employment" + description: OK + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Search messages for CLI output - tags: - - API - /api/v1/cli/stats: - get: - operationId: getCLIStats - parameters: - - description: Restrict to one account/source - in: query - name: account - schema: - type: string - - description: Restrict to one collection - in: query - name: collection - schema: - type: string - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/CliStatsResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -7973,39 +10114,68 @@ paths: description: Error security: - apiKey: [] - summary: Get CLI-compatible archive statistics + summary: End an employment record tags: - API - /api/v1/cli/sync: + /api/v1/employments/{id}/primary: post: - operationId: syncCLI + operationId: setPrimaryEmployment parameters: - - description: Account email or display name to sync - in: query - name: email - schema: - type: string - - description: IMAP folder names to include (repeatable) - in: query - name: folder + - description: Employment ID + in: path + name: id + required: true schema: - items: - type: string - type: array - - description: IMAP folder names to exclude (repeatable) - in: query - name: skip-folder + format: int64 + type: integer + - description: Strong ETag returned by the latest employment read + in: header + name: If-Match + required: true schema: - items: - type: string - type: array + type: string responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLISyncEvent" + $ref: "#/components/schemas/Employment" description: OK + headers: + ETag: + description: Strong employment revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8014,65 +10184,45 @@ paths: description: Error security: - apiKey: [] - summary: Run CLI incremental sync + summary: Set the primary current employment tags: - API - /api/v1/cli/sync-full: + /api/v1/explore: post: - operationId: syncFullCLI - parameters: - - description: Account email or display name to sync - in: query - name: email - schema: - type: string - - description: Gmail search query - in: query - name: query - schema: - type: string - - description: Only messages on or after this YYYY-MM-DD date - in: query - name: after - schema: - type: string - - description: Only messages before this YYYY-MM-DD date - in: query - name: before - schema: - type: string - - description: Maximum messages to sync - in: query - name: limit - schema: - format: int64 - type: integer - - description: Ignore checkpoints and start fresh - in: query - name: noresume - schema: - type: boolean - - description: IMAP folder names to include (repeatable) - in: query - name: folder - schema: - items: - type: string - type: array - - description: IMAP folder names to exclude (repeatable) - in: query - name: skip-folder - schema: - items: - type: string - type: array + operationId: explore + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ExploreHTTPRequest" + required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLISyncEvent" + $ref: "#/components/schemas/ExploreHTTPResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8081,70 +10231,71 @@ paths: description: Error security: - apiKey: [] - summary: Run CLI full sync + summary: Explore canonical archive entries tags: - - API - /api/v1/cli/verify: + - Exploration + /api/v1/explore/files: post: - operationId: verifyCLI - parameters: - - description: Account email to verify - in: query - name: email - required: true - schema: - type: string - - description: Number of messages to sample for MIME verification - in: query - name: sample - schema: - format: int64 - type: integer - - description: Skip SQLite integrity check - in: query - name: skip_db_check - schema: - type: boolean - - description: Emit JSON summary output - in: query - name: json - schema: - type: boolean + operationId: listExploreFiles + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ExploreFilesHTTPRequest" + required: true responses: "200": content: - application/x-ndjson: + application/json: schema: - $ref: "#/components/schemas/CLIVerifyEvent" + $ref: "#/components/schemas/ExploreFilesHTTPResponse" description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Verify the CLI archive against Gmail - tags: - - API - /api/v1/communication-services: - get: - description: Lists the small open service catalog without pagination, including aliases and normalization policy. - operationId: listCommunicationServices - parameters: - - description: Include inactive catalog entries - in: query - name: include_inactive - schema: - default: false - type: boolean + security: + - apiKey: [] + summary: List bounded chronological attachment facts + tags: + - Exploration + /api/v1/explore/groups: + post: + operationId: exploreGroups + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ExploreGroupsHTTPRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CommunicationServicesResponse" + $ref: "#/components/schemas/ExploreGroupsHTTPResponse" description: OK "400": content: @@ -8152,12 +10303,20 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8166,24 +10325,24 @@ paths: description: Error security: - apiKey: [] - summary: List communication services + summary: Group canonical archive entries tags: - - API + - Exploration + /api/v1/explore/match-counts: post: - description: Registers an unknown or custom service without a schema migration. Re-registering a slug is idempotent. - operationId: createCommunicationService + operationId: countExploreMatches requestBody: content: application/json: schema: - $ref: "#/components/schemas/CreateCommunicationServiceRequest" + $ref: "#/components/schemas/ExploreMatchCountsRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CommunicationService" + $ref: "#/components/schemas/ExploreMatchCountsResponse" description: OK "400": content: @@ -8191,12 +10350,6 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error "409": content: application/json: @@ -8207,8 +10360,10 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8217,26 +10372,24 @@ paths: description: Error security: - apiKey: [] - summary: Register a communication service + summary: Count exact lexical matches in visible rows tags: - - API - /api/v1/content/remote-image: + - Exploration + /api/v1/explore/preflight: post: - operationId: getRemoteImage + operationId: preflightExploreSelection requestBody: content: application/json: schema: - $ref: "#/components/schemas/RemoteImageRequest" + $ref: "#/components/schemas/ExplorePreflightRequest" required: true responses: "200": content: - image/*: + application/json: schema: - format: binary - type: string - x-contentMediaType: application/octet-stream + $ref: "#/components/schemas/ExplorePreflightResponse" description: OK "400": content: @@ -8244,24 +10397,20 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "415": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "502": + "503": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8270,83 +10419,45 @@ paths: description: Error security: - apiKey: [] - summary: Fetch a consented remote mail image through the SSRF-hardened daemon proxy + summary: Preflight a revision-pinned analytical selection tags: - - API - /api/v1/conversations/{id}: - get: - operationId: getConversation - parameters: - - description: Conversation ID - in: path - name: id - required: true - schema: - format: int64 - type: integer - - description: Selected message ID anchoring the chronological window - in: query - name: anchor - required: true - schema: - format: int64 - type: integer - - description: Messages before the anchor (default 25, max 50) - in: query - name: before - schema: - format: int64 - type: integer - - description: Messages after the anchor (default 25, max 50) - in: query - name: after - schema: - format: int64 - type: integer - - description: Lower UTC bound, inclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) - in: query - name: start - schema: - type: string - - description: Upper UTC bound, exclusive (RFC3339). Restricts the window, before/after counts, and has_before/has_after to messages in [start, end) - in: query - name: end - schema: - type: string + - Exploration + /api/v1/files/groups: + post: + operationId: groupFiles + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/FileGroupsHTTPRequest" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ConversationResponse" + $ref: "#/components/schemas/FileGroupsHTTPResponse" description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Get a bounded containing conversation - tags: - - API - /api/v1/deletions: - get: - operationId: listDeletions - parameters: - - description: Filter manifests by status (pending, in_progress, completed, failed, cancelled) - in: query - name: status - schema: - type: string - responses: - "200": + "409": content: application/json: schema: - $ref: "#/components/schemas/ListDeletionsResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8355,30 +10466,45 @@ paths: description: Error security: - apiKey: [] - summary: List staged deletion manifests + summary: Group analytical files tags: - - API + - Exploration + /api/v1/files/search: post: - operationId: stageDeletion + operationId: searchFiles requestBody: content: application/json: schema: - $ref: "#/components/schemas/StageDeletionRequest" + $ref: "#/components/schemas/FileSearchHTTPRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/StageDeletionResponse" + $ref: "#/components/schemas/FileSearchHTTPResponse" description: OK - "201": + "400": content: application/json: schema: - $ref: "#/components/schemas/StageDeletionResponse" - description: Created + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" + - $ref: "#/components/schemas/ErrorResponse" + description: Service Unavailable default: content: application/json: @@ -8387,25 +10513,26 @@ paths: description: Error security: - apiKey: [] - summary: Stage messages for deletion + summary: Search analytical files tags: - - API - /api/v1/deletions/{id}: - delete: - operationId: cancelDeletion + - Exploration + /api/v1/files/{id}: + get: + operationId: getFile parameters: - - description: Deletion manifest ID + - description: File attachment ID in: path name: id required: true schema: - type: string + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/CancelDeletionResponse" + $ref: "#/components/schemas/FileMetadataResponse" description: OK default: content: @@ -8415,59 +10542,48 @@ paths: description: Error security: - apiKey: [] - summary: Cancel a staged deletion manifest + summary: Get authoritative file metadata tags: - API + /api/v1/files/{id}/content: get: - operationId: getDeletion + operationId: getFileContent parameters: - - description: Deletion manifest ID + - description: File attachment ID in: path name: id required: true schema: - type: string + format: int64 + type: integer responses: "200": content: - application/json: + "*/*": schema: - $ref: "#/components/schemas/DeletionManifestDetail" + format: binary + type: string + x-contentMediaType: application/octet-stream description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Inspect a staged deletion manifest - tags: - - API - /api/v1/domains/search: - post: - operationId: searchDomains - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/IdentitySearchHTTPRequest" - required: true - responses: - "200": + "401": content: application/json: schema: - $ref: "#/components/schemas/DomainSearchHTTPResponse" - description: OK - "400": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "500": content: application/json: schema: @@ -8477,10 +10593,8 @@ paths: content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8489,25 +10603,18 @@ paths: description: Error security: - apiKey: [] - summary: Search analytical domains + summary: Download content for one authoritative file row tags: - - Exploration - /api/v1/domains/{domain}: + - API + /api/v1/health: get: - operationId: getDomain - parameters: - - description: Exact normalized domain fact - in: path - name: domain - required: true - schema: - type: string + operationId: getHealth responses: "200": content: application/json: schema: - $ref: "#/components/schemas/DomainSummary" + $ref: "#/components/schemas/HealthResponse" description: OK default: content: @@ -8517,52 +10624,25 @@ paths: description: Error security: - apiKey: [] - summary: Get one analytical domain + summary: Get authenticated health details tags: - API - /api/v1/domains/{domain}/files/search: + /api/v1/identity/links: post: - operationId: searchDomainFiles - parameters: - - description: Exact normalized domain fact - in: path - name: domain - required: true - schema: - type: string + operationId: linkIdentityParticipants requestBody: content: application/json: schema: - $ref: "#/components/schemas/FileSearchHTTPRequest" + $ref: "#/components/schemas/IdentityLinkRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/FileSearchHTTPResponse" + $ref: "#/components/schemas/IdentityLinkResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": - content: - application/json: - schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable default: content: application/json: @@ -8571,52 +10651,58 @@ paths: description: Error security: - apiKey: [] - summary: Search one domain's analytical files + summary: Assert two participants are the same person tags: - - Exploration - /api/v1/domains/{domain}/summary: + - API + /api/v1/identity/unlinks: post: - operationId: getDomainContextSummary - parameters: - - description: Exact normalized domain fact - in: path - name: domain - required: true - schema: - type: string + operationId: unlinkIdentityParticipants requestBody: content: application/json: schema: - $ref: "#/components/schemas/ExploreHTTPRequest" + $ref: "#/components/schemas/IdentityLinkRequest" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/DomainContextSummaryHTTPResponse" + $ref: "#/components/schemas/IdentityLinkResponse" description: OK - "400": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + security: + - apiKey: [] + summary: Remove a link edge between two participants + tags: + - API + /api/v1/import/meeting: + post: + operationId: importMeeting + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MeetingImportRequest" + required: true + responses: + "200": content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + $ref: "#/components/schemas/MeetingImportResponse" + description: OK + "201": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/MeetingImportResponse" + description: Created default: content: application/json: @@ -8625,52 +10711,47 @@ paths: description: Error security: - apiKey: [] - summary: Get one domain's contextual analytical summary + summary: Import one meeting tags: - - Exploration - /api/v1/domains/{domain}/timeline: - post: - operationId: getDomainTimeline + - API + /api/v1/integrations/tasks/search: + get: + operationId: searchIntegrationTasks parameters: - - description: Exact normalized domain fact - in: path - name: domain + - description: Task title search within the configured project + in: query + name: q required: true schema: type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreHTTPRequest" - required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreHTTPResponse" + $ref: "#/components/schemas/TaskSearchResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: Search tasks in the configured project + tags: + - API + /api/v1/integrations/tasks/status: + get: + operationId: getTaskIntegrationStatus + responses: + "200": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/TaskIntegrationStatusResponse" + description: OK default: content: application/json: @@ -8679,45 +10760,53 @@ paths: description: Error security: - apiKey: [] - summary: Get one domain's canonical activity timeline + summary: Get task integration capability status tags: - - Exploration - /api/v1/explore: + - API + /api/v1/integrations/tasks/test: post: - operationId: explore - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreHTTPRequest" - required: true + operationId: testTaskIntegration responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreHTTPResponse" + $ref: "#/components/schemas/TaskIntegrationStatusResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: Test task integration discovery, authentication, capabilities, and project + tags: + - API + /api/v1/messages: + get: + operationId: listMessages + parameters: + - description: One-based page number (default 1; values below 1 are clamped to 1). Non-numeric values are rejected with 400. + in: query + name: page + schema: + format: int64 + type: integer + - description: Page size (default 20, max 100; out-of-range values are clamped). Non-numeric values are rejected with 400. + in: query + name: page_size + schema: + format: int64 + type: integer + responses: + "200": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/MessageListResponse" + description: OK default: content: application/json: @@ -8726,32 +10815,50 @@ paths: description: Error security: - apiKey: [] - summary: Explore canonical archive entries + summary: List messages tags: - - Exploration - /api/v1/explore/files: - post: - operationId: listExploreFiles - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreFilesHTTPRequest" - required: true + - API + /api/v1/messages/changes: + get: + operationId: listChangedMessages + parameters: + - description: "Opaque cursor from the next_cursor of the previous response, sent back verbatim. Do not parse, construct, compare, or order it; its contents may change without notice. Omit, or send it empty, to start from the beginning of the archive. The token is not authenticated: the server does not sign it and cannot tell one it issued from a well-formed one you built, so a fabricated cursor naming this archive is accepted and simply moves your own position. Rejected with 400 invalid_cursor, rather than read as the beginning: a token the server cannot read, one carrying a cursor format this build does not speak, and one issued against a different archive" + in: query + name: cursor + schema: + type: string + - description: Maximum number of rows to return (default 100, max 500; values below 1 fall back to the default) + in: query + name: limit + schema: + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreFilesHTTPResponse" - description: OK - "400": + $ref: "#/components/schemas/ChangesResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "429": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "500": content: application/json: schema: @@ -8761,10 +10868,8 @@ paths: content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8773,45 +10878,279 @@ paths: description: Error security: - apiKey: [] - summary: List bounded chronological attachment facts + summary: List messages whose content changed since a cursor tags: - - Exploration - /api/v1/explore/groups: - post: - operationId: exploreGroups - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreGroupsHTTPRequest" - required: true + - API + /api/v1/messages/filter: + get: + operationId: filterMessages + parameters: + - description: Sender email/address filter + in: query + name: sender + schema: + type: string + - description: Sender display-name filter + in: query + name: sender_name + schema: + type: string + - description: Recipient email/address filter + in: query + name: recipient + schema: + type: string + - description: Recipient display-name filter + in: query + name: recipient_name + schema: + type: string + - description: Domain filter + in: query + name: domain + schema: + type: string + - description: Label filter + in: query + name: label + schema: + type: string + - description: Message type filter + in: query + name: message_type + schema: + type: string + - description: Named time period + in: query + name: time_period + schema: + type: string + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Conversation ID + in: query + name: conversation_id + schema: + format: int64 + type: integer + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + - description: Comma-separated aggregate view names to match empty values + in: query + name: empty_targets + schema: + type: string + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + - description: Maximum number of rows to return (default and max 500; larger values are clamped) + in: query + name: limit + schema: + format: int64 + type: integer + - description: "Sort field: date, size, or subject" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/FilteredMessagesResponse" + description: OK + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List filtered messages + tags: + - API + /api/v1/messages/gmail-ids: + get: + operationId: getGmailIDsByFilter + parameters: + - description: Sender email/address filter + in: query + name: sender + schema: + type: string + - description: Sender display-name filter + in: query + name: sender_name + schema: + type: string + - description: Recipient email/address filter + in: query + name: recipient + schema: + type: string + - description: Recipient display-name filter + in: query + name: recipient_name + schema: + type: string + - description: Domain filter + in: query + name: domain + schema: + type: string + - description: Label filter + in: query + name: label + schema: + type: string + - description: Message type filter + in: query + name: message_type + schema: + type: string + - description: Named time period + in: query + name: time_period + schema: + type: string + - description: Time bucket granularity + in: query + name: time_granularity + schema: + type: string + - description: Conversation ID + in: query + name: conversation_id + schema: + format: int64 + type: integer + - description: Source ID + in: query + name: source_id + schema: + format: int64 + type: integer + - description: Only include messages with attachments + in: query + name: attachments_only + schema: + type: boolean + - description: Exclude deleted messages + in: query + name: hide_deleted + schema: + type: boolean + - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: after + schema: + type: string + - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) + in: query + name: before + schema: + type: string + - description: Comma-separated aggregate view names to match empty values + in: query + name: empty_targets + schema: + type: string + - description: Zero-based row offset + in: query + name: offset + schema: + format: int64 + type: integer + - description: Maximum number of rows to return (default and max 500; larger values are clamped) + in: query + name: limit + schema: + format: int64 + type: integer + - description: "Sort field: date, size, or subject" + in: query + name: sort + schema: + type: string + - description: "Sort direction: asc or desc" + in: query + name: direction + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ExploreGroupsHTTPResponse" + $ref: "#/components/schemas/GmailIDsResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": + security: + - apiKey: [] + summary: List Gmail message IDs matching a filter + tags: + - API + /api/v1/messages/{id}: + get: + operationId: getMessage + parameters: + - description: Message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + responses: + "200": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/MessageDetail" + description: OK default: content: application/json: @@ -8820,24 +11159,34 @@ paths: description: Error security: - apiKey: [] - summary: Group canonical archive entries + summary: Get one message tags: - - Exploration - /api/v1/explore/match-counts: - post: - operationId: countExploreMatches - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExploreMatchCountsRequest" - required: true + - API + /api/v1/messages/{id}/inline: + get: + operationId: getMessageInlinePart + parameters: + - description: Message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Inline MIME Content-ID + in: query + name: cid + required: true + schema: + type: string responses: "200": content: - application/json: + application/octet-stream: schema: - $ref: "#/components/schemas/ExploreMatchCountsResponse" + format: binary + type: string + x-contentMediaType: application/octet-stream description: OK "400": content: @@ -8845,54 +11194,31 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "401": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "503": - content: - application/json: - schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable - default: + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Count exact lexical matches in visible rows - tags: - - Exploration - /api/v1/explore/preflight: - post: - operationId: preflightExploreSelection - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ExplorePreflightRequest" - required: true - responses: - "200": + "415": content: application/json: schema: - $ref: "#/components/schemas/ExplorePreflightResponse" - description: OK - "400": + $ref: "#/components/schemas/ErrorResponse" + description: Error + "500": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "409": + "501": content: application/json: schema: @@ -8902,10 +11228,8 @@ paths: content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -8914,45 +11238,27 @@ paths: description: Error security: - apiKey: [] - summary: Preflight a revision-pinned analytical selection + summary: Get an inline MIME part tags: - - Exploration - /api/v1/files/groups: - post: - operationId: groupFiles - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/FileGroupsHTTPRequest" - required: true + - API + /api/v1/messages/{id}/tasks: + get: + operationId: listMessageTasks + parameters: + - description: Archived email message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/FileGroupsHTTPResponse" + $ref: "#/components/schemas/TaskLinkLookupResponse" description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": - content: - application/json: - schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable default: content: application/json: @@ -8961,45 +11267,38 @@ paths: description: Error security: - apiKey: [] - summary: Group analytical files + summary: List tasks linked to an archived email tags: - - Exploration - /api/v1/files/search: + - API post: - operationId: searchFiles + operationId: createOrLinkMessageTask + parameters: + - description: Archived email message ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Browser-generated retry-stable request ID + in: header + name: X-Request-Id + required: true + schema: + type: string requestBody: content: application/json: schema: - $ref: "#/components/schemas/FileSearchHTTPRequest" + $ref: "#/components/schemas/TaskLinkMutationRequest" required: true responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/FileSearchHTTPResponse" - description: OK - "400": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "409": - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - description: Error - "503": + "201": content: application/json: schema: - anyOf: - - $ref: "#/components/schemas/ExploreCacheUnavailableResponse" - - $ref: "#/components/schemas/ErrorResponse" - description: Service Unavailable + $ref: "#/components/schemas/TaskLinkMutationResponse" + description: Created default: content: application/json: @@ -9008,26 +11307,32 @@ paths: description: Error security: - apiKey: [] - summary: Search analytical files + summary: Create or link a task for an archived email tags: - - Exploration - /api/v1/files/{id}: - get: - operationId: getFile + - API + /api/v1/messages/{id}/tasks/{task_id}: + delete: + operationId: unlinkMessageTask parameters: - - description: File attachment ID + - description: Archived email message ID in: path name: id required: true schema: format: int64 type: integer + - description: External task ID + in: path + name: task_id + required: true + schema: + type: string responses: "200": content: application/json: schema: - $ref: "#/components/schemas/FileMetadataResponse" + $ref: "#/components/schemas/TaskLinkMutationResponse" description: OK default: content: @@ -9037,28 +11342,41 @@ paths: description: Error security: - apiKey: [] - summary: Get authoritative file metadata + summary: Unlink a task from an archived email tags: - API - /api/v1/files/{id}/content: + /api/v1/organizations: get: - operationId: getFileContent + operationId: listOrganizations parameters: - - description: File attachment ID - in: path - name: id - required: true + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer + - description: Include retired organizations + in: query + name: include_retired + schema: + type: boolean + - description: Normalized-name search + in: query + name: q schema: - format: int64 - type: integer + type: string responses: "200": content: - "*/*": + application/json: schema: - format: binary - type: string - x-contentMediaType: application/octet-stream + $ref: "#/components/schemas/OrganizationsResponse" description: OK "400": content: @@ -9066,19 +11384,48 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "503": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + security: + - apiKey: [] + summary: List organizations + tags: + - API + post: + operationId: createOrganization + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationCreateBody" + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/Organization" + description: Created + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + Location: + description: Canonical URL of the created organization + schema: + type: string + "400": content: application/json: schema: @@ -9098,46 +11445,49 @@ paths: description: Error security: - apiKey: [] - summary: Download content for one authoritative file row + summary: Create an organization tags: - API - /api/v1/health: + /api/v1/organizations/duplicate-suggestions: get: - operationId: getHealth + operationId: listOrganizationDuplicateSuggestions + parameters: + - description: Filter by status + in: query + name: status + schema: + type: string + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer responses: "200": content: application/json: schema: - $ref: "#/components/schemas/HealthResponse" + $ref: "#/components/schemas/OrganizationDuplicateSuggestionsResponse" description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Get authenticated health details - tags: - - API - /api/v1/identity/links: - post: - operationId: linkIdentityParticipants - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/IdentityLinkRequest" - required: true - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/IdentityLinkResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9146,25 +11496,25 @@ paths: description: Error security: - apiKey: [] - summary: Assert two participants are the same person + summary: List organization duplicate suggestions tags: - API - /api/v1/identity/unlinks: + /api/v1/organizations/duplicate-suggestions/refresh: post: - operationId: unlinkIdentityParticipants - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/IdentityLinkRequest" - required: true + operationId: refreshOrganizationDuplicateSuggestions responses: "200": content: application/json: schema: - $ref: "#/components/schemas/IdentityLinkResponse" + $ref: "#/components/schemas/RefreshOrganizationDuplicateSuggestionsResponse" description: OK + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9173,31 +11523,57 @@ paths: description: Error security: - apiKey: [] - summary: Remove a link edge between two participants + summary: Refresh organization duplicate suggestions tags: - API - /api/v1/import/meeting: + /api/v1/organizations/duplicate-suggestions/{id}/resolve: post: - operationId: importMeeting + operationId: resolveOrganizationDuplicateSuggestion + parameters: + - description: Duplicate suggestion ID + in: path + name: id + required: true + schema: + format: int64 + type: integer requestBody: content: application/json: schema: - $ref: "#/components/schemas/MeetingImportRequest" + $ref: "#/components/schemas/ResolveOrganizationDuplicateSuggestionBody" required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/MeetingImportResponse" + $ref: "#/components/schemas/OrganizationDuplicateSuggestion" description: OK - "201": + "400": content: application/json: schema: - $ref: "#/components/schemas/MeetingImportResponse" - description: Created + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9206,68 +11582,59 @@ paths: description: Error security: - apiKey: [] - summary: Import one meeting + summary: Resolve an organization duplicate suggestion tags: - API - /api/v1/integrations/tasks/search: - get: - operationId: searchIntegrationTasks + /api/v1/organizations/{id}: + delete: + operationId: deleteOrganization parameters: - - description: Task title search within the configured project - in: query - name: q + - description: Organization ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Strong ETag returned by the latest organization read + in: header + name: If-Match required: true schema: type: string responses: - "200": + "204": + description: No Content + "400": content: application/json: schema: - $ref: "#/components/schemas/TaskSearchResponse" - description: OK - default: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Search tasks in the configured project - tags: - - API - /api/v1/integrations/tasks/status: - get: - operationId: getTaskIntegrationStatus - responses: - "200": + "409": content: application/json: schema: - $ref: "#/components/schemas/TaskIntegrationStatusResponse" - description: OK - default: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: Get task integration capability status - tags: - - API - /api/v1/integrations/tasks/test: - post: - operationId: testTaskIntegration - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/TaskIntegrationStatusResponse" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9276,22 +11643,16 @@ paths: description: Error security: - apiKey: [] - summary: Test task integration discovery, authentication, capabilities, and project + summary: Delete an organization without employment records tags: - API - /api/v1/messages: get: - operationId: listMessages + operationId: getOrganization parameters: - - description: One-based page number (default 1; values below 1 are clamped to 1). Non-numeric values are rejected with 400. - in: query - name: page - schema: - format: int64 - type: integer - - description: Page size (default 20, max 100; out-of-range values are clamped). Non-numeric values are rejected with 400. - in: query - name: page_size + - description: Organization ID + in: path + name: id + required: true schema: format: int64 type: integer @@ -9300,8 +11661,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/MessageListResponse" + $ref: "#/components/schemas/OrganizationProfile" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9310,50 +11694,62 @@ paths: description: Error security: - apiKey: [] - summary: List messages + summary: Get an organization tags: - API - /api/v1/messages/changes: - get: - operationId: listChangedMessages + patch: + operationId: patchOrganization parameters: - - description: "Opaque cursor from the next_cursor of the previous response, sent back verbatim. Do not parse, construct, compare, or order it; its contents may change without notice. Omit, or send it empty, to start from the beginning of the archive. The token is not authenticated: the server does not sign it and cannot tell one it issued from a well-formed one you built, so a fabricated cursor naming this archive is accepted and simply moves your own position. Rejected with 400 invalid_cursor, rather than read as the beginning: a token the server cannot read, one carrying a cursor format this build does not speak, and one issued against a different archive" - in: query - name: cursor - schema: - type: string - - description: Maximum number of rows to return (default 100, max 500; values below 1 fall back to the default) - in: query - name: limit + - description: Organization ID + in: path + name: id + required: true schema: format: int64 type: integer + - description: Strong ETag returned by the latest organization read + in: header + name: If-Match + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationBody" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ChangesResponse" + $ref: "#/components/schemas/Organization" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "429": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + "428": content: application/json: schema: @@ -9373,115 +11769,28 @@ paths: description: Error security: - apiKey: [] - summary: List messages whose content changed since a cursor + summary: Replace an organization's mutable fields tags: - API - /api/v1/messages/filter: + /api/v1/organizations/{id}/attributes: get: - operationId: filterMessages + operationId: listOrganizationAttributes parameters: - - description: Sender email/address filter - in: query - name: sender - schema: - type: string - - description: Sender display-name filter - in: query - name: sender_name - schema: - type: string - - description: Recipient email/address filter - in: query - name: recipient - schema: - type: string - - description: Recipient display-name filter - in: query - name: recipient_name - schema: - type: string - - description: Domain filter - in: query - name: domain - schema: - type: string - - description: Label filter - in: query - name: label - schema: - type: string - - description: Message type filter - in: query - name: message_type - schema: - type: string - - description: Named time period - in: query - name: time_period - schema: - type: string - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Conversation ID - in: query - name: conversation_id - schema: - format: int64 - type: integer - - description: Source ID - in: query - name: source_id + - description: Organization ID + in: path + name: id + required: true schema: format: int64 type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages + - description: Include superseded values in: query - name: hide_deleted + name: include_superseded schema: type: boolean - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - - description: Comma-separated aggregate view names to match empty values - in: query - name: empty_targets - schema: - type: string - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer - - description: Maximum number of rows to return (default and max 500; larger values are clamped) - in: query - name: limit - schema: - format: int64 - type: integer - - description: "Sort field: date, size, or subject" + - description: Restrict to one definition in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction + name: definition_slug schema: type: string responses: @@ -9489,163 +11798,26 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/FilteredMessagesResponse" + $ref: "#/components/schemas/OrganizationAttributesResponse" description: OK - default: + "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: List filtered messages - tags: - - API - /api/v1/messages/gmail-ids: - get: - operationId: getGmailIDsByFilter - parameters: - - description: Sender email/address filter - in: query - name: sender - schema: - type: string - - description: Sender display-name filter - in: query - name: sender_name - schema: - type: string - - description: Recipient email/address filter - in: query - name: recipient - schema: - type: string - - description: Recipient display-name filter - in: query - name: recipient_name - schema: - type: string - - description: Domain filter - in: query - name: domain - schema: - type: string - - description: Label filter - in: query - name: label - schema: - type: string - - description: Message type filter - in: query - name: message_type - schema: - type: string - - description: Named time period - in: query - name: time_period - schema: - type: string - - description: Time bucket granularity - in: query - name: time_granularity - schema: - type: string - - description: Conversation ID - in: query - name: conversation_id - schema: - format: int64 - type: integer - - description: Source ID - in: query - name: source_id - schema: - format: int64 - type: integer - - description: Only include messages with attachments - in: query - name: attachments_only - schema: - type: boolean - - description: Exclude deleted messages - in: query - name: hide_deleted - schema: - type: boolean - - description: Lower date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: after - schema: - type: string - - description: Upper date/time bound (RFC3339 or YYYY-MM-DD) - in: query - name: before - schema: - type: string - - description: Comma-separated aggregate view names to match empty values - in: query - name: empty_targets - schema: - type: string - - description: Zero-based row offset - in: query - name: offset - schema: - format: int64 - type: integer - - description: Maximum number of rows to return (default and max 500; larger values are clamped) - in: query - name: limit - schema: - format: int64 - type: integer - - description: "Sort field: date, size, or subject" - in: query - name: sort - schema: - type: string - - description: "Sort direction: asc or desc" - in: query - name: direction - schema: - type: string - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/GmailIDsResponse" - description: OK - default: + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - security: - - apiKey: [] - summary: List Gmail message IDs matching a filter - tags: - - API - /api/v1/messages/{id}: - get: - operationId: getMessage - parameters: - - description: Message ID - in: path - name: id - required: true - schema: - format: int64 - type: integer - responses: - "200": + "503": content: application/json: schema: - $ref: "#/components/schemas/MessageDetail" - description: OK + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9654,66 +11826,115 @@ paths: description: Error security: - apiKey: [] - summary: Get one message + summary: List organization typed attributes tags: - API - /api/v1/messages/{id}/inline: - get: - operationId: getMessageInlinePart + post: + operationId: setOrganizationAttribute parameters: - - description: Message ID + - description: Organization ID in: path name: id required: true schema: format: int64 type: integer - - description: Inline MIME Content-ID - in: query - name: cid - required: true - schema: - type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SetOrganizationAttributeBody" + required: true responses: "200": content: - application/octet-stream: + application/json: schema: - format: binary - type: string - x-contentMediaType: application/octet-stream + $ref: "#/components/schemas/OrganizationAttributeWrite" description: OK + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationAttributeWrite" + description: Created "400": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "401": + "404": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "404": + "409": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "415": + "503": content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "500": + default: content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" description: Error - "501": + security: + - apiKey: [] + summary: Set an organization typed attribute + tags: + - API + /api/v1/organizations/{id}/employments: + get: + operationId: listOrganizationEmployments + parameters: + - description: Organization ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Only current employments + in: query + name: current_only + schema: + type: boolean + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EmploymentsResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": content: application/json: schema: @@ -9733,14 +11954,14 @@ paths: description: Error security: - apiKey: [] - summary: Get an inline MIME part + summary: List an organization's employment records tags: - API - /api/v1/messages/{id}/tasks: + /api/v1/organizations/{id}/history: get: - operationId: listMessageTasks + operationId: getOrganizationHistory parameters: - - description: Archived email message ID + - description: Organization ID in: path name: id required: true @@ -9752,8 +11973,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TaskLinkLookupResponse" + $ref: "#/components/schemas/OrganizationProfile" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9762,22 +12006,23 @@ paths: description: Error security: - apiKey: [] - summary: List tasks linked to an archived email + summary: Get organization profile history tags: - API + /api/v1/organizations/{id}/merge: post: - operationId: createOrLinkMessageTask + operationId: mergeOrganization parameters: - - description: Archived email message ID + - description: Organization ID in: path name: id required: true schema: format: int64 type: integer - - description: Browser-generated retry-stable request ID + - description: Strong ETag returned by the latest organization read in: header - name: X-Request-Id + name: If-Match required: true schema: type: string @@ -9785,15 +12030,50 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TaskLinkMutationRequest" + $ref: "#/components/schemas/MergeOrganizationBody" required: true responses: - "201": + "200": content: application/json: schema: - $ref: "#/components/schemas/TaskLinkMutationResponse" - description: Created + $ref: "#/components/schemas/Organization" + description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9802,33 +12082,74 @@ paths: description: Error security: - apiKey: [] - summary: Create or link a task for an archived email + summary: Merge another organization into this organization tags: - API - /api/v1/messages/{id}/tasks/{task_id}: - delete: - operationId: unlinkMessageTask + /api/v1/organizations/{id}/profile: + put: + operationId: putOrganizationProfile parameters: - - description: Archived email message ID + - description: Organization ID in: path name: id required: true schema: format: int64 type: integer - - description: External task ID - in: path - name: task_id + - description: Strong ETag returned by the latest organization read + in: header + name: If-Match required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OrganizationProfileBody" + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/TaskLinkMutationResponse" + $ref: "#/components/schemas/OrganizationProfile" description: OK + headers: + ETag: + description: Strong organization revision tag for optimistic concurrency + schema: + type: string + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "428": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error default: content: application/json: @@ -9837,7 +12158,7 @@ paths: description: Error security: - apiKey: [] - summary: Unlink a task from an archived email + summary: Replace organization profile collections tags: - API /api/v1/people/search: @@ -10548,6 +12869,70 @@ paths: summary: Set a person's attribute value tags: - API + /api/v1/persons/{id}/employments: + get: + operationId: listPersonEmployments + parameters: + - description: Person ID + in: path + name: id + required: true + schema: + format: int64 + type: integer + - description: Only current employments + in: query + name: current_only + schema: + type: boolean + - description: Maximum results + in: query + name: limit + schema: + format: int64 + type: integer + - description: Results to skip + in: query + name: offset + schema: + format: int64 + type: integer + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/EmploymentsResponse" + description: OK + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + default: + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + description: Error + security: + - apiKey: [] + summary: List a person's employment history + tags: + - API /api/v1/persons/{id}/profile: get: description: Returns only current structured values at one person revision. Superseded values and archive observations are available from the separate history endpoint. diff --git a/web/src/lib/api/generated/schema.d.ts b/web/src/lib/api/generated/schema.d.ts index bc3d3788d..fd70f4477 100644 --- a/web/src/lib/api/generated/schema.d.ts +++ b/web/src/lib/api/generated/schema.d.ts @@ -923,6 +923,76 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/employments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create an employment record */ + post: operations["createEmployment"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/employments/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get an employment record */ + get: operations["getEmployment"]; + put?: never; + post?: never; + /** Delete an employment record */ + delete: operations["deleteEmployment"]; + options?: never; + head?: never; + /** Replace an employment record's mutable fields */ + patch: operations["patchEmployment"]; + trace?: never; + }; + "/api/v1/employments/{id}/end": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** End an employment record */ + post: operations["endEmployment"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/employments/{id}/primary": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set the primary current employment */ + post: operations["setPrimaryEmployment"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/explore": { parameters: { query?: never; @@ -1332,6 +1402,180 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/organizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List organizations */ + get: operations["listOrganizations"]; + put?: never; + /** Create an organization */ + post: operations["createOrganization"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/duplicate-suggestions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List organization duplicate suggestions */ + get: operations["listOrganizationDuplicateSuggestions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/duplicate-suggestions/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Refresh organization duplicate suggestions */ + post: operations["refreshOrganizationDuplicateSuggestions"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/duplicate-suggestions/{id}/resolve": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Resolve an organization duplicate suggestion */ + post: operations["resolveOrganizationDuplicateSuggestion"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get an organization */ + get: operations["getOrganization"]; + put?: never; + post?: never; + /** Delete an organization without employment records */ + delete: operations["deleteOrganization"]; + options?: never; + head?: never; + /** Replace an organization's mutable fields */ + patch: operations["patchOrganization"]; + trace?: never; + }; + "/api/v1/organizations/{id}/attributes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List organization typed attributes */ + get: operations["listOrganizationAttributes"]; + put?: never; + /** Set an organization typed attribute */ + post: operations["setOrganizationAttribute"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{id}/employments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List an organization's employment records */ + get: operations["listOrganizationEmployments"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{id}/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get organization profile history */ + get: operations["getOrganizationHistory"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{id}/merge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Merge another organization into this organization */ + post: operations["mergeOrganization"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{id}/profile": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Replace organization profile collections */ + put: operations["putOrganizationProfile"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/people/search": { parameters: { query?: never; @@ -1498,6 +1742,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/persons/{id}/employments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List a person's employment history */ + get: operations["listPersonEmployments"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/persons/{id}/profile": { parameters: { query?: never; @@ -2848,6 +3109,88 @@ export interface components { } & { [key: string]: unknown; }; + Employment: { + /** Format: int64 */ + address_id?: number; + /** Format: double */ + confidence?: number; + /** Format: date-time */ + created_at: string; + department?: string; + description?: string; + end_date?: components["schemas"]["PartialDate"]; + /** Format: int64 */ + id: number; + is_current: boolean; + is_primary: boolean; + location?: string; + /** Format: int64 */ + organization_id: number; + /** Format: int64 */ + person_id: number; + /** Format: int64 */ + revision: number; + role?: string; + source: string; + source_ref?: string; + start_date?: components["schemas"]["PartialDate"]; + title?: string; + /** Format: date-time */ + updated_at: string; + } & { + [key: string]: unknown; + }; + EmploymentBody: { + /** Format: int64 */ + address_id?: number | null; + /** Format: double */ + confidence?: number | null; + department?: string | null; + description?: string | null; + end_date?: string | null; + is_current?: boolean | null; + is_primary?: boolean | null; + location?: string | null; + /** Format: int64 */ + organization_id: number; + /** Format: int64 */ + person_id: number; + role?: string | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + start_date?: string | null; + title?: string | null; + }; + EmploymentProjectionResponse: { + department?: string; + /** Format: int64 */ + employment_id: number; + /** Format: int64 */ + organization_id: number; + organization_name: string; + role?: string; + title?: string; + vcard: components["schemas"]["EmploymentVCard"]; + } & { + [key: string]: unknown; + }; + EmploymentVCard: { + org?: string[] | null; + role?: string; + title?: string; + } & { + [key: string]: unknown; + }; + EmploymentsResponse: { + employments: components["schemas"]["Employment"][] | null; + projection?: components["schemas"]["EmploymentProjectionResponse"]; + } & { + [key: string]: unknown; + }; + EndEmploymentBody: { + end_date: string; + }; EntryRow: { /** Format: int64 */ anchor_message_id?: number; @@ -3440,6 +3783,12 @@ export interface components { email: string; name?: string; }; + MergeOrganizationBody: { + /** Format: int64 */ + losing_organization_id: number; + /** Format: int64 */ + losing_revision: number; + }; MessageDetail: { attachments: components["schemas"]["AttachmentInfo"][] | null; bcc?: string[] | null; @@ -3524,9 +3873,346 @@ export interface components { } & { [key: string]: unknown; }; - PartialDate: { + Organization: { + /** Format: date-time */ + created_at: string; + description?: string; /** Format: int64 */ - day?: number; + id: number; + kind: string; + /** Format: int64 */ + merged_into_id?: number; + name: string; + primary_domain?: string; + /** Format: date-time */ + retired_at?: string; + /** Format: int64 */ + revision: number; + /** Format: date-time */ + updated_at: string; + } & { + [key: string]: unknown; + }; + OrganizationAddress: { + address_kind: string; + country_code?: string; + country_name?: string; + envelope: components["schemas"]["ValueEnvelope"]; + extended_address?: string; + extended_components?: string; + free_text?: string; + geo_uri?: string; + label?: string; + locality?: string; + /** Format: int64 */ + organization_id: number; + original_value: string; + place_uri?: string; + post_office_box?: string; + postal_code?: string; + region?: string; + street_address?: string; + timezone?: string; + } & { + [key: string]: unknown; + }; + OrganizationAddressBody: { + address_kind: string; + country_code?: string | null; + country_name?: string | null; + extended_address?: string | null; + extended_components?: string | null; + free_text?: string | null; + geo_uri?: string | null; + label?: string | null; + locality?: string | null; + /** Format: int64 */ + ordinal?: number; + original_value?: string; + place_uri?: string | null; + post_office_box?: string | null; + postal_code?: string | null; + /** Format: int64 */ + pref?: number | null; + region?: string | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + street_address?: string | null; + timezone?: string | null; + vcard_altid?: string | null; + vcard_group?: string | null; + vcard_pid?: string[] | null; + vcard_prop_id?: string | null; + vcard_property?: string | null; + }; + OrganizationAttributeValue: { + /** Format: date-time */ + active_from: string; + /** Format: date-time */ + active_until?: string; + actor?: string; + /** Format: double */ + confidence?: number; + /** Format: date-time */ + created_at: string; + /** Format: int64 */ + definition_id: number; + definition_slug: string; + /** Format: int64 */ + id: number; + /** Format: int64 */ + ordinal: number; + /** Format: int64 */ + organization_id: number; + source: string; + source_ref?: string; + /** Format: date-time */ + superseded_at?: string; + value: components["schemas"]["AttributeValue"]; + } & { + [key: string]: unknown; + }; + OrganizationAttributeWrite: { + dry_run: boolean; + superseded?: components["schemas"]["OrganizationAttributeValue"]; + value?: components["schemas"]["OrganizationAttributeValue"]; + } & { + [key: string]: unknown; + }; + OrganizationAttributesResponse: { + values: components["schemas"]["OrganizationAttributeValue"][] | null; + } & { + [key: string]: unknown; + }; + OrganizationBody: { + description?: string | null; + /** @enum {string} */ + kind?: "company" | "nonprofit" | "school" | "government" | "household" | "other"; + name: string; + primary_domain?: string | null; + retired?: boolean; + }; + OrganizationCategory: { + category: string; + category_normalized: string; + envelope: components["schemas"]["ValueEnvelope"]; + /** Format: int64 */ + organization_id: number; + } & { + [key: string]: unknown; + }; + OrganizationCategoryBody: { + category: string; + /** Format: int64 */ + ordinal?: number; + /** Format: int64 */ + pref?: number | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + vcard_altid?: string | null; + vcard_group?: string | null; + vcard_pid?: string[] | null; + vcard_prop_id?: string | null; + vcard_property?: string | null; + }; + OrganizationContactPoint: { + address_kind: string; + envelope: components["schemas"]["ValueEnvelope"]; + normalization: string; + /** Format: int64 */ + normalization_version: number; + normalized_value: string; + /** Format: int64 */ + organization_id: number; + original_value: string; + scope_kind?: string; + scope_value?: string; + service_slug?: string; + uri?: string; + } & { + [key: string]: unknown; + }; + OrganizationContactPointBody: { + /** @enum {string} */ + contact_kind: "email" | "phone" | "username" | "impp" | "url" | "social" | "calendar" | "contact_uri" | "org_directory" | "language"; + /** Format: int64 */ + ordinal?: number; + original_value: string; + /** Format: int64 */ + pref?: number | null; + scope_kind?: string | null; + scope_value?: string | null; + service_slug?: string | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + uri?: string | null; + vcard_altid?: string | null; + vcard_group?: string | null; + vcard_pid?: string[] | null; + vcard_prop_id?: string | null; + vcard_property?: string | null; + }; + OrganizationCreateBody: { + description?: string | null; + /** @enum {string} */ + kind?: "company" | "nonprofit" | "school" | "government" | "household" | "other"; + name: string; + primary_domain?: string | null; + }; + OrganizationDuplicateSuggestion: { + /** Format: double */ + confidence: number; + /** Format: date-time */ + created_at: string; + criterion: string; + evidence: string; + /** Format: int64 */ + id: number; + /** Format: int64 */ + organization_a_id: number; + /** Format: int64 */ + organization_b_id: number; + resolution_note?: string; + /** Format: date-time */ + resolved_at?: string; + resolved_by?: string; + source: string; + status: string; + /** Format: date-time */ + updated_at: string; + } & { + [key: string]: unknown; + }; + OrganizationDuplicateSuggestionsResponse: { + suggestions: components["schemas"]["OrganizationDuplicateSuggestion"][] | null; + } & { + [key: string]: unknown; + }; + OrganizationIdentifier: { + envelope: components["schemas"]["ValueEnvelope"]; + identifier_kind: string; + identifier_value: string; + normalized_value: string; + /** Format: int64 */ + organization_id: number; + } & { + [key: string]: unknown; + }; + OrganizationIdentifierBody: { + /** @enum {string} */ + identifier_kind: "domain" | "linkedin" | "duns" | "tax_id" | "registry" | "other"; + identifier_value: string; + /** Format: int64 */ + ordinal?: number; + /** Format: int64 */ + pref?: number | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + vcard_altid?: string | null; + vcard_group?: string | null; + vcard_pid?: string[] | null; + vcard_prop_id?: string | null; + vcard_property?: string | null; + }; + OrganizationMedia: { + /** Format: int64 */ + byte_size?: number; + content_hash?: string; + envelope: components["schemas"]["ValueEnvelope"]; + has_data: boolean; + media_kind: string; + media_type?: string; + /** Format: int64 */ + organization_id: number; + original_value: string; + uri?: string; + } & { + [key: string]: unknown; + }; + OrganizationMediaBody: { + data?: string; + /** @enum {string} */ + media_kind: "photo" | "logo" | "sound" | "key"; + media_type?: string | null; + /** Format: int64 */ + ordinal?: number; + original_value?: string; + /** Format: int64 */ + pref?: number | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + uri?: string | null; + vcard_altid?: string | null; + vcard_group?: string | null; + vcard_pid?: string[] | null; + vcard_prop_id?: string | null; + vcard_property?: string | null; + }; + OrganizationName: { + envelope: components["schemas"]["ValueEnvelope"]; + name: string; + name_kind: string; + name_normalized: string; + /** Format: int64 */ + organization_id: number; + } & { + [key: string]: unknown; + }; + OrganizationNameBody: { + name: string; + /** @enum {string} */ + name_kind: "alias" | "legal" | "former" | "abbreviation" | "sort"; + /** Format: int64 */ + ordinal?: number; + /** Format: int64 */ + pref?: number | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + vcard_altid?: string | null; + vcard_group?: string | null; + vcard_pid?: string[] | null; + vcard_prop_id?: string | null; + vcard_property?: string | null; + }; + OrganizationProfile: { + addresses: components["schemas"]["OrganizationAddress"][] | null; + categories: components["schemas"]["OrganizationCategory"][] | null; + contact_points: components["schemas"]["OrganizationContactPoint"][] | null; + identifiers: components["schemas"]["OrganizationIdentifier"][] | null; + media: components["schemas"]["OrganizationMedia"][] | null; + names: components["schemas"]["OrganizationName"][] | null; + organization: components["schemas"]["Organization"]; + } & { + [key: string]: unknown; + }; + OrganizationProfileBody: { + addresses?: components["schemas"]["OrganizationAddressBody"][] | null; + categories?: components["schemas"]["OrganizationCategoryBody"][] | null; + contact_points?: components["schemas"]["OrganizationContactPointBody"][] | null; + identifiers?: components["schemas"]["OrganizationIdentifierBody"][] | null; + media?: components["schemas"]["OrganizationMediaBody"][] | null; + names?: components["schemas"]["OrganizationNameBody"][] | null; + }; + OrganizationsResponse: { + /** Format: int64 */ + limit: number; + /** Format: int64 */ + offset: number; + organizations: components["schemas"]["Organization"][] | null; + /** Format: int64 */ + total: number; + } & { + [key: string]: unknown; + }; + PartialDate: { + /** Format: int64 */ + day?: number; /** Format: int64 */ month?: number; /** Format: int64 */ @@ -3955,6 +4641,12 @@ export interface components { } & { [key: string]: unknown; }; + RefreshOrganizationDuplicateSuggestionsResponse: { + /** Format: int64 */ + created: number; + } & { + [key: string]: unknown; + }; RejectedCandidate: { identifier: string; reason: string; @@ -4051,6 +4743,11 @@ export interface components { } & { [key: string]: unknown; }; + ResolveOrganizationDuplicateSuggestionBody: { + note?: string | null; + /** @enum {string} */ + status: "accepted" | "rejected"; + }; SavedView: { canonical_state: components["schemas"]["SavedViewStateEnvelope"]; /** Format: date-time */ @@ -4187,6 +4884,21 @@ export interface components { } & { [key: string]: unknown; }; + SetOrganizationAttributeBody: { + actor?: string | null; + /** Format: double */ + confidence?: number | null; + definition_slug: string; + dry_run?: boolean; + /** Format: int64 */ + expected_value_id?: number | null; + /** Format: int64 */ + ordinal?: number | null; + /** @enum {string} */ + source: "user" | "carddav_import" | "vcard_import" | "archive_observation" | "extraction" | "enrichment" | "system"; + source_ref?: string | null; + value: components["schemas"]["AttributeValue"]; + }; SetPersonAttributeRequest: { /** Format: date-time */ active_from?: string; @@ -7790,7 +8502,7 @@ export interface operations { }; }; }; - explore: { + createEmployment: { parameters: { query?: never; header?: never; @@ -7799,17 +8511,21 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["ExploreHTTPRequest"]; + "application/json": components["schemas"]["EmploymentBody"]; }; }; responses: { - /** @description OK */ - 200: { + /** @description Created */ + 201: { headers: { + /** @description Strong employment revision tag for optimistic concurrency */ + ETag?: string; + /** @description Canonical URL of the created employment record */ + Location?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreHTTPResponse"]; + "application/json": components["schemas"]["Employment"]; }; }; /** @description Error */ @@ -7830,13 +8546,13 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service Unavailable */ + /** @description Error */ 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -7850,26 +8566,27 @@ export interface operations { }; }; }; - listExploreFiles: { + getEmployment: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ExploreFilesHTTPRequest"]; + path: { + /** @description Employment ID */ + id: number; }; + cookie?: never; }; + requestBody?: never; responses: { /** @description OK */ 200: { headers: { + /** @description Strong employment revision tag for optimistic concurrency */ + ETag?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreFilesHTTPResponse"]; + "application/json": components["schemas"]["Employment"]; }; }; /** @description Error */ @@ -7882,7 +8599,7 @@ export interface operations { }; }; /** @description Error */ - 409: { + 404: { headers: { [name: string]: unknown; }; @@ -7890,13 +8607,13 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service Unavailable */ + /** @description Error */ 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -7910,27 +8627,27 @@ export interface operations { }; }; }; - exploreGroups: { + deleteEmployment: { parameters: { query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ExploreGroupsHTTPRequest"]; + header: { + /** @description Strong ETag returned by the latest employment read */ + "If-Match": string; + }; + path: { + /** @description Employment ID */ + id: number; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description OK */ - 200: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ExploreGroupsHTTPResponse"]; - }; + content?: never; }; /** @description Error */ 400: { @@ -7942,7 +8659,7 @@ export interface operations { }; }; /** @description Error */ - 409: { + 404: { headers: { [name: string]: unknown; }; @@ -7950,13 +8667,31 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service Unavailable */ - 503: { + /** @description Error */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -7970,26 +8705,34 @@ export interface operations { }; }; }; - countExploreMatches: { + patchEmployment: { parameters: { query?: never; - header?: never; - path?: never; + header: { + /** @description Strong ETag returned by the latest employment read */ + "If-Match": string; + }; + path: { + /** @description Employment ID */ + id: number; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["ExploreMatchCountsRequest"]; + "application/json": components["schemas"]["EmploymentBody"]; }; }; responses: { /** @description OK */ 200: { headers: { + /** @description Strong employment revision tag for optimistic concurrency */ + ETag?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreMatchCountsResponse"]; + "application/json": components["schemas"]["Employment"]; }; }; /** @description Error */ @@ -8002,6 +8745,15 @@ export interface operations { }; }; /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ 409: { headers: { [name: string]: unknown; @@ -8010,13 +8762,22 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service Unavailable */ + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8030,26 +8791,34 @@ export interface operations { }; }; }; - preflightExploreSelection: { + endEmployment: { parameters: { query?: never; - header?: never; - path?: never; + header: { + /** @description Strong ETag returned by the latest employment read */ + "If-Match": string; + }; + path: { + /** @description Employment ID */ + id: number; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["ExplorePreflightRequest"]; + "application/json": components["schemas"]["EndEmploymentBody"]; }; }; responses: { /** @description OK */ 200: { headers: { + /** @description Strong employment revision tag for optimistic concurrency */ + ETag?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExplorePreflightResponse"]; + "application/json": components["schemas"]["Employment"]; }; }; /** @description Error */ @@ -8062,6 +8831,15 @@ export interface operations { }; }; /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ 409: { headers: { [name: string]: unknown; @@ -8070,13 +8848,22 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service Unavailable */ + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8090,26 +8877,30 @@ export interface operations { }; }; }; - groupFiles: { + setPrimaryEmployment: { parameters: { query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["FileGroupsHTTPRequest"]; + header: { + /** @description Strong ETag returned by the latest employment read */ + "If-Match": string; }; + path: { + /** @description Employment ID */ + id: number; + }; + cookie?: never; }; + requestBody?: never; responses: { /** @description OK */ 200: { headers: { + /** @description Strong employment revision tag for optimistic concurrency */ + ETag?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FileGroupsHTTPResponse"]; + "application/json": components["schemas"]["Employment"]; }; }; /** @description Error */ @@ -8122,6 +8913,15 @@ export interface operations { }; }; /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ 409: { headers: { [name: string]: unknown; @@ -8130,13 +8930,22 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Service Unavailable */ + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8150,7 +8959,7 @@ export interface operations { }; }; }; - searchFiles: { + explore: { parameters: { query?: never; header?: never; @@ -8159,7 +8968,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["FileSearchHTTPRequest"]; + "application/json": components["schemas"]["ExploreHTTPRequest"]; }; }; responses: { @@ -8169,7 +8978,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FileSearchHTTPResponse"]; + "application/json": components["schemas"]["ExploreHTTPResponse"]; }; }; /** @description Error */ @@ -8210,17 +9019,18 @@ export interface operations { }; }; }; - getFile: { + listExploreFiles: { parameters: { query?: never; header?: never; - path: { - /** @description File attachment ID */ - id: number; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ExploreFilesHTTPRequest"]; + }; + }; responses: { /** @description OK */ 200: { @@ -8228,11 +9038,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FileMetadataResponse"]; + "application/json": components["schemas"]["ExploreFilesHTTPResponse"]; }; }; /** @description Error */ - default: { + 400: { headers: { [name: string]: unknown; }; @@ -8240,31 +9050,26 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - }; - }; - getFileContent: { - parameters: { - query?: never; - header?: never; - path: { - /** @description File attachment ID */ - id: number; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Service Unavailable */ + 503: { headers: { [name: string]: unknown; }; content: { - "*/*": string; + "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ - 400: { + default: { headers: { [name: string]: unknown; }; @@ -8272,17 +9077,32 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Error */ - 401: { + }; + }; + exploreGroups: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExploreGroupsHTTPRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ExploreGroupsHTTPResponse"]; }; }; /** @description Error */ - 404: { + 400: { headers: { [name: string]: unknown; }; @@ -8291,7 +9111,7 @@ export interface operations { }; }; /** @description Error */ - 500: { + 409: { headers: { [name: string]: unknown; }; @@ -8299,13 +9119,13 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - /** @description Error */ + /** @description Service Unavailable */ 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8319,14 +9139,18 @@ export interface operations { }; }; }; - getHealth: { + countExploreMatches: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ExploreMatchCountsRequest"]; + }; + }; responses: { /** @description OK */ 200: { @@ -8334,11 +9158,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["HealthResponse"]; + "application/json": components["schemas"]["ExploreMatchCountsResponse"]; }; }; /** @description Error */ - default: { + 400: { headers: { [name: string]: unknown; }; @@ -8346,28 +9170,22 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - }; - }; - linkIdentityParticipants: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["IdentityLinkRequest"]; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; }; - }; - responses: { - /** @description OK */ - 200: { + /** @description Service Unavailable */ + 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IdentityLinkResponse"]; + "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8381,7 +9199,7 @@ export interface operations { }; }; }; - unlinkIdentityParticipants: { + preflightExploreSelection: { parameters: { query?: never; header?: never; @@ -8390,7 +9208,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["IdentityLinkRequest"]; + "application/json": components["schemas"]["ExplorePreflightRequest"]; }; }; responses: { @@ -8400,7 +9218,34 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IdentityLinkResponse"]; + "application/json": components["schemas"]["ExplorePreflightResponse"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Service Unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8414,7 +9259,7 @@ export interface operations { }; }; }; - importMeeting: { + groupFiles: { parameters: { query?: never; header?: never; @@ -8423,7 +9268,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["MeetingImportRequest"]; + "application/json": components["schemas"]["FileGroupsHTTPRequest"]; }; }; responses: { @@ -8433,20 +9278,20 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MeetingImportResponse"]; + "application/json": components["schemas"]["FileGroupsHTTPResponse"]; }; }; - /** @description Created */ - 201: { + /** @description Error */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MeetingImportResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ - default: { + 409: { headers: { [name: string]: unknown; }; @@ -8454,27 +9299,13 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - }; - }; - searchIntegrationTasks: { - parameters: { - query: { - /** @description Task title search within the configured project */ - q: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Service Unavailable */ + 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskSearchResponse"]; + "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8488,14 +9319,18 @@ export interface operations { }; }; }; - getTaskIntegrationStatus: { + searchFiles: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["FileSearchHTTPRequest"]; + }; + }; responses: { /** @description OK */ 200: { @@ -8503,11 +9338,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskIntegrationStatusResponse"]; + "application/json": components["schemas"]["FileSearchHTTPResponse"]; }; }; /** @description Error */ - default: { + 400: { headers: { [name: string]: unknown; }; @@ -8515,24 +9350,22 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - }; - }; - testTaskIntegration: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Error */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskIntegrationStatusResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Service Unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExploreCacheUnavailableResponse"] | components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8546,16 +9379,14 @@ export interface operations { }; }; }; - listMessages: { + getFile: { parameters: { - query?: { - /** @description One-based page number (default 1; values below 1 are clamped to 1). Non-numeric values are rejected with 400. */ - page?: number; - /** @description Page size (default 20, max 100; out-of-range values are clamped). Non-numeric values are rejected with 400. */ - page_size?: number; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description File attachment ID */ + id: number; + }; cookie?: never; }; requestBody?: never; @@ -8566,7 +9397,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MessageListResponse"]; + "application/json": components["schemas"]["FileMetadataResponse"]; }; }; /** @description Error */ @@ -8580,16 +9411,14 @@ export interface operations { }; }; }; - listChangedMessages: { + getFileContent: { parameters: { - query?: { - /** @description Opaque cursor from the next_cursor of the previous response, sent back verbatim. Do not parse, construct, compare, or order it; its contents may change without notice. Omit, or send it empty, to start from the beginning of the archive. The token is not authenticated: the server does not sign it and cannot tell one it issued from a well-formed one you built, so a fabricated cursor naming this archive is accepted and simply moves your own position. Rejected with 400 invalid_cursor, rather than read as the beginning: a token the server cannot read, one carrying a cursor format this build does not speak, and one issued against a different archive */ - cursor?: string; - /** @description Maximum number of rows to return (default 100, max 500; values below 1 fall back to the default) */ - limit?: number; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description File attachment ID */ + id: number; + }; cookie?: never; }; requestBody?: never; @@ -8600,7 +9429,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ChangesResponse"]; + "*/*": string; }; }; /** @description Error */ @@ -8622,7 +9451,7 @@ export interface operations { }; }; /** @description Error */ - 429: { + 404: { headers: { [name: string]: unknown; }; @@ -8659,50 +9488,9 @@ export interface operations { }; }; }; - filterMessages: { + getHealth: { parameters: { - query?: { - /** @description Sender email/address filter */ - sender?: string; - /** @description Sender display-name filter */ - sender_name?: string; - /** @description Recipient email/address filter */ - recipient?: string; - /** @description Recipient display-name filter */ - recipient_name?: string; - /** @description Domain filter */ - domain?: string; - /** @description Label filter */ - label?: string; - /** @description Message type filter */ - message_type?: string; - /** @description Named time period */ - time_period?: string; - /** @description Time bucket granularity */ - time_granularity?: string; - /** @description Conversation ID */ - conversation_id?: number; - /** @description Source ID */ - source_id?: number; - /** @description Only include messages with attachments */ - attachments_only?: boolean; - /** @description Exclude deleted messages */ - hide_deleted?: boolean; - /** @description Lower date/time bound (RFC3339 or YYYY-MM-DD) */ - after?: string; - /** @description Upper date/time bound (RFC3339 or YYYY-MM-DD) */ - before?: string; - /** @description Comma-separated aggregate view names to match empty values */ - empty_targets?: string; - /** @description Zero-based row offset */ - offset?: number; - /** @description Maximum number of rows to return (default and max 500; larger values are clamped) */ - limit?: number; - /** @description Sort field: date, size, or subject */ - sort?: string; - /** @description Sort direction: asc or desc */ - direction?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8715,7 +9503,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FilteredMessagesResponse"]; + "application/json": components["schemas"]["HealthResponse"]; }; }; /** @description Error */ @@ -8729,55 +9517,18 @@ export interface operations { }; }; }; - getGmailIDsByFilter: { + linkIdentityParticipants: { parameters: { - query?: { - /** @description Sender email/address filter */ - sender?: string; - /** @description Sender display-name filter */ - sender_name?: string; - /** @description Recipient email/address filter */ - recipient?: string; - /** @description Recipient display-name filter */ - recipient_name?: string; - /** @description Domain filter */ - domain?: string; - /** @description Label filter */ - label?: string; - /** @description Message type filter */ - message_type?: string; - /** @description Named time period */ - time_period?: string; - /** @description Time bucket granularity */ - time_granularity?: string; - /** @description Conversation ID */ - conversation_id?: number; - /** @description Source ID */ - source_id?: number; - /** @description Only include messages with attachments */ - attachments_only?: boolean; - /** @description Exclude deleted messages */ - hide_deleted?: boolean; - /** @description Lower date/time bound (RFC3339 or YYYY-MM-DD) */ - after?: string; - /** @description Upper date/time bound (RFC3339 or YYYY-MM-DD) */ - before?: string; - /** @description Comma-separated aggregate view names to match empty values */ - empty_targets?: string; - /** @description Zero-based row offset */ - offset?: number; - /** @description Maximum number of rows to return (default and max 500; larger values are clamped) */ - limit?: number; - /** @description Sort field: date, size, or subject */ - sort?: string; - /** @description Sort direction: asc or desc */ - direction?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["IdentityLinkRequest"]; + }; + }; responses: { /** @description OK */ 200: { @@ -8785,11 +9536,1282 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GmailIDsResponse"]; + "application/json": components["schemas"]["IdentityLinkResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + unlinkIdentityParticipants: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IdentityLinkRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IdentityLinkResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + importMeeting: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MeetingImportRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MeetingImportResponse"]; + }; + }; + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MeetingImportResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + searchIntegrationTasks: { + parameters: { + query: { + /** @description Task title search within the configured project */ + q: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskSearchResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getTaskIntegrationStatus: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskIntegrationStatusResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + testTaskIntegration: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskIntegrationStatusResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listMessages: { + parameters: { + query?: { + /** @description One-based page number (default 1; values below 1 are clamped to 1). Non-numeric values are rejected with 400. */ + page?: number; + /** @description Page size (default 20, max 100; out-of-range values are clamped). Non-numeric values are rejected with 400. */ + page_size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageListResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listChangedMessages: { + parameters: { + query?: { + /** @description Opaque cursor from the next_cursor of the previous response, sent back verbatim. Do not parse, construct, compare, or order it; its contents may change without notice. Omit, or send it empty, to start from the beginning of the archive. The token is not authenticated: the server does not sign it and cannot tell one it issued from a well-formed one you built, so a fabricated cursor naming this archive is accepted and simply moves your own position. Rejected with 400 invalid_cursor, rather than read as the beginning: a token the server cannot read, one carrying a cursor format this build does not speak, and one issued against a different archive */ + cursor?: string; + /** @description Maximum number of rows to return (default 100, max 500; values below 1 fall back to the default) */ + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ChangesResponse"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + filterMessages: { + parameters: { + query?: { + /** @description Sender email/address filter */ + sender?: string; + /** @description Sender display-name filter */ + sender_name?: string; + /** @description Recipient email/address filter */ + recipient?: string; + /** @description Recipient display-name filter */ + recipient_name?: string; + /** @description Domain filter */ + domain?: string; + /** @description Label filter */ + label?: string; + /** @description Message type filter */ + message_type?: string; + /** @description Named time period */ + time_period?: string; + /** @description Time bucket granularity */ + time_granularity?: string; + /** @description Conversation ID */ + conversation_id?: number; + /** @description Source ID */ + source_id?: number; + /** @description Only include messages with attachments */ + attachments_only?: boolean; + /** @description Exclude deleted messages */ + hide_deleted?: boolean; + /** @description Lower date/time bound (RFC3339 or YYYY-MM-DD) */ + after?: string; + /** @description Upper date/time bound (RFC3339 or YYYY-MM-DD) */ + before?: string; + /** @description Comma-separated aggregate view names to match empty values */ + empty_targets?: string; + /** @description Zero-based row offset */ + offset?: number; + /** @description Maximum number of rows to return (default and max 500; larger values are clamped) */ + limit?: number; + /** @description Sort field: date, size, or subject */ + sort?: string; + /** @description Sort direction: asc or desc */ + direction?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FilteredMessagesResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getGmailIDsByFilter: { + parameters: { + query?: { + /** @description Sender email/address filter */ + sender?: string; + /** @description Sender display-name filter */ + sender_name?: string; + /** @description Recipient email/address filter */ + recipient?: string; + /** @description Recipient display-name filter */ + recipient_name?: string; + /** @description Domain filter */ + domain?: string; + /** @description Label filter */ + label?: string; + /** @description Message type filter */ + message_type?: string; + /** @description Named time period */ + time_period?: string; + /** @description Time bucket granularity */ + time_granularity?: string; + /** @description Conversation ID */ + conversation_id?: number; + /** @description Source ID */ + source_id?: number; + /** @description Only include messages with attachments */ + attachments_only?: boolean; + /** @description Exclude deleted messages */ + hide_deleted?: boolean; + /** @description Lower date/time bound (RFC3339 or YYYY-MM-DD) */ + after?: string; + /** @description Upper date/time bound (RFC3339 or YYYY-MM-DD) */ + before?: string; + /** @description Comma-separated aggregate view names to match empty values */ + empty_targets?: string; + /** @description Zero-based row offset */ + offset?: number; + /** @description Maximum number of rows to return (default and max 500; larger values are clamped) */ + limit?: number; + /** @description Sort field: date, size, or subject */ + sort?: string; + /** @description Sort direction: asc or desc */ + direction?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GmailIDsResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getMessage: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Message ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageDetail"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getMessageInlinePart: { + parameters: { + query: { + /** @description Inline MIME Content-ID */ + cid: string; + }; + header?: never; + path: { + /** @description Message ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/octet-stream": string; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 415: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 501: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listMessageTasks: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Archived email message ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskLinkLookupResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + createOrLinkMessageTask: { + parameters: { + query?: never; + header: { + /** @description Browser-generated retry-stable request ID */ + "X-Request-Id": string; + }; + path: { + /** @description Archived email message ID */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TaskLinkMutationRequest"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskLinkMutationResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + unlinkMessageTask: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Archived email message ID */ + id: number; + /** @description External task ID */ + task_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskLinkMutationResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listOrganizations: { + parameters: { + query?: { + /** @description Maximum results */ + limit?: number; + /** @description Results to skip */ + offset?: number; + /** @description Include retired organizations */ + include_retired?: boolean; + /** @description Normalized-name search */ + q?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationsResponse"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + createOrganization: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationCreateBody"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + /** @description Strong organization revision tag for optimistic concurrency */ + ETag?: string; + /** @description Canonical URL of the created organization */ + Location?: string; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Organization"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listOrganizationDuplicateSuggestions: { + parameters: { + query?: { + /** @description Filter by status */ + status?: string; + /** @description Maximum results */ + limit?: number; + /** @description Results to skip */ + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationDuplicateSuggestionsResponse"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + refreshOrganizationDuplicateSuggestions: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RefreshOrganizationDuplicateSuggestionsResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + resolveOrganizationDuplicateSuggestion: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Duplicate suggestion ID */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ResolveOrganizationDuplicateSuggestionBody"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationDuplicateSuggestion"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getOrganization: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Organization ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + /** @description Strong organization revision tag for optimistic concurrency */ + ETag?: string; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationProfile"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + deleteOrganization: { + parameters: { + query?: never; + header: { + /** @description Strong ETag returned by the latest organization read */ + "If-Match": string; + }; + path: { + /** @description Organization ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + patchOrganization: { + parameters: { + query?: never; + header: { + /** @description Strong ETag returned by the latest organization read */ + "If-Match": string; + }; + path: { + /** @description Organization ID */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationBody"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + /** @description Strong organization revision tag for optimistic concurrency */ + ETag?: string; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Organization"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listOrganizationAttributes: { + parameters: { + query?: { + /** @description Include superseded values */ + include_superseded?: boolean; + /** @description Restrict to one definition */ + definition_slug?: string; + }; + header?: never; + path: { + /** @description Organization ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationAttributesResponse"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + setOrganizationAttribute: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Organization ID */ + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetOrganizationAttributeBody"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationAttributeWrite"]; + }; + }; + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationAttributeWrite"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ - default: { + 404: { headers: { [name: string]: unknown; }; @@ -8797,27 +10819,22 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - }; - }; - getMessage: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Message ID */ - id: number; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Error */ + 503: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MessageDetail"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8831,15 +10848,19 @@ export interface operations { }; }; }; - getMessageInlinePart: { + listOrganizationEmployments: { parameters: { - query: { - /** @description Inline MIME Content-ID */ - cid: string; + query?: { + /** @description Only current employments */ + current_only?: boolean; + /** @description Maximum results */ + limit?: number; + /** @description Results to skip */ + offset?: number; }; header?: never; path: { - /** @description Message ID */ + /** @description Organization ID */ id: number; }; cookie?: never; @@ -8852,7 +10873,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/octet-stream": string; + "application/json": components["schemas"]["EmploymentsResponse"]; }; }; /** @description Error */ @@ -8865,7 +10886,7 @@ export interface operations { }; }; /** @description Error */ - 401: { + 404: { headers: { [name: string]: unknown; }; @@ -8874,7 +10895,7 @@ export interface operations { }; }; /** @description Error */ - 404: { + 503: { headers: { [name: string]: unknown; }; @@ -8883,7 +10904,7 @@ export interface operations { }; }; /** @description Error */ - 415: { + default: { headers: { [name: string]: unknown; }; @@ -8891,8 +10912,33 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; + }; + }; + getOrganizationHistory: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Organization ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + /** @description Strong organization revision tag for optimistic concurrency */ + ETag?: string; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationProfile"]; + }; + }; /** @description Error */ - 500: { + 400: { headers: { [name: string]: unknown; }; @@ -8901,7 +10947,7 @@ export interface operations { }; }; /** @description Error */ - 501: { + 404: { headers: { [name: string]: unknown; }; @@ -8929,25 +10975,79 @@ export interface operations { }; }; }; - listMessageTasks: { + mergeOrganization: { parameters: { query?: never; - header?: never; + header: { + /** @description Strong ETag returned by the latest organization read */ + "If-Match": string; + }; path: { - /** @description Archived email message ID */ + /** @description Organization ID */ id: number; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["MergeOrganizationBody"]; + }; + }; responses: { /** @description OK */ 200: { headers: { + /** @description Strong organization revision tag for optimistic concurrency */ + ETag?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskLinkLookupResponse"]; + "application/json": components["schemas"]["Organization"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -8961,36 +11061,38 @@ export interface operations { }; }; }; - createOrLinkMessageTask: { + putOrganizationProfile: { parameters: { query?: never; header: { - /** @description Browser-generated retry-stable request ID */ - "X-Request-Id": string; + /** @description Strong ETag returned by the latest organization read */ + "If-Match": string; }; path: { - /** @description Archived email message ID */ + /** @description Organization ID */ id: number; }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["TaskLinkMutationRequest"]; + "application/json": components["schemas"]["OrganizationProfileBody"]; }; }; responses: { - /** @description Created */ - 201: { + /** @description OK */ + 200: { headers: { + /** @description Strong organization revision tag for optimistic concurrency */ + ETag?: string; [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskLinkMutationResponse"]; + "application/json": components["schemas"]["OrganizationProfile"]; }; }; /** @description Error */ - default: { + 400: { headers: { [name: string]: unknown; }; @@ -8998,29 +11100,40 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; - }; - }; - unlinkMessageTask: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Archived email message ID */ - id: number; - /** @description External task ID */ - task_id: string; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Error */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskLinkMutationResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 428: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; }; }; /** @description Error */ @@ -9851,6 +11964,72 @@ export interface operations { }; }; }; + listPersonEmployments: { + parameters: { + query?: { + /** @description Only current employments */ + current_only?: boolean; + /** @description Maximum results */ + limit?: number; + /** @description Results to skip */ + offset?: number; + }; + header?: never; + path: { + /** @description Person ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EmploymentsResponse"]; + }; + }; + /** @description Error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; getPersonStructuredProfile: { parameters: { query?: never;