entx

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 8, 2025 License: Apache-2.0 Imports: 17 Imported by: 3

README

Build status Quality gate Go Report Card Go Reference

entx

A helper package for working with ent, which includes the following high-level utilities:

  • Mixin used for assigning a different ID type than the one generated by ent (we use ULIDs)
  • Mixin used for assigning some default columns (created_at, created_by, etc. - check out the mixin directory for more details)
  • A vanilla, drop-in, setup for using ent with our standard tool chains gqlgen, gqlgenc, and some other helpers
  • Soft-delete extension with cascade delete functionality added in
  • Multi-driver support for various databases
  • SQLite connection interface management

enthistory

enthistory is a powerful extension for generating history tables using ent - the plugin will add-on to your existing entc usage and enumerate over your current schemas to create new "history" schemas containing an inventory of the changes related to the existing tables.

Credit to flume/enthistory for the inspiration - we chose to create our own for a number of reasons, some being:

  • We have existing patterns within the theopenlane/core repo which would today require an import of the entx package and likely be a non-starter for the original authors
  • We have more complex schemas, mixins, code gen usage; when attempting to use the originally developed plugin we ran into numerous problems based on the types / methods we had already chosen and was easier to short-term directly update with the changes we needed
  • integration with and/or mutual code updates for our "soft delete" constructs to continue to function
  • Specific desires / levels of control regarding data retention and tracking
  • Authorization policies specific to using openFGA may be harder for others to adopt

Installation

You can install enthistory by running the following command:

go get github.com/theopenlane/entx/history@latest

In addition to installing enthistory, you need to already have, or create two files (entc.go and generate.go) - this can be within your ent directory, but full instructions can be found in the upstream godoc documentation. The entc.go file should reference the ent history plugin via enthistory.New, and the options you include for the plugin depend on your desired implementation (see the Configuration section below) but you can use the following example for reference:

//go:build ignore

package main

import (
    "log"
    "github.com/theopenlane/entx/history"
    "entgo.io/ent/entc"
)

func main() {
    // create new extension with options
    historyExt := enthistory.New(
        enthistory.WithAuditing(),
    )

    // generate the history schemas
    if err := historyExt.GenerateSchemas(); err != nil {
        log.Fatalf("generating history schema: %v", err)
    }

    // run ent generate with extension for other templates
    if err := entc.Generate("./schema",
        &gen.Config{},
        entc.Extensions(
            historyExt,
        ),
    ); err != nil {
        log.Fatal("running ent codegen:", err)
    }
}

Be sure to read the upstream ent documentation describing the differences between entc and ent, but assuming you're using entc as a package you would want the minimum reference to the run the code generate processes with entc command like below:

package ent

//go:generate go run -mod=mod entc.go

You can additionally call other packages such as mockery within your generate.go - the core repo could be a good reference point for this.

Usage

Querying History

After generating your history tables from your schema, you can use the ent client to query the history tables. The generated code automatically creates history tables for every table in your schema and hooks them up to the ent client.

You can query the history tables directly, just like any other ent table. You can also retrieve the history of a specific row using the History() method.

enthistory tracks the user who updates a row if you provide a key during initialization. You can store a user's ID, email, IP address, etc., in the context with the key you provide to track it in the history.

Here's an example that demonstrates these features:

// Create
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1")
// Activate the history hooks on the client
client.WithHistory()
character, _ := client.Character.Create().SetName("Marceline").Save(ctx)
characterHistory, _ := character.History().All(ctx)
fmt.Println(len(characterHistory)) // 1

// Update
character, _ = character.Update().SetName("Marshall Lee").Save(ctx)
characterHistory, _ = character.History().All(ctx)
fmt.Println(len(characterHistory)) // 2

// Delete
client.Character.DeleteOne(character)
characterHistory, _ = character.History().All(ctx)
fmt.Println(len(characterHistory)) // 3

In addition to regular queries, you can perform common history queries such as retrieving the earliest history, the latest history, and the history of a row at a specific point in time. enthistory provides functions for these queries:

character, _ := client.Character.Query().First(ctx)

// Get the earliest history for this character (i.e., when the character was created)
earliest, _ := character.History().Earliest(ctx)

// Get the latest history for this character (i.e., the current state of the actual character)
latest, _ := character.History().Latest(ctx)

