Skip to content

Commit

Permalink
feat: add prefixed human readable id option, change mapping id default (
Browse files Browse the repository at this point in the history
#78)

* feat: add prefixed human readable id option, change mapping id to not be included by default

Signed-off-by: Sarah Funkhouser <[email protected]>

* add tests

Signed-off-by: Sarah Funkhouser <[email protected]>

---------

Signed-off-by: Sarah Funkhouser <[email protected]>
  • Loading branch information
golanglemonade authored Jan 18, 2025
1 parent 41e917d commit a6ae194
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 5 deletions.
55 changes: 55 additions & 0 deletions customtypes/prefixedIdentifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package customtypes

import (
"database/sql"
"database/sql/driver"
"fmt"
"strconv"
"strings"

"entgo.io/ent/schema/field"
)

// PrefixedIdentifier is a custom type that implements the TypeValueScanner interface
type PrefixedIdentifier struct {
prefix string
}

// NewPrefixedIdentifier returns a new PrefixedIdentifier with the given prefix
func NewPrefixedIdentifier(prefix string) PrefixedIdentifier {
return PrefixedIdentifier{prefix: prefix}
}

// Value implements the TypeValueScanner.Value method.
func (p PrefixedIdentifier) Value(s string) (driver.Value, error) {
value := strings.TrimPrefix(s, p.prefix+"-")
if value == "" {
return "", nil
}

trimValue, err := strconv.Atoi(value)
if err != nil {
return nil, err
}

return fmt.Sprintf("%d", trimValue), nil
}

// ScanValue implements the TypeValueScanner.ScanValue method.
func (PrefixedIdentifier) ScanValue() field.ValueScanner {
return &sql.NullString{}
}

// FromValue implements the TypeValueScanner.FromValue method.
func (p PrefixedIdentifier) FromValue(v driver.Value) (string, error) {
s, ok := v.(*sql.NullString)
if !ok {
return "", fmt.Errorf("unexpected input for FromValue: %T", v) // nolint:err113
}

if !s.Valid {
return "", nil
}

return fmt.Sprintf("%s-%06s", p.prefix, s.String), nil
}
96 changes: 96 additions & 0 deletions customtypes/prefixedIdentifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package customtypes

import (
"database/sql"
"database/sql/driver"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPrefixedIdentifierValue(t *testing.T) {
tests := []struct {
name string
prefix string
input string
want driver.Value
}{
{
name: "with prefix",
prefix: "test",
input: "test-000001",
want: "1",
},
{
name: "without prefix",
prefix: "test",
input: "123",
want: "123",
},
{
name: "empty input",
prefix: "test",
input: "",
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := PrefixedIdentifier{prefix: tt.prefix}
got, err := p.Value(tt.input)
require.NoError(t, err)

assert.Equal(t, tt.want, got)
})
}
}
func TestPrefixedIdentifierFromValue(t *testing.T) {
tests := []struct {
name string
prefix string
input driver.Value
want string
wantErr bool
}{
{
name: "valid input",
prefix: "test",
input: &sql.NullString{String: "1", Valid: true},
want: "test-000001",
},
{
name: "valid input",
prefix: "test",
input: &sql.NullString{String: "999999", Valid: true},
want: "test-999999",
},
{
name: "invalid input type",
prefix: "test",
input: "invalid",
wantErr: true,
},
{
name: "null string input",
prefix: "test",
input: &sql.NullString{String: "", Valid: false},
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := PrefixedIdentifier{prefix: tt.prefix}

got, err := p.FromValue(tt.input)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
47 changes: 42 additions & 5 deletions mixin/idmixin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,36 @@ package mixin
import (
"entgo.io/contrib/entgql"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"

"github.com/theopenlane/utils/ulids"

"github.com/theopenlane/entx/customtypes"

"github.com/theopenlane/entx"
)

// IDMixin holds the schema definition for the ID
type IDMixin struct {
mixin.Schema
// ExcludeMappingID to exclude the mapping ID field to the schema that can be used without exposing the primary ID
// by default, it is included in any schema that uses this mixin.
ExcludeMappingID bool
// IncludeMappingID to include the mapping ID field to the schema that can be used without exposing the primary ID
// by default, it is not included by default
IncludeMappingID bool
// IncludeHumanID to exclude the human ID field to
HumanIdentifierPrefix string
}

// NewIDMixinWithPrefixedID creates a new IDMixin and includes an additional prefixed ID, e.g. TSK-000001
func NewIDMixinWithPrefixedID(prefix string) IDMixin {
return IDMixin{HumanIdentifierPrefix: prefix}
}

// NewIDMixinWithMappingID creates a new IDMixin and includes an additional mapping ID
func NewIDMixinWithMappingID() IDMixin {
return IDMixin{IncludeMappingID: true}
}

// Fields of the IDMixin.
Expand All @@ -25,10 +41,13 @@ func (i IDMixin) Fields() []ent.Field {
field.String("id").
Immutable().
DefaultFunc(func() string { return ulids.New().String() }).
Annotations(entx.FieldSearchable()),
Annotations(
entx.FieldSearchable(),
entgql.Skip(entgql.SkipMutationCreateInput|entgql.SkipMutationUpdateInput),
),
}

if !i.ExcludeMappingID {
if !i.IncludeMappingID {
fields = append(fields,
field.String("mapping_id").
Immutable().
Expand All @@ -40,5 +59,23 @@ func (i IDMixin) Fields() []ent.Field {
)
}

if i.HumanIdentifierPrefix != "" {
fields = append(fields,
field.String("identifier").
Comment("a prefixed incremental field to use as a human readable identifier").
SchemaType(map[string]string{
dialect.Postgres: "SERIAL",
}).
ValueScanner(customtypes.NewPrefixedIdentifier(i.HumanIdentifierPrefix)).
Immutable().
Annotations(
entx.FieldSearchable(),
entgql.Skip(entgql.SkipMutationCreateInput|entgql.SkipMutationUpdateInput),
entsql.DefaultExpr("nextval('identifier_id_seq')"),
).
Unique(),
)
}

return fields
}

0 comments on commit a6ae194

Please sign in to comment.