From 667a7614a7175deeca436195da3f95f40f4b3c9e Mon Sep 17 00:00:00 2001 From: mattgd Date: Wed, 18 Dec 2024 16:38:02 -0500 Subject: [PATCH] Integrate Python and Typescript. --- go.mod | 6 +- go.sum | 4 +- internal/cmd/typegen.go | 339 ++++------------------- internal/typegen/common.go | 88 ++++++ internal/typegen/python_generator.go | 201 ++++++++++++++ internal/typegen/typescript_generator.go | 145 ++++++++++ 6 files changed, 492 insertions(+), 291 deletions(-) create mode 100644 internal/typegen/common.go create mode 100644 internal/typegen/python_generator.go create mode 100644 internal/typegen/typescript_generator.go diff --git a/go.mod b/go.mod index 3294eb9..3157878 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/workos/workos-cli -go 1.23 +go 1.23.4 require ( github.com/charmbracelet/huh v0.5.1 @@ -9,6 +9,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/workos/workos-go/v4 v4.21.0 + golang.org/x/text v0.16.0 ) require ( @@ -56,7 +57,8 @@ require ( golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/workos/workos-go/v4 => github.com/workos/workos-go/v4 v4.26.1-0.20241219170524-3ecd219ec436 diff --git a/go.sum b/go.sum index 9f56534..71a598f 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/workos/workos-go/v4 v4.21.0 h1:pEoAJzCsBPU46dL6/PwwwS5BrBV8LWOZQp0mERrRPCc= -github.com/workos/workos-go/v4 v4.21.0/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c= +github.com/workos/workos-go/v4 v4.26.1-0.20241219170524-3ecd219ec436 h1:3qRQq9res1qTa68OKTwZ+xdGKKr6esRorTxWkdEM8kQ= +github.com/workos/workos-go/v4 v4.26.1-0.20241219170524-3ecd219ec436/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= diff --git a/internal/cmd/typegen.go b/internal/cmd/typegen.go index 90e5969..a8f01ba 100644 --- a/internal/cmd/typegen.go +++ b/internal/cmd/typegen.go @@ -1,307 +1,72 @@ package cmd import ( - "encoding/json" "fmt" - "io" - "net/http" - "os" - "strings" + "slices" + "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/text/cases" - "golang.org/x/text/language" + "github.com/workos/workos-cli/internal/typegen" ) -type TypeGenerator struct { - writer io.Writer -} - -func NewTypeGenerator(w io.Writer) *TypeGenerator { - return &TypeGenerator{writer: w} -} - -// Permission types -type PermissionResponse struct { - Data []Permission `json:"data"` -} - -type Permission struct { - Slug string `json:"slug"` -} - -// Permission Generator -func (g *TypeGenerator) GenerateFromPermissionsAPI() error { - jsonData, err := os.ReadFile("permissions.json") - if err != nil { - return err - } - - var response PermissionResponse - if err := json.Unmarshal(jsonData, &response); err != nil { - return fmt.Errorf("failed to parse permissions JSON: %w", err) - } - - slugs := make([]string, len(response.Data)) - for i, permission := range response.Data { - slugs[i] = permission.Slug - } - - return g.generateUnionType("WorkOSPermission", slugs) -} - -// Role types -type RoleResponse struct { - Data []Role `json:"data"` -} - -type Role struct { - Slug string `json:"slug"` -} - -// Role Generator -func (g *TypeGenerator) GenerateFromRolesAPI() error { - jsonData, err := os.ReadFile("roles.json") - if err != nil { - return err - } - - var response RoleResponse - if err := json.Unmarshal(jsonData, &response); err != nil { - return fmt.Errorf("failed to parse roles JSON: %w", err) - } - - slugs := make([]string, len(response.Data)) - for i, role := range response.Data { - slugs[i] = role.Slug - } - - return g.generateUnionType("WorkOSRole", slugs) -} - -// Audit Log types -type AuditLogResponse struct { - Data []AuditLogEvent `json:"data"` -} - -type AuditLogEvent struct { - Name string `json:"name"` - Schema AuditLogSchema `json:"schema"` -} - -type AuditLogSchema struct { - ActorSchema AuditLogActorSchema `json:"actor"` - TargetSchemas []AuditLogTargetSchema `json:"targets"` - MetadataSchema *AuditLogMetadataSchema `json:"metadata"` -} - -type AuditLogActorSchema struct { - MetadataSchema *AuditLogMetadataSchema `json:"metadata"` -} - -type AuditLogTargetSchema struct { - Type string `json:"type"` - MetadataSchema *AuditLogMetadataSchema `json:"metadata"` -} - -type AuditLogMetadataSchema struct { - Type string `json:"type"` - Properties map[string]AuditLogMetadataProperty `json:"properties"` -} - -type AuditLogMetadataProperty struct { - Type string `json:"type"` - Nullable *bool `json:"nullable,omitempty"` -} - -// Audit Log Generator -func (g *TypeGenerator) GenerateFromAuditLogsAPI() error { - req, err := http.NewRequest("GET", "https://api.workos.com/audit_logs/actions", nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Authorization", "Bearer "+os.Getenv("WORKOS_API_KEY")) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("failed to fetch audit logs actions: %w", err) - } - defer resp.Body.Close() - - var response AuditLogResponse - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return fmt.Errorf("failed to parse audit logs response: %w", err) - } - - var interfaces []string - var eventNames []string - - for _, action := range response.Data { - interfaceName := g.generateInterfaceName(action.Name) + "AuditLogEvent" - eventNames = append(eventNames, interfaceName) - - interfaceContent := g.generateAuditLogEventInterface(action) - interfaces = append(interfaces, interfaceContent) - } - - output := strings.Join(interfaces, "\n\n") - - unionType := fmt.Sprintf("\nexport type AuditLogEvent = %s;\n", - strings.Join(eventNames, " | ")) - output += unionType - - _, err = fmt.Fprint(g.writer, output) - return err -} - -func (g *TypeGenerator) generateAuditLogEventInterface(event AuditLogEvent) string { - interfaceName := g.generateInterfaceName(event.Name) - - var properties []string - - properties = append(properties, fmt.Sprintf(" action: '%s';", event.Name)) - properties = append(properties, " occurredAt: string;") - properties = append(properties, fmt.Sprintf(" version?: number;")) - - actorProps := []string{ - " id: string;", - " name?: string;", - } - - if event.Schema.ActorSchema.MetadataSchema != nil { - metadataProps := g.generateMetadataProperties(event.Schema.ActorSchema.MetadataSchema.Properties) - actorProps = append(actorProps, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) - } - - properties = append(properties, fmt.Sprintf(" actor: {\n%s\n };", strings.Join(actorProps, "\n"))) - - if len(event.Schema.TargetSchemas) > 0 { - var targetTypes []string - for _, target := range event.Schema.TargetSchemas { - targetProps := []string{ - fmt.Sprintf(" type: '%s';", target.Type), - " id: string;", - " name?: string;", - } - if target.MetadataSchema != nil { - metadataProps := g.generateMetadataProperties(target.MetadataSchema.Properties) - targetProps = append(targetProps, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) - } +const ( + FlagLanguage = "language" + FlagResources = "resources" +) - targetTypes = append(targetTypes, fmt.Sprintf("{\n%s\n}", strings.Join(targetProps, "\n"))) +// CLI +var typegenCmd = &cobra.Command{ + Use: "generate-types", + Short: "Generate types for WorkOS resources", + Long: "A tool to generate type definitions for your WorkOS resources.", + Example: "workos generate-types --language typescript --resources permissions", + RunE: func(cmd *cobra.Command, args []string) error { + language, err := cmd.Flags().GetString(FlagLanguage) + if err != nil { + return errors.New("Invalid language flag") } - - properties = append(properties, fmt.Sprintf(" targets: (%s)[];", strings.Join(targetTypes, " | "))) - } - - contextProps := []string{ - " location: string;", - " userAgent?: string;", - } - - properties = append(properties, fmt.Sprintf(" context: {\n%s\n };", strings.Join(contextProps, "\n"))) - - if event.Schema.MetadataSchema != nil { - metadataProps := g.generateMetadataProperties(event.Schema.MetadataSchema.Properties) - properties = append(properties, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) - } - - return fmt.Sprintf("export interface %s {\n%s\n}", - interfaceName, - strings.Join(properties, "\n")) -} - -func (g *TypeGenerator) generateMetadataProperties(properties map[string]AuditLogMetadataProperty) string { - var props []string - for key, prop := range properties { - nullable := "" - if prop.Nullable != nil && *prop.Nullable { - nullable = " | null" + resources, err := cmd.Flags().GetStringArray(FlagResources) + if err != nil { + return errors.New("Invalid resources flag") } - props = append(props, fmt.Sprintf(" %s: %s%s;", key, prop.Type, nullable)) - } - return strings.Join(props, "\n") -} - -// Utils -func (g *TypeGenerator) generateInterfaceName(action string) string { - parts := strings.Split(action, ".") - var name string - for _, part := range parts { - part = strings.ReplaceAll(part, "_", " ") - words := strings.Fields(part) - for _, word := range words { - name += cases.Title(language.English).String(word) + var languageGenerator typegen.LanguageTypeGenerator + switch language { + case "typescript": + languageGenerator = typegen.NewTypeScriptTypeGenerator(cmd.OutOrStdout()) + case "python": + languageGenerator = typegen.NewPythonTypeGenerator(cmd.OutOrStdout()) + default: + return errors.New(fmt.Sprintf("Invalid language: %s. Valid options are: typescript, python")) } - } - - return name -} - -func (g *TypeGenerator) generateUnionType(typeName string, values []string) error { - quotedValues := make([]string, len(values)) - for i, v := range values { - quotedValues[i] = fmt.Sprintf("'%s'", v) - } - - output := fmt.Sprintf("export type %s = %s;\n", typeName, strings.Join(quotedValues, " | ")) - _, err := fmt.Fprint(g.writer, output) - return err -} - -// CLI -var typegenCmd = &cobra.Command{ - Use: "typegen", - Short: "Generate TypeScript types from WorkOS APIs", - Long: `A tool to generate TypeScript type definitions from WorkOS permissions, roles, and audit logs.`, -} - -func NewTypeGeneratorCmd() *cobra.Command { - cmd := typegenCmd - - cmd.AddCommand(newPermissionsCmd()) - cmd.AddCommand(newRolesCmd()) - cmd.AddCommand(newAuditLogsCmd()) - - return cmd -} - -func newPermissionsCmd() *cobra.Command { - return &cobra.Command{ - Use: "permissions", - Short: "Generate TypeScript types for WorkOS permissions", - RunE: func(cmd *cobra.Command, args []string) error { - generator := NewTypeGenerator(cmd.OutOrStdout()) - return generator.GenerateFromPermissionsAPI() - }, - } -} + if err := typegen.GenerateImports(languageGenerator, resources); err != nil { + return errors.Wrap(err, "error generating imports") + } -func newRolesCmd() *cobra.Command { - return &cobra.Command{ - Use: "roles", - Short: "Generate TypeScript types for WorkOS roles", - RunE: func(cmd *cobra.Command, args []string) error { - generator := NewTypeGenerator(cmd.OutOrStdout()) - return generator.GenerateFromRolesAPI() - }, - } -} + if slices.Contains(resources, "all") { + typegen.GeneratePermissionsType(languageGenerator) + typegen.GenerateRolesType(languageGenerator) + typegen.GenerateAuditLogsTypes(languageGenerator) + } else { + for _, resource := range resources { + switch resource { + case "permissions": + typegen.GeneratePermissionsType(languageGenerator) + case "roles": + typegen.GenerateRolesType(languageGenerator) + case "audit-logs": + typegen.GenerateAuditLogsTypes(languageGenerator) + } + } + } -func newAuditLogsCmd() *cobra.Command { - return &cobra.Command{ - Use: "audit-logs", - Short: "Generate TypeScript types for WorkOS audit logs", - RunE: func(cmd *cobra.Command, args []string) error { - generator := NewTypeGenerator(cmd.OutOrStdout()) - return generator.GenerateFromAuditLogsAPI() - }, - } + return nil + }, } func init() { - cmd := NewTypeGeneratorCmd() - rootCmd.AddCommand(cmd) + typegenCmd.Flags().StringP(FlagLanguage, "l", "typescript", "Language to to output types in") + typegenCmd.Flags().StringArrayP(FlagResources, "r", []string{"all"}, "Resources to output types for") + rootCmd.AddCommand(typegenCmd) } diff --git a/internal/typegen/common.go b/internal/typegen/common.go new file mode 100644 index 0000000..ebcb4a0 --- /dev/null +++ b/internal/typegen/common.go @@ -0,0 +1,88 @@ +package typegen + +import ( + "context" + "os" + + "github.com/pkg/errors" + "github.com/workos/workos-go/v4/pkg/auditlogs" + "github.com/workos/workos-go/v4/pkg/permissions" + "github.com/workos/workos-go/v4/pkg/roles" +) + +type TypeGenerator interface { + GeneratePermissionsType() error + GenerateRolesType() error + GenerateAuditLogsTypes() error +} + +type LanguageTypeGenerator interface { + generateImports(resources []string) error + generateUnionType(typeName string, values []string) error + generateInterfaceName(resourceName string) string + generateAuditLogActionsTypes(actions []auditlogs.AuditLogAction) error +} + +// Language Import Generator +func GenerateImports(languageGenerator LanguageTypeGenerator, resources []string) error { + return languageGenerator.generateImports(resources) +} + +// Permission Generator +func GeneratePermissionsType(languageGenerator LanguageTypeGenerator) error { + // TODO: Figure out why we need to set the API key here + permissions.SetAPIKey(os.Getenv("WORKOS_API_KEY")) + + // TODO: Need to support pagination + permissionsList, err := permissions.ListPermissions( + context.Background(), + permissions.ListPermissionsOpts{}, + ) + if err != nil { + return errors.Wrap(err, "error fetching permissions") + } + + slugs := make([]string, len(permissionsList.Data)) + for i, permission := range permissionsList.Data { + slugs[i] = permission.Slug + } + + return languageGenerator.generateUnionType("WorkOSPermission", slugs) +} + +// Role Generator +func GenerateRolesType(languageGenerator LanguageTypeGenerator) error { + // TODO: Figure out why we need to set the API key here + roles.SetAPIKey(os.Getenv("WORKOS_API_KEY")) + + rolesList, err := roles.ListRoles( + context.Background(), + roles.ListRolesOpts{}, + ) + if err != nil { + return errors.Wrap(err, "error fetching roles") + } + + slugs := make([]string, len(rolesList.Data)) + for i, role := range rolesList.Data { + slugs[i] = role.Slug + } + + return languageGenerator.generateUnionType("WorkOSRole", slugs) +} + +// Audit Log Generator +func GenerateAuditLogsTypes(languageGenerator LanguageTypeGenerator) error { + auditlogs.SetAPIKey(os.Getenv("WORKOS_API_KEY")) + + // TODO: Need to support pagination + auditLogsList, err := auditlogs.ListActions( + context.Background(), + auditlogs.ListActionsOpts{}, + ) + if err != nil { + return errors.Wrap(err, "error fetching audit logs actions") + } + + return languageGenerator.generateAuditLogActionsTypes(auditLogsList.Data) +} diff --git a/internal/typegen/python_generator.go b/internal/typegen/python_generator.go new file mode 100644 index 0000000..d3962a5 --- /dev/null +++ b/internal/typegen/python_generator.go @@ -0,0 +1,201 @@ +package typegen + +import ( + "bytes" + "fmt" + "io" + "slices" + "strings" + + "github.com/workos/workos-go/v4/pkg/auditlogs" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type PythonTypeGenerator struct { + writer io.Writer +} + +func NewPythonTypeGenerator(w io.Writer) *PythonTypeGenerator { + return &PythonTypeGenerator{writer: w} +} + +// Import generator +func (g *PythonTypeGenerator) generateImports(resources []string) error { + if slices.Contains(resources, "all") || slices.Contains(resources, "audit-logs") { + output := "from typing import Literal, NotRequired, Sequence, TypedDict, Union\n\n" + + _, err := fmt.Fprint(g.writer, output) + return err + } + + return nil +} + +// Audit Log Generator +func (g *PythonTypeGenerator) generateAuditLogActionsTypes(actions []auditlogs.AuditLogAction) error { + var interfaces []string + var eventNames []string + + for _, action := range actions { + interfaceName := g.generateInterfaceName(action.Name) + eventNames = append(eventNames, interfaceName) + + interfaceContent := g.generateAuditLogEventInterface(action) + interfaces = append(interfaces, interfaceContent) + } + + output := strings.Join(interfaces, "\n") + + unionType := fmt.Sprintf("\nAuditLogEvent = Union[%s]\n", + strings.Join(eventNames, ", ")) + output += unionType + + _, err := fmt.Fprint(g.writer, output) + return err +} + +func (g *PythonTypeGenerator) generateTypedDictFromMap(name string, properties map[string]string) string { + generatedCode := fmt.Sprintf("class %s(TypedDict):\n", name) + propertiesBuffer := new(bytes.Buffer) + + for key, pythonType := range properties { + fmt.Fprintf(propertiesBuffer, " %s: %s\n", key, pythonType) + } + + generatedCode += propertiesBuffer.String() + "\n" + return generatedCode +} + +func (g *PythonTypeGenerator) generateTypedDictFromMetadataProperties(className string, metadataPropertyMap map[string]auditlogs.AuditLogActionSchemaMetadataProperty) string { + return g.generateTypedDictFromMap(className, g.generateMetadataProperties(metadataPropertyMap)) +} + +func (g *PythonTypeGenerator) generateAuditLogEventInterface(action auditlogs.AuditLogAction) string { + interfaceName := g.generateInterfaceName(action.Name) + + var generatedCode string + + // Map of top-level property keys and types + properties := map[string]string{ + "action": fmt.Sprintf("Literal['%s']", action.Name), + "occurred_at": "str", + "version": "NotRequired[int]", + } + + actorProperties := map[string]string{ + "id": "str", + "name": "NotRequired[str]", + } + + if len(action.Schema.Actor.Metadata.Properties) > 0 { + actorMetadataInterfaceName := interfaceName + "ActorMetadata" + generatedCode += g.generateTypedDictFromMetadataProperties(actorMetadataInterfaceName, action.Schema.Actor.Metadata.Properties) + actorProperties["metadata"] = actorMetadataInterfaceName + } + + // Add actor property + actorInterfaceName := interfaceName + "Actor" + generatedCode += g.generateTypedDictFromMap(actorInterfaceName, actorProperties) + properties["actor"] = actorInterfaceName + + if len(action.Schema.Targets) > 0 { + var targetTypes []string + for _, target := range action.Schema.Targets { + targetProps := map[string]string{ + "type": fmt.Sprintf("Literal['%s']", target.Type), + "id": "str", + "name": "NotRequired[str]", + } + if len(target.Metadata.Properties) > 0 { + targetMetadataInterfaceName := interfaceName + "TargetMetadata" + generatedCode += g.generateTypedDictFromMetadataProperties(targetMetadataInterfaceName, target.Metadata.Properties) + targetProps["metadata"] = targetMetadataInterfaceName + } + + targetInterfaceName := fmt.Sprintf("%s%s%s", interfaceName, cases.Title(language.English).String(target.Type), "Target") + generatedCode += g.generateTypedDictFromMap(targetInterfaceName, targetProps) + targetTypes = append(targetTypes, targetInterfaceName) + } + + if len(targetTypes) > 1 { + properties["targets"] = fmt.Sprintf("Sequence[Union[%s]]", strings.Join(targetTypes, ", ")) + } else { + properties["targets"] = fmt.Sprintf("Sequence[%s]", targetTypes[0]) + } + } + + // Add context property + contextInterfaceName := interfaceName + "Context" + generatedCode += g.generateTypedDictFromMap(contextInterfaceName, map[string]string{ + "location": "str", + "user_agent": "NotRequired[str]", + }) + properties["context"] = contextInterfaceName + + // Add top-level metadata property + if len(action.Schema.Metadata.Properties) > 0 { + metadataInterfaceName := interfaceName + "Metadata" + generatedCode += g.generateTypedDictFromMetadataProperties(metadataInterfaceName, action.Schema.Metadata.Properties) + properties["metadata"] = metadataInterfaceName + } + + generatedCode += fmt.Sprintf("# Types for %ss\nclass %s(TypedDict):\n", interfaceName, interfaceName) + propertiesBuffer := new(bytes.Buffer) + + for key, pythonType := range properties { + fmt.Fprintf(propertiesBuffer, " %s: %s\n", key, pythonType) + } + + generatedCode += propertiesBuffer.String() + return generatedCode +} + +func (g *PythonTypeGenerator) generateMetadataProperties(properties map[string]auditlogs.AuditLogActionSchemaMetadataProperty) map[string]string { + convertedProperties := make(map[string]string) + for key, prop := range properties { + if prop.Nullable != nil && *prop.Nullable { + convertedProperties[key] = fmt.Sprintf("NotRequired[%s]", prop.Type) + } else { + convertedProperties[key] = g.convertMetadataType(prop.Type) + } + } + + return convertedProperties +} + +func (g *PythonTypeGenerator) convertMetadataType(propertyType string) string { + switch propertyType { + case "string": + return "str" + default: + return propertyType + } +} + +// Utils +func (g *PythonTypeGenerator) generateInterfaceName(action string) string { + parts := strings.Split(action, ".") + var name string + for _, part := range parts { + part = strings.ReplaceAll(part, "_", " ") + words := strings.Fields(part) + for _, word := range words { + name += cases.Title(language.English).String(word) + } + } + + return name + "AuditLogEvent" +} + +func (g *PythonTypeGenerator) generateUnionType(typeName string, values []string) error { + quotedValues := make([]string, len(values)) + for i, v := range values { + quotedValues[i] = fmt.Sprintf("'%s'", v) + } + + output := fmt.Sprintf("%s = Literal[%s]\n\n", typeName, strings.Join(quotedValues, ", ")) + _, err := fmt.Fprint(g.writer, output) + + return err +} diff --git a/internal/typegen/typescript_generator.go b/internal/typegen/typescript_generator.go new file mode 100644 index 0000000..8aa6c54 --- /dev/null +++ b/internal/typegen/typescript_generator.go @@ -0,0 +1,145 @@ +package typegen + +import ( + "fmt" + "io" + "strings" + + "github.com/workos/workos-go/v4/pkg/auditlogs" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type TypeScriptTypeGenerator struct { + writer io.Writer +} + +func NewTypeScriptTypeGenerator(w io.Writer) *TypeScriptTypeGenerator { + return &TypeScriptTypeGenerator{writer: w} +} + +// Import generator +func (g *TypeScriptTypeGenerator) generateImports(resources []string) error { + // No imports needed for TypeScript + return nil +} + +// Audit Log Generator +func (g *TypeScriptTypeGenerator) generateAuditLogActionsTypes(actions []auditlogs.AuditLogAction) error { + var interfaces []string + var eventNames []string + + for _, action := range actions { + interfaceName := g.generateInterfaceName(action.Name) + eventNames = append(eventNames, interfaceName) + + interfaceContent := g.generateAuditLogEventInterface(action) + interfaces = append(interfaces, interfaceContent) + } + + output := strings.Join(interfaces, "\n") + + unionType := fmt.Sprintf("\nexport type AuditLogEvent = %s;\n", + strings.Join(eventNames, " | ")) + output += unionType + + _, err := fmt.Fprint(g.writer, output) + return err +} + +func (g *TypeScriptTypeGenerator) generateAuditLogEventInterface(action auditlogs.AuditLogAction) string { + interfaceName := g.generateInterfaceName(action.Name) + + var properties []string + + properties = append(properties, fmt.Sprintf(" action: '%s';", action.Name)) + properties = append(properties, " occurredAt: string;") + properties = append(properties, fmt.Sprintf(" version?: number;")) + + actorProps := []string{ + " id: string;", + " name?: string;", + } + + if len(action.Schema.Actor.Metadata.Properties) > 0 { + metadataProps := g.generateMetadataProperties(action.Schema.Actor.Metadata.Properties) + actorProps = append(actorProps, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) + } + + properties = append(properties, fmt.Sprintf(" actor: {\n%s\n };", strings.Join(actorProps, "\n"))) + + if len(action.Schema.Targets) > 0 { + var targetTypes []string + for _, target := range action.Schema.Targets { + targetProps := []string{ + fmt.Sprintf(" type: '%s';", target.Type), + " id: string;", + " name?: string;", + } + if len(target.Metadata.Properties) > 0 { + metadataProps := g.generateMetadataProperties(target.Metadata.Properties) + targetProps = append(targetProps, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) + } + + targetTypes = append(targetTypes, fmt.Sprintf("{\n%s\n }", strings.Join(targetProps, "\n"))) + } + + properties = append(properties, fmt.Sprintf(" targets: (%s)[];", strings.Join(targetTypes, " | "))) + } + + contextProps := []string{ + " location: string;", + " userAgent?: string;", + } + + properties = append(properties, fmt.Sprintf(" context: {\n%s\n };", strings.Join(contextProps, "\n"))) + + if len(action.Schema.Metadata.Properties) > 0 { + metadataProps := g.generateMetadataProperties(action.Schema.Metadata.Properties) + properties = append(properties, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) + } + + return fmt.Sprintf("export interface %s {\n%s\n}\n", + interfaceName, + strings.Join(properties, "\n")) +} + +func (g *TypeScriptTypeGenerator) generateMetadataProperties(properties map[string]auditlogs.AuditLogActionSchemaMetadataProperty) string { + var props []string + for key, prop := range properties { + nullable := "" + if prop.Nullable != nil && *prop.Nullable { + nullable = " | null" + } + props = append(props, fmt.Sprintf(" %s: %s%s;", key, prop.Type, nullable)) + } + + return strings.Join(props, "\n") +} + +// Utils +func (g *TypeScriptTypeGenerator) generateInterfaceName(action string) string { + parts := strings.Split(action, ".") + var name string + for _, part := range parts { + part = strings.ReplaceAll(part, "_", " ") + words := strings.Fields(part) + for _, word := range words { + name += cases.Title(language.English).String(word) + } + } + + return fmt.Sprintf("%sAuditLogEvent", name) +} + +func (g *TypeScriptTypeGenerator) generateUnionType(typeName string, values []string) error { + quotedValues := make([]string, len(values)) + for i, v := range values { + quotedValues[i] = fmt.Sprintf("'%s'", v) + } + + output := fmt.Sprintf("export type %s = %s;\n\n", typeName, strings.Join(quotedValues, " | ")) + _, err := fmt.Fprint(g.writer, output) + + return err +}