// Get the history for this character as it was at a given point in time
// (i.e., the state of the actual character at the given point in time)
historyNow, _ := character.History().AsOf(ctx, time.Now())

You can also use the .Next() and .Prev() methods to navigate to the next or previous history entries in time:

character, _ := client.Character.Query().First(ctx)

// Get the earliest history for this character (i.e., when the character was created)
earliest, _ := character.History().Earliest(ctx)

// Get the next history after the earliest history
next, _ := earliest.Next(ctx)

// Get the previous history before the next history
prev, _ := next.Prev(ctx)

// prev would now be the earliest history once again
fmt.Println(prev.ID == earliest.ID) // true
Restoring History

If you need to rollback a row in the database to a specific history entry, you can use the .Restore() function to accomplish that. NOTE: do not attempt to do this in your production environment or otherwise without testing in advance and creating your own SOP's around these types of procedures. By rolling back you are effectively overwriting your primary data source with a new entry, so use with caution!

Here's an example:

// Let's say we create this character
simon, _ := client.Character.Create().SetName("Simon Petrikov").Save(ctx)
// And we update the character's name
iceking, _ := simon.Update().SetName("Ice King").Save(ctx)
// We can find the exact point in history we want to restore, in this case, the oldest history entry
icekingHistory, _ := iceking.History().Order(ent.Asc(characterhistory.FieldHistoryTime)).First(ctx)
// And we can restore the value back to the original table
restored, _ = icekingHistory.Restore(ctx)

fmt.Println(simon.ID == restored.ID) // true
fmt.Println(simon.Name == restored.Name) // true
// The restoration is also tracked in history
simonHistory, _ := restored.History().All(ctx)
fmt.Println(len(simonHistory)) // 3
Auditing

enthistory includes tools for "auditing" history tables by providing a means of exporting the data inside of them. You can enable auditing by using the enthistory.WithAuditing() option when initializing the extension. The main tool for auditing is the Audit() method, which builds an audit log of the history tables that you can export as a file, upload to S3, or inspect.

Here's an example of how to use the Audit() method to export an audit log as a CSV file:

auditTable, _ := client.Audit(ctx)

The audit log contains six columns when user tracking is enabled. Here's an example of how the audit log might look:

Table Ref Id History Time Operation Changes Updated By
CharacterHistory 1 Sat Mar 18 16:31:31 2023 INSERT age: 47 name: "Simon Petrikov" 75
CharacterHistory 1 Sat Mar 18 16:31:31 2023 UPDATE name: "Simon Petrikov" -> "Ice King" 75
CharacterHistory 1 Sat Mar 18 16:31:31 2023 DELETE age: 47 name: "Ice King" 75

You can also build your own custom audit log using the .Diff() method on history models. The Diff() method returns the older history, the newer history, and the changes to fields when comparing the newer history to the older history.

Configuration Options

enthistory provides several configuration options to customize its behavior.

Setting All Tracked Fields as Nillable and/or Immutable

By default, enthistory does not modify the columns in the history tables that are being tracked from your original tables; it simply copies their state from ent when loading them.

However, you may want to set all tracked fields in the history tables as either Nillable or Immutable for various reasons. You can use the enthistory.WithNillableFields() option to set them all as Nillable, or enthistory.WithImmutableFields() to set them all as Immutable.

Note: Setting enthistory.WithNillableFields() will remove the ability to call the Restore() function on a history object. Setting all fields to Nillable causes the history tables to diverge from the original tables, and the unpredictability of that means the Restore() function cannot be generated.

History Time Indexing

By default, an index is not placed on the history_time field. If you want to enable indexing on the history_time field, you can use the enthistory.WithHistoryTimeIndex() configuration option. This option gives you more control over indexing based on your specific needs.

Updated By

To track which users are making changes to your tables, you can use the enthistory.WithUpdatedBy() option when initializing the extension. You need to provide a key name (string) and specify the type of value (enthistory.ValueTypeInt for integers or enthistory.ValueTypeString for strings). The value corresponding to the key should be stored in the context using context.WithValue(). If you don't plan to use this feature, you can omit - you may also already have an existing audit mixin or similar which tracks the user performing the action, in which case, these fields would already be contained within the created history tables.

// Example for tracking user ID
enthistory.WithUpdatedBy("userId", enthistory.ValueTypeInt)

// Example for tracking user email
enthistory.WithUpdatedBy("userEmail", enthistory.ValueTypeString)
Deleted By

