Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,135 changes: 6,320 additions & 3,815 deletions api/openapi.yaml

Large diffs are not rendered by default.

368 changes: 368 additions & 0 deletions cmd/msgvault/cmd/employment.go
Original file line number Diff line number Diff line change
@@ -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 <id>", 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 <id>", 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 <id>", 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 <id>", 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 <id>", 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: &currentOnly}})
})
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: &currentOnly}})
})
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")
}
Loading