To track which users are making changes to your tables, you can use the enthistory.WithDeletedBy() option when initializing the extension. You need to provide a key name (string) and specify the type of value (enthistory.ValueTypeInt for integers or enthistory.ValueTypeString for strings). The value corresponding to the key should be stored in the context using context.WithValue(). If you don't plan to use this feature, you can omit it.

// Example for tracking user ID
enthistory.WithDeletedBy("userId", enthistory.ValueTypeInt)

// Example for tracking user email
enthistory.WithDeletedBy("userEmail", enthistory.ValueTypeString)
Auditing

As mentioned earlier, you can enable auditing by using the enthistory.WithAuditing() configuration option when initializing the extension.

Excluding History on a Schema

enthistory is designed to always track history, but in cases where you don't want to generate history tables for a particular schema, you can apply annotations to the schema to exclude it. Here's an example:

func (Character) Annotations() []schema.Annotation {
    return []schema.Annotation{
        enthistory.Annotations{
            // Exclude history tables for this schema
            Exclude: true,
        },
    }
}
Setting a Schema Path

If you want to set an alternative schema location other than ent/schema, you can use the enthistory.WithSchemaPath() configuration option. The schema path should be the same as the one set in the entc.Generate function. If you don't plan to set an alternative schema location, you can omit this option.

func main() {
    entc.Generate("./schema2",
        &gen.Config{},
        entc.Extensions(
            enthistory.NewHistoryExtension(
                enthistory.WithSchemaPath("./schema2")
            ),
        ),
    )
}

For a complete example of using a custom schema path, refer to the custompaths example.

Setting a Schema Name

If you want to set the schema name for entsql, you can use the enthistory.WithSchemaName() configuration option. This can be used in conjunction with ent Multiple Schema Migrations and the Schema Config features.

Adding GQL Query

If you are using gqlgen and want to generate the query resolvers for the history schemas, you can use the enthistory.WithGQLQuery() configuration option. With this enabled, ent.resolvers with be created, such as:

// TodoHistories is the resolver for the todoHistories field.
func (r *queryResolver) TodoHistories(ctx context.Context, after *entgql.Cursor[string], first *int, before *entgql.Cursor[string], last *int, orderBy *generated.TodoHistoryOrder, where *generated.OrganizationHistoryWhereInput) (*generated.TodoHistoryConnection, error) {
    panic(fmt.Errorf("not implemented: TodoHistories - todoHistories"))
}

Adding a Skipper Function

If you want to conditionally skip saving history data, you can use the enthistory.WithSkipper() configuration option. This should be the string representation that returns true or false. The function has access to the mutation object and the context. For example:

    skipper := `
        hasFeature := m.CheckFeature(ctx)

        return !hasFeature
    `

    historyExt := enthistory.NewHistoryExtension(
        enthistory.WithSkipper(skipper),
    )

Caveats

Here are a few caveats to keep in mind when using enthistory:

Edges

To track edges with history, you need to manage your own through tables. enthistory does not hook into the ent-generated through tables automatically, but managing through tables manually is straightforward. Note that if you use the setters for edges on the main schema tables, the history on the through tables won't be tracked. To track history on through tables, you must update the through tables directly with the required information.

Instead of using .AddFriends() like this:

finn, _ := client.Character.Create().SetName("Finn the Human").Save(ctx)
jake, _ := client.Character.Create().SetName("Jake the Dog").Save(ctx)
finn, _ = finn.Update().AddFriends(jake).Save(ctx)

You should use the Friendship through table:

finn, _ := client.Character.Create().SetName("Finn the Human").Save(ctx)
jake, _ := client.Character.Create().SetName("Jake the Dog").Save(ctx)
friendship, _ := client.Friendship.Create().SetCharacterID(finn.ID).SetFriendID(jake.ID).Save(ctx)

For more information on through tables and edges, refer to the ent documentation.

Enums

If your ent schemas contain enum fields, it is recommended to create Go enums and set the GoType on the enum field. This is because ent generates a unique enum type for both your schema and the history table schema, which may not work well together.

Instead of using .Values() like this:

field.Enum("action").
    Values("PUSH", "PULL")

Use .GoType() like this:

field.Enum("action").
    GoType(types.Action(""))

For more information on enums, refer to the ent documentation.

Contributing

Please read the contributing guide.

Documentation

Overview

Package entx is a package of tools for interacting with ent

Index

Constants

View Source
const (
	DefaultCacheTTL = 1 * time.Second
)

Variables

View Source
var CascadeAnnotationName = "OPENLANE_CASCADE"

CascadeAnnotationName is a name for our cascading delete annotation

View Source
var CascadeThroughAnnotationName = "OPENLANE_CASCADE_THROUGH"

CascadeThroughAnnotationName is a name for our cascading through edge delete annotation

View Source
var (
	// ErrUnsupportedDialect is returned when an unsupported dialect is used
	ErrUnsupportedDialect = errors.New("unsupported dialect")
)
View Source
var QueryGenAnnotationName = "OPENLANE_QUERYGEN"

QueryGenAnnotationName is a name for our graphql query generation annotation

View Source
var SchemaGenAnnotationName = "OPENLANE_SCHEMAGEN"

SchemaGenAnnotationName is a name for our graphql schema generation annotation

View Source
var SearchFieldAnnotationName = "OPENLANE_SEARCH"

SearchFieldAnnotationName is a name for the search field annotation

Functions

func CheckEntDialect

func CheckEntDialect(d string) (string, error)

CheckEntDialect checks if the dialect is supported and returns the ent dialect corresponding to the given dialect

func CheckIsSoftDelete

func CheckIsSoftDelete(ctx context.Context) bool

CheckIsSoftDelete checks if the softDeleteKey is set in the context

func CheckMultiwriteSupport

func CheckMultiwriteSupport(d string) bool

CheckMultiwriteSupport checks if the dialect supports multiwrite

func CheckSkipSoftDelete

func CheckSkipSoftDelete(ctx context.Context) bool

CheckSkipSoftDelete checks if the SoftDeleteSkipKey is set in the context

func Healthcheck

func Healthcheck(client *entsql.Driver) func(ctx context.Context) error

Healthcheck pings the DB to check if the connection is working

func IsSoftDelete

func IsSoftDelete(parent context.Context) context.Context

IsSoftDelete returns a new context that informs the delete is a soft-delete for interceptor/hooks.

func MarshalRawMessage

func MarshalRawMessage(t json.RawMessage) graphql.Marshaler

MarshalRawMessage provides a graphql.Marshaler for json.RawMessage

func SkipSoftDelete

func SkipSoftDelete(parent context.Context) context.Context

SkipSoftDelete returns a new context that skips the soft-delete interceptor/hooks.

func UnmarshalRawMessage

func UnmarshalRawMessage(v interface{}) (json.RawMessage, error)

UnmarshalRawMessage provides a graphql.Unmarshaler for json.RawMessage

Types

type CascadeAnnotation

type CascadeAnnotation struct {
	Field string
}

CascadeAnnotation is an annotation used to indicate that an edge should be cascaded

func CascadeAnnotationField

func CascadeAnnotationField(fieldname string) *CascadeAnnotation

CascadeAnnotationField sets the field name of the edge containing the ID of a record from the current schema

func (*CascadeAnnotation) Decode

func (a *CascadeAnnotation) Decode(annotation interface{}) error

Decode unmarshalls the CascadeAnnotation

func (CascadeAnnotation) Name

func (a CascadeAnnotation) Name() string

Name returns the name of the CascadeAnnotation

type CascadeThroughAnnotation

type CascadeThroughAnnotation struct {
	Schemas []ThroughCleanup
}

CascadeThroughAnnotation is an annotation used to indicate that an edge should be cascaded through

func CascadeThroughAnnotationField

func CascadeThroughAnnotationField(schemas []ThroughCleanup) *CascadeThroughAnnotation

CascadeThroughAnnotationField sets the field name of the edge containing the ID of a record from the current schema

func (*CascadeThroughAnnotation) Decode

func (a *CascadeThroughAnnotation) Decode(annotation interface{}) error

Decode unmarshalls the CascadeThroughAnnotation

func (CascadeThroughAnnotation) Name

Name returns the name of the CascadeThroughAnnotation

type Config

type Config struct {
	// Debug to print debug database logs
	Debug bool `json:"debug" koanf:"debug" jsonschema:"description=debug enables printing the debug database logs" default:"false"`
	// DatabaseName is the name of the database to use with otel tracing
	DatabaseName string `` /* 134-byte string literal not displayed */
	// DriverName name from dialect.Driver
	DriverName string `` /* 167-byte string literal not displayed */
	// MultiWrite enabled writing to two databases simultaneously
	MultiWrite bool `json:"multiWrite" koanf:"multiWrite" jsonschema:"description=enables writing to two databases simultaneously" default:"false"`
	// PrimaryDBSource is the primary database source for all read and write operations
	PrimaryDBSource string `` /* 135-byte string literal not displayed */
	// SecondaryDBSource for when multi write is enabled
	SecondaryDBSource string `` /* 156-byte string literal not displayed */
	// CacheTTL to have results cached for subsequent requests
	CacheTTL time.Duration `json:"cacheTTL" koanf:"cacheTTL" jsonschema:"description=cache results for subsequent requests, defaults to 1s" default:"1s"`
	// RunMigrations to run migrations on startup
	RunMigrations bool `json:"runMigrations" koanf:"runMigrations" jsonschema:"description=run migrations on startup" default:"true"`
	// MigrationProvider to use for running migrations
	MigrationProvider string `` /* 140-byte string literal not displayed */
	// EnableHistory to enable history data to be logged to the database
	EnableHistory bool `` /* 132-byte string literal not displayed */
}

Config Settings for the ent database client

type DBOption

type DBOption func(opts *EntClientConfig)

DBOption allows users to optionally supply configuration to the ent connection

func WithSecondaryDB

func WithSecondaryDB() DBOption

WithSecondaryDB sets the secondary db connection if the driver supports multiwrite

type EntClientConfig

type EntClientConfig struct {
	// contains filtered or unexported fields
}

EntClientConfig configures the entsql drivers

func NewDBConfig

func NewDBConfig(c Config, opts ...DBOption) (*EntClientConfig, error)

NewDBConfig returns a new ent database configuration

func (*EntClientConfig) GetPrimaryDB

func (c *EntClientConfig) GetPrimaryDB() *entsql.Driver

GetPrimaryDB returns the primary database configuration

func (*EntClientConfig) GetSecondaryDB

func (c *EntClientConfig) GetSecondaryDB() *entsql.Driver

GetSecondaryDB returns the secondary db connection

func (*EntClientConfig) NewEntDB

func (c *EntClientConfig) NewEntDB(dataSource string) (*entsql.Driver, error)

NewEntDB creates a new ent database connection

type Extension

type Extension struct {
	entc.DefaultExtension
	// contains filtered or unexported fields
}

Extension is an implementation of entc.Extension

func NewExtension

func NewExtension(opts ...ExtensionOption) (*Extension, error)

NewExtension returns an entc Extension that allows the entx package to generate the schema changes and templates needed to function

func (*Extension) GQLSchemaHooks

func (e *Extension) GQLSchemaHooks() []entgql.SchemaHook

GQLSchemaHooks of the extension to seamlessly edit the final gql interface

func (*Extension) Templates

func (e *Extension) Templates() []*gen.Template

Templates of the extension

type ExtensionOption

type ExtensionOption func(*Extension) error

ExtensionOption allow for control over the behavior of the generator

func WithJSONScalar

func WithJSONScalar() ExtensionOption

WithJSONScalar adds the JSON scalar definition

type MultiWriteDriver

type MultiWriteDriver struct {
	// Wp (write-primary), Ws (write-secondary) Drivers
	Wp, Ws dialect.Driver
}

MultiWriteDriver allows you to write to a primary and secondary database

func (*MultiWriteDriver) BeginTx

func (d *MultiWriteDriver) BeginTx(ctx context.Context, opts *sql.TxOptions) (dialect.Tx, error)

BeginTx adds an log-id for the transaction and calls the underlying driver BeginTx command if it is supported.

func (*MultiWriteDriver) Close

func (d *MultiWriteDriver) Close() error

Close the underlying connections

func (*MultiWriteDriver) Dialect

func (d *MultiWriteDriver) Dialect() string

Dialect returns the dialect name of the primary driver

func (*MultiWriteDriver) Exec

func (d *MultiWriteDriver) Exec(ctx context.Context, query string, args, v any) error

Exec logs its params and calls the underlying driver Exec method for both write drivers

func (*MultiWriteDriver) Query

func (d *MultiWriteDriver) Query(ctx context.Context, query string, args, v any) error

Query will query the primary write database

func (*MultiWriteDriver) Tx

Tx wraps the Exec and Query operations in transaction.

type QueryGenAnnotation

type QueryGenAnnotation struct {
	Skip bool
}

QueryGenAnnotation is an annotation used to indicate that query generation should be skipped for this type

func QueryGenSkip

func QueryGenSkip(skip bool) *QueryGenAnnotation

QueryGenSkip sets whether the query generation should be skipped for this type

func (*QueryGenAnnotation) Decode

func (a *QueryGenAnnotation) Decode(annotation interface{}) error

Decode unmarshalls the QueryGenAnnotation

func (QueryGenAnnotation) Name

func (a QueryGenAnnotation) Name() string

Name returns the name of the QueryGenAnnotation

type SchemaGenAnnotation

type SchemaGenAnnotation struct {
	// Skip indicates that the schema generation should be skipped for this type
	Skip bool
	// SkipSearch indicates that the schema should not be searchable
	// Schemas are also not searchable if not fields are marked as searchable
	SkipSearch bool
}

SchemaGenAnnotation is an annotation used to indicate that schema generation should be skipped for this type When Skip is true, the search schema generation is always skipped SkipSearch allow for schemas to be be opt out of search schema generation

func SchemaGenSkip

func SchemaGenSkip(skip bool) *SchemaGenAnnotation

SchemaGenSkip sets whether the schema generation should be skipped for this type

func SchemaSearchable

func SchemaSearchable(s bool) *SchemaGenAnnotation

SchemaSearchable sets if the schema should be searchable and generated in the search schema template

func (*SchemaGenAnnotation) Decode

func (a *SchemaGenAnnotation) Decode(annotation interface{}) error

Decode unmarshalls the SchemaGenAnnotation

func (SchemaGenAnnotation) Name

func (a SchemaGenAnnotation) Name() string

Name returns the name of the SchemaGenAnnotation

type SearchFieldAnnotation

type SearchFieldAnnotation struct {
	// Searchable indicates that the field should be searchable
	Searchable bool
	// ExcludeAdmin indicates that the field will be excluded from the admin search which includes all fields by default
	ExcludeAdmin bool
	// JSONPath is the path to the field in the JSON object
	JSONPath string
	// JSONDotPath is the path to the field in the JSON object using dot notation
	JSONDotPath string
}

SearchFieldAnnotation is an annotation used to indicate that the field should be searchable

func FieldAdminSearchable added in v0.1.5

func FieldAdminSearchable(s bool) *SearchFieldAnnotation

FieldAdminSearchable returns a new SearchFieldAnnotation with the exclude admin searchable flag set

func FieldJSONDotPathSearchable added in v0.1.7

func FieldJSONDotPathSearchable(path string) *SearchFieldAnnotation

FieldJSONDotPathSearchable returns a new SearchFieldAnnotation with the searchable flag set and the JSONDotPath set

func FieldJSONPathSearchable added in v0.1.7

func FieldJSONPathSearchable(path string) *SearchFieldAnnotation

FieldJSONPathSearchable returns a new SearchFieldAnnotation with the searchable flag set and the JSONPath set

func FieldSearchable

func FieldSearchable() *SearchFieldAnnotation

FieldSearchable returns a new SearchFieldAnnotation with the searchable flag set

func (*SearchFieldAnnotation) Decode

func (a *SearchFieldAnnotation) Decode(annotation interface{}) error

Decode unmarshalls the SearchFieldAnnotation

func (SearchFieldAnnotation) Name

func (a SearchFieldAnnotation) Name() string

Name returns the name of the SearchFieldAnnotation

type SoftDeleteKey

type SoftDeleteKey struct{}

SoftDeleteKey is used to indicate a soft delete mutation is in progress

type SoftDeleteSkipKey

type SoftDeleteSkipKey struct{}

SoftDeleteSkipKey is used to indicate to allow soft deleted records to be returned in records and to skip soft delete on mutations and proceed with a regular delete

type ThroughCleanup

type ThroughCleanup struct {
	Field   string
	Through string
}

ThroughCleanup is a struct used to indicate the field and through edge to cascade through

Directories

Path Synopsis
Package genhooks provides a set of entgo hooks for generating files from the ent schema.
Package genhooks provides a set of entgo hooks for generating files from the ent schema.
package history provides code generation to add history tables for an ent schema
package history provides code generation to add history tables for an ent schema
Package mixin contains the mixin package
Package mixin contains the mixin package
vanilla
_example/ent/enums
Package enums has enums
Package enums has enums

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL