diff --git a/tavern/internal/ent/client.go b/tavern/internal/ent/client.go index 88ef3dc98..68f82957e 100644 --- a/tavern/internal/ent/client.go +++ b/tavern/internal/ent/client.go @@ -531,6 +531,22 @@ func (c *FileClient) GetX(ctx context.Context, id int) *File { return obj } +// QueryTomes queries the tomes edge of a File. +func (c *FileClient) QueryTomes(f *File) *TomeQuery { + query := (&TomeClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := f.ID + step := sqlgraph.NewStep( + sqlgraph.From(file.Table, file.FieldID, id), + sqlgraph.To(tome.Table, tome.FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, file.TomesTable, file.TomesPrimaryKey...), + ) + fromV = sqlgraph.Neighbors(f.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *FileClient) Hooks() []Hook { hooks := c.hooks.File @@ -1350,7 +1366,7 @@ func (c *TomeClient) QueryFiles(t *Tome) *FileQuery { step := sqlgraph.NewStep( sqlgraph.From(tome.Table, tome.FieldID, id), sqlgraph.To(file.Table, file.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, tome.FilesTable, tome.FilesColumn), + sqlgraph.Edge(sqlgraph.M2M, false, tome.FilesTable, tome.FilesPrimaryKey...), ) fromV = sqlgraph.Neighbors(t.driver.Dialect(), step) return fromV, nil diff --git a/tavern/internal/ent/file.go b/tavern/internal/ent/file.go index cb948ce2f..7033c1a06 100644 --- a/tavern/internal/ent/file.go +++ b/tavern/internal/ent/file.go @@ -28,11 +28,35 @@ type File struct { // A SHA3 digest of the content field Hash string `json:"hash,omitempty"` // The content of the file - Content []byte `json:"content,omitempty"` - tome_files *int + Content []byte `json:"content,omitempty"` + // Edges holds the relations/edges for other nodes in the graph. + // The values are being populated by the FileQuery when eager-loading is set. + Edges FileEdges `json:"edges"` selectValues sql.SelectValues } +// FileEdges holds the relations/edges for other nodes in the graph. +type FileEdges struct { + // Tomes holds the value of the tomes edge. + Tomes []*Tome `json:"tomes,omitempty"` + // loadedTypes holds the information for reporting if a + // type was loaded (or requested) in eager-loading or not. + loadedTypes [1]bool + // totalCount holds the count of the edges above. + totalCount [1]map[string]int + + namedTomes map[string][]*Tome +} + +// TomesOrErr returns the Tomes value or an error if the edge +// was not loaded in eager-loading. +func (e FileEdges) TomesOrErr() ([]*Tome, error) { + if e.loadedTypes[0] { + return e.Tomes, nil + } + return nil, &NotLoadedError{edge: "tomes"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*File) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) @@ -46,8 +70,6 @@ func (*File) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullString) case file.FieldCreatedAt, file.FieldLastModifiedAt: values[i] = new(sql.NullTime) - case file.ForeignKeys[0]: // tome_files - values[i] = new(sql.NullInt64) default: values[i] = new(sql.UnknownType) } @@ -105,13 +127,6 @@ func (f *File) assignValues(columns []string, values []any) error { } else if value != nil { f.Content = *value } - case file.ForeignKeys[0]: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for edge-field tome_files", value) - } else if value.Valid { - f.tome_files = new(int) - *f.tome_files = int(value.Int64) - } default: f.selectValues.Set(columns[i], values[i]) } @@ -125,6 +140,11 @@ func (f *File) Value(name string) (ent.Value, error) { return f.selectValues.Get(name) } +// QueryTomes queries the "tomes" edge of the File entity. +func (f *File) QueryTomes() *TomeQuery { + return NewFileClient(f.config).QueryTomes(f) +} + // Update returns a builder for updating this File. // Note that you need to call File.Unwrap() before calling this method if this File // was returned from a transaction, and the transaction was committed or rolled back. @@ -169,5 +189,29 @@ func (f *File) String() string { return builder.String() } +// NamedTomes returns the Tomes named value or an error if the edge was not +// loaded in eager-loading with this name. +func (f *File) NamedTomes(name string) ([]*Tome, error) { + if f.Edges.namedTomes == nil { + return nil, &NotLoadedError{edge: name} + } + nodes, ok := f.Edges.namedTomes[name] + if !ok { + return nil, &NotLoadedError{edge: name} + } + return nodes, nil +} + +func (f *File) appendNamedTomes(name string, edges ...*Tome) { + if f.Edges.namedTomes == nil { + f.Edges.namedTomes = make(map[string][]*Tome) + } + if len(edges) == 0 { + f.Edges.namedTomes[name] = []*Tome{} + } else { + f.Edges.namedTomes[name] = append(f.Edges.namedTomes[name], edges...) + } +} + // Files is a parsable slice of File. type Files []*File diff --git a/tavern/internal/ent/file/file.go b/tavern/internal/ent/file/file.go index 578ee6500..74b9302dc 100644 --- a/tavern/internal/ent/file/file.go +++ b/tavern/internal/ent/file/file.go @@ -7,6 +7,7 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" ) const ( @@ -26,8 +27,15 @@ const ( FieldHash = "hash" // FieldContent holds the string denoting the content field in the database. FieldContent = "content" + // EdgeTomes holds the string denoting the tomes edge name in mutations. + EdgeTomes = "tomes" // Table holds the table name of the file in the database. Table = "files" + // TomesTable is the table that holds the tomes relation/edge. The primary key declared below. + TomesTable = "tome_files" + // TomesInverseTable is the table name for the Tome entity. + // It exists in this package in order to avoid circular dependency with the "tome" package. + TomesInverseTable = "tomes" ) // Columns holds all SQL columns for file fields. @@ -41,11 +49,11 @@ var Columns = []string{ FieldContent, } -// ForeignKeys holds the SQL foreign-keys that are owned by the "files" -// table and are not defined as standalone fields in the schema. -var ForeignKeys = []string{ - "tome_files", -} +var ( + // TomesPrimaryKey and TomesColumn2 are the table columns denoting the + // primary key for the tomes relation (M2M). + TomesPrimaryKey = []string{"tome_id", "file_id"} +) // ValidColumn reports if the column name is valid (part of the table columns). func ValidColumn(column string) bool { @@ -54,11 +62,6 @@ func ValidColumn(column string) bool { return true } } - for i := range ForeignKeys { - if column == ForeignKeys[i] { - return true - } - } return false } @@ -117,3 +120,24 @@ func BySize(opts ...sql.OrderTermOption) OrderOption { func ByHash(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldHash, opts...).ToFunc() } + +// ByTomesCount orders the results by tomes count. +func ByTomesCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newTomesStep(), opts...) + } +} + +// ByTomes orders the results by tomes terms. +func ByTomes(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newTomesStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} +func newTomesStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(TomesInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, TomesTable, TomesPrimaryKey...), + ) +} diff --git a/tavern/internal/ent/file/where.go b/tavern/internal/ent/file/where.go index 697af8dee..3ec089b50 100644 --- a/tavern/internal/ent/file/where.go +++ b/tavern/internal/ent/file/where.go @@ -6,6 +6,7 @@ import ( "time" "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" "realm.pub/tavern/internal/ent/predicate" ) @@ -374,6 +375,29 @@ func ContentLTE(v []byte) predicate.File { return predicate.File(sql.FieldLTE(FieldContent, v)) } +// HasTomes applies the HasEdge predicate on the "tomes" edge. +func HasTomes() predicate.File { + return predicate.File(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, TomesTable, TomesPrimaryKey...), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasTomesWith applies the HasEdge predicate on the "tomes" edge with a given conditions (other predicates). +func HasTomesWith(preds ...predicate.Tome) predicate.File { + return predicate.File(func(s *sql.Selector) { + step := newTomesStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.File) predicate.File { return predicate.File(sql.AndPredicates(predicates...)) diff --git a/tavern/internal/ent/file_create.go b/tavern/internal/ent/file_create.go index cb47d5742..b8ae1f94a 100644 --- a/tavern/internal/ent/file_create.go +++ b/tavern/internal/ent/file_create.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "realm.pub/tavern/internal/ent/file" + "realm.pub/tavern/internal/ent/tome" ) // FileCreate is the builder for creating a File entity. @@ -82,6 +83,21 @@ func (fc *FileCreate) SetContent(b []byte) *FileCreate { return fc } +// AddTomeIDs adds the "tomes" edge to the Tome entity by IDs. +func (fc *FileCreate) AddTomeIDs(ids ...int) *FileCreate { + fc.mutation.AddTomeIDs(ids...) + return fc +} + +// AddTomes adds the "tomes" edges to the Tome entity. +func (fc *FileCreate) AddTomes(t ...*Tome) *FileCreate { + ids := make([]int, len(t)) + for i := range t { + ids[i] = t[i].ID + } + return fc.AddTomeIDs(ids...) +} + // Mutation returns the FileMutation object of the builder. func (fc *FileCreate) Mutation() *FileMutation { return fc.mutation @@ -226,6 +242,22 @@ func (fc *FileCreate) createSpec() (*File, *sqlgraph.CreateSpec) { _spec.SetField(file.FieldContent, field.TypeBytes, value) _node.Content = value } + if nodes := fc.mutation.TomesIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } diff --git a/tavern/internal/ent/file_query.go b/tavern/internal/ent/file_query.go index dc299e3e3..b602d08fc 100644 --- a/tavern/internal/ent/file_query.go +++ b/tavern/internal/ent/file_query.go @@ -4,6 +4,7 @@ package ent import ( "context" + "database/sql/driver" "fmt" "math" @@ -12,18 +13,20 @@ import ( "entgo.io/ent/schema/field" "realm.pub/tavern/internal/ent/file" "realm.pub/tavern/internal/ent/predicate" + "realm.pub/tavern/internal/ent/tome" ) // FileQuery is the builder for querying File entities. type FileQuery struct { config - ctx *QueryContext - order []file.OrderOption - inters []Interceptor - predicates []predicate.File - withFKs bool - modifiers []func(*sql.Selector) - loadTotal []func(context.Context, []*File) error + ctx *QueryContext + order []file.OrderOption + inters []Interceptor + predicates []predicate.File + withTomes *TomeQuery + modifiers []func(*sql.Selector) + loadTotal []func(context.Context, []*File) error + withNamedTomes map[string]*TomeQuery // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -60,6 +63,28 @@ func (fq *FileQuery) Order(o ...file.OrderOption) *FileQuery { return fq } +// QueryTomes chains the current query on the "tomes" edge. +func (fq *FileQuery) QueryTomes() *TomeQuery { + query := (&TomeClient{config: fq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := fq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := fq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(file.Table, file.FieldID, selector), + sqlgraph.To(tome.Table, tome.FieldID), + sqlgraph.Edge(sqlgraph.M2M, true, file.TomesTable, file.TomesPrimaryKey...), + ) + fromU = sqlgraph.SetNeighbors(fq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first File entity from the query. // Returns a *NotFoundError when no File was found. func (fq *FileQuery) First(ctx context.Context) (*File, error) { @@ -252,12 +277,24 @@ func (fq *FileQuery) Clone() *FileQuery { order: append([]file.OrderOption{}, fq.order...), inters: append([]Interceptor{}, fq.inters...), predicates: append([]predicate.File{}, fq.predicates...), + withTomes: fq.withTomes.Clone(), // clone intermediate query. sql: fq.sql.Clone(), path: fq.path, } } +// WithTomes tells the query-builder to eager-load the nodes that are connected to +// the "tomes" edge. The optional arguments are used to configure the query builder of the edge. +func (fq *FileQuery) WithTomes(opts ...func(*TomeQuery)) *FileQuery { + query := (&TomeClient{config: fq.config}).Query() + for _, opt := range opts { + opt(query) + } + fq.withTomes = query + return fq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -334,19 +371,19 @@ func (fq *FileQuery) prepareQuery(ctx context.Context) error { func (fq *FileQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*File, error) { var ( - nodes = []*File{} - withFKs = fq.withFKs - _spec = fq.querySpec() + nodes = []*File{} + _spec = fq.querySpec() + loadedTypes = [1]bool{ + fq.withTomes != nil, + } ) - if withFKs { - _spec.Node.Columns = append(_spec.Node.Columns, file.ForeignKeys...) - } _spec.ScanValues = func(columns []string) ([]any, error) { return (*File).scanValues(nil, columns) } _spec.Assign = func(columns []string, values []any) error { node := &File{config: fq.config} nodes = append(nodes, node) + node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } if len(fq.modifiers) > 0 { @@ -361,6 +398,20 @@ func (fq *FileQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*File, e if len(nodes) == 0 { return nodes, nil } + if query := fq.withTomes; query != nil { + if err := fq.loadTomes(ctx, query, nodes, + func(n *File) { n.Edges.Tomes = []*Tome{} }, + func(n *File, e *Tome) { n.Edges.Tomes = append(n.Edges.Tomes, e) }); err != nil { + return nil, err + } + } + for name, query := range fq.withNamedTomes { + if err := fq.loadTomes(ctx, query, nodes, + func(n *File) { n.appendNamedTomes(name) }, + func(n *File, e *Tome) { n.appendNamedTomes(name, e) }); err != nil { + return nil, err + } + } for i := range fq.loadTotal { if err := fq.loadTotal[i](ctx, nodes); err != nil { return nil, err @@ -369,6 +420,68 @@ func (fq *FileQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*File, e return nodes, nil } +func (fq *FileQuery) loadTomes(ctx context.Context, query *TomeQuery, nodes []*File, init func(*File), assign func(*File, *Tome)) error { + edgeIDs := make([]driver.Value, len(nodes)) + byID := make(map[int]*File) + nids := make(map[int]map[*File]struct{}) + for i, node := range nodes { + edgeIDs[i] = node.ID + byID[node.ID] = node + if init != nil { + init(node) + } + } + query.Where(func(s *sql.Selector) { + joinT := sql.Table(file.TomesTable) + s.Join(joinT).On(s.C(tome.FieldID), joinT.C(file.TomesPrimaryKey[0])) + s.Where(sql.InValues(joinT.C(file.TomesPrimaryKey[1]), edgeIDs...)) + columns := s.SelectedColumns() + s.Select(joinT.C(file.TomesPrimaryKey[1])) + s.AppendSelect(columns...) + s.SetDistinct(false) + }) + if err := query.prepareQuery(ctx); err != nil { + return err + } + qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) { + assign := spec.Assign + values := spec.ScanValues + spec.ScanValues = func(columns []string) ([]any, error) { + values, err := values(columns[1:]) + if err != nil { + return nil, err + } + return append([]any{new(sql.NullInt64)}, values...), nil + } + spec.Assign = func(columns []string, values []any) error { + outValue := int(values[0].(*sql.NullInt64).Int64) + inValue := int(values[1].(*sql.NullInt64).Int64) + if nids[inValue] == nil { + nids[inValue] = map[*File]struct{}{byID[outValue]: {}} + return assign(columns[1:], values[1:]) + } + nids[inValue][byID[outValue]] = struct{}{} + return nil + } + }) + }) + neighbors, err := withInterceptors[[]*Tome](ctx, query, qr, query.inters) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nids[n.ID] + if !ok { + return fmt.Errorf(`unexpected "tomes" node returned %v`, n.ID) + } + for kn := range nodes { + assign(kn, n) + } + } + return nil +} + func (fq *FileQuery) sqlCount(ctx context.Context) (int, error) { _spec := fq.querySpec() if len(fq.modifiers) > 0 { @@ -453,6 +566,20 @@ func (fq *FileQuery) sqlQuery(ctx context.Context) *sql.Selector { return selector } +// WithNamedTomes tells the query-builder to eager-load the nodes that are connected to the "tomes" +// edge with the given name. The optional arguments are used to configure the query builder of the edge. +func (fq *FileQuery) WithNamedTomes(name string, opts ...func(*TomeQuery)) *FileQuery { + query := (&TomeClient{config: fq.config}).Query() + for _, opt := range opts { + opt(query) + } + if fq.withNamedTomes == nil { + fq.withNamedTomes = make(map[string]*TomeQuery) + } + fq.withNamedTomes[name] = query + return fq +} + // FileGroupBy is the group-by builder for File entities. type FileGroupBy struct { selector diff --git a/tavern/internal/ent/file_update.go b/tavern/internal/ent/file_update.go index 0bc9ae52b..561cf432c 100644 --- a/tavern/internal/ent/file_update.go +++ b/tavern/internal/ent/file_update.go @@ -13,6 +13,7 @@ import ( "entgo.io/ent/schema/field" "realm.pub/tavern/internal/ent/file" "realm.pub/tavern/internal/ent/predicate" + "realm.pub/tavern/internal/ent/tome" ) // FileUpdate is the builder for updating File entities. @@ -73,11 +74,47 @@ func (fu *FileUpdate) SetContent(b []byte) *FileUpdate { return fu } +// AddTomeIDs adds the "tomes" edge to the Tome entity by IDs. +func (fu *FileUpdate) AddTomeIDs(ids ...int) *FileUpdate { + fu.mutation.AddTomeIDs(ids...) + return fu +} + +// AddTomes adds the "tomes" edges to the Tome entity. +func (fu *FileUpdate) AddTomes(t ...*Tome) *FileUpdate { + ids := make([]int, len(t)) + for i := range t { + ids[i] = t[i].ID + } + return fu.AddTomeIDs(ids...) +} + // Mutation returns the FileMutation object of the builder. func (fu *FileUpdate) Mutation() *FileMutation { return fu.mutation } +// ClearTomes clears all "tomes" edges to the Tome entity. +func (fu *FileUpdate) ClearTomes() *FileUpdate { + fu.mutation.ClearTomes() + return fu +} + +// RemoveTomeIDs removes the "tomes" edge to Tome entities by IDs. +func (fu *FileUpdate) RemoveTomeIDs(ids ...int) *FileUpdate { + fu.mutation.RemoveTomeIDs(ids...) + return fu +} + +// RemoveTomes removes "tomes" edges to Tome entities. +func (fu *FileUpdate) RemoveTomes(t ...*Tome) *FileUpdate { + ids := make([]int, len(t)) + for i := range t { + ids[i] = t[i].ID + } + return fu.RemoveTomeIDs(ids...) +} + // Save executes the query and returns the number of nodes affected by the update operation. func (fu *FileUpdate) Save(ctx context.Context) (int, error) { if err := fu.defaults(); err != nil { @@ -170,6 +207,51 @@ func (fu *FileUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := fu.mutation.Content(); ok { _spec.SetField(file.FieldContent, field.TypeBytes, value) } + if fu.mutation.TomesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := fu.mutation.RemovedTomesIDs(); len(nodes) > 0 && !fu.mutation.TomesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := fu.mutation.TomesIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if n, err = sqlgraph.UpdateNodes(ctx, fu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{file.Label} @@ -235,11 +317,47 @@ func (fuo *FileUpdateOne) SetContent(b []byte) *FileUpdateOne { return fuo } +// AddTomeIDs adds the "tomes" edge to the Tome entity by IDs. +func (fuo *FileUpdateOne) AddTomeIDs(ids ...int) *FileUpdateOne { + fuo.mutation.AddTomeIDs(ids...) + return fuo +} + +// AddTomes adds the "tomes" edges to the Tome entity. +func (fuo *FileUpdateOne) AddTomes(t ...*Tome) *FileUpdateOne { + ids := make([]int, len(t)) + for i := range t { + ids[i] = t[i].ID + } + return fuo.AddTomeIDs(ids...) +} + // Mutation returns the FileMutation object of the builder. func (fuo *FileUpdateOne) Mutation() *FileMutation { return fuo.mutation } +// ClearTomes clears all "tomes" edges to the Tome entity. +func (fuo *FileUpdateOne) ClearTomes() *FileUpdateOne { + fuo.mutation.ClearTomes() + return fuo +} + +// RemoveTomeIDs removes the "tomes" edge to Tome entities by IDs. +func (fuo *FileUpdateOne) RemoveTomeIDs(ids ...int) *FileUpdateOne { + fuo.mutation.RemoveTomeIDs(ids...) + return fuo +} + +// RemoveTomes removes "tomes" edges to Tome entities. +func (fuo *FileUpdateOne) RemoveTomes(t ...*Tome) *FileUpdateOne { + ids := make([]int, len(t)) + for i := range t { + ids[i] = t[i].ID + } + return fuo.RemoveTomeIDs(ids...) +} + // Where appends a list predicates to the FileUpdate builder. func (fuo *FileUpdateOne) Where(ps ...predicate.File) *FileUpdateOne { fuo.mutation.Where(ps...) @@ -362,6 +480,51 @@ func (fuo *FileUpdateOne) sqlSave(ctx context.Context) (_node *File, err error) if value, ok := fuo.mutation.Content(); ok { _spec.SetField(file.FieldContent, field.TypeBytes, value) } + if fuo.mutation.TomesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := fuo.mutation.RemovedTomesIDs(); len(nodes) > 0 && !fuo.mutation.TomesCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := fuo.mutation.TomesIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2M, + Inverse: true, + Table: file.TomesTable, + Columns: file.TomesPrimaryKey, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(tome.FieldID, field.TypeInt), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _node = &File{config: fuo.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/tavern/internal/ent/gql_collection.go b/tavern/internal/ent/gql_collection.go index 6edfff931..b708cf6a4 100644 --- a/tavern/internal/ent/gql_collection.go +++ b/tavern/internal/ent/gql_collection.go @@ -175,6 +175,18 @@ func (f *FileQuery) collectField(ctx context.Context, opCtx *graphql.OperationCo ) for _, field := range graphql.CollectFields(opCtx, collected.Selections, satisfies) { switch field.Name { + case "tomes": + var ( + alias = field.Alias + path = append(path, alias) + query = (&TomeClient{config: f.config}).Query() + ) + if err := query.collectField(ctx, opCtx, field, path, satisfies...); err != nil { + return err + } + f.WithNamedTomes(alias, func(wq *TomeQuery) { + *wq = *query + }) case "createdAt": if _, ok := fieldSeen[file.FieldCreatedAt]; !ok { selectedFields = append(selectedFields, file.FieldCreatedAt) diff --git a/tavern/internal/ent/gql_edge.go b/tavern/internal/ent/gql_edge.go index 17e2203e0..baeeb612e 100644 --- a/tavern/internal/ent/gql_edge.go +++ b/tavern/internal/ent/gql_edge.go @@ -28,6 +28,18 @@ func (b *Beacon) Tasks(ctx context.Context) (result []*Task, err error) { return result, err } +func (f *File) Tomes(ctx context.Context) (result []*Tome, err error) { + if fc := graphql.GetFieldContext(ctx); fc != nil && fc.Field.Alias != "" { + result, err = f.NamedTomes(graphql.GetFieldContext(ctx).Field.Alias) + } else { + result, err = f.Edges.TomesOrErr() + } + if IsNotLoaded(err) { + result, err = f.QueryTomes().All(ctx) + } + return result, err +} + func (h *Host) Tags(ctx context.Context) (result []*Tag, err error) { if fc := graphql.GetFieldContext(ctx); fc != nil && fc.Field.Alias != "" { result, err = h.NamedTags(graphql.GetFieldContext(ctx).Field.Alias) diff --git a/tavern/internal/ent/gql_where_input.go b/tavern/internal/ent/gql_where_input.go index 35f291807..a960260f6 100644 --- a/tavern/internal/ent/gql_where_input.go +++ b/tavern/internal/ent/gql_where_input.go @@ -578,6 +578,10 @@ type FileWhereInput struct { HashHasSuffix *string `json:"hashHasSuffix,omitempty"` HashEqualFold *string `json:"hashEqualFold,omitempty"` HashContainsFold *string `json:"hashContainsFold,omitempty"` + + // "tomes" edge predicates. + HasTomes *bool `json:"hasTomes,omitempty"` + HasTomesWith []*TomeWhereInput `json:"hasTomesWith,omitempty"` } // AddPredicates adds custom predicates to the where input to be used during the filtering phase. @@ -826,6 +830,24 @@ func (i *FileWhereInput) P() (predicate.File, error) { predicates = append(predicates, file.HashContainsFold(*i.HashContainsFold)) } + if i.HasTomes != nil { + p := file.HasTomes() + if !*i.HasTomes { + p = file.Not(p) + } + predicates = append(predicates, p) + } + if len(i.HasTomesWith) > 0 { + with := make([]predicate.Tome, 0, len(i.HasTomesWith)) + for _, w := range i.HasTomesWith { + p, err := w.P() + if err != nil { + return nil, fmt.Errorf("%w: field 'HasTomesWith'", err) + } + with = append(with, p) + } + predicates = append(predicates, file.HasTomesWith(with...)) + } switch len(predicates) { case 0: return nil, ErrEmptyFileWhereInput diff --git a/tavern/internal/ent/migrate/schema.go b/tavern/internal/ent/migrate/schema.go index dbe7d4e38..e24e26468 100644 --- a/tavern/internal/ent/migrate/schema.go +++ b/tavern/internal/ent/migrate/schema.go @@ -42,21 +42,12 @@ var ( {Name: "size", Type: field.TypeInt, Default: 0}, {Name: "hash", Type: field.TypeString, Size: 100}, {Name: "content", Type: field.TypeBytes}, - {Name: "tome_files", Type: field.TypeInt, Nullable: true}, } // FilesTable holds the schema information for the "files" table. FilesTable = &schema.Table{ Name: "files", Columns: FilesColumns, PrimaryKey: []*schema.Column{FilesColumns[0]}, - ForeignKeys: []*schema.ForeignKey{ - { - Symbol: "files_tomes_files", - Columns: []*schema.Column{FilesColumns[7]}, - RefColumns: []*schema.Column{TomesColumns[0]}, - OnDelete: schema.SetNull, - }, - }, } // HostsColumns holds the columns for the "hosts" table. HostsColumns = []*schema.Column{ @@ -214,6 +205,31 @@ var ( }, }, } + // TomeFilesColumns holds the columns for the "tome_files" table. + TomeFilesColumns = []*schema.Column{ + {Name: "tome_id", Type: field.TypeInt}, + {Name: "file_id", Type: field.TypeInt}, + } + // TomeFilesTable holds the schema information for the "tome_files" table. + TomeFilesTable = &schema.Table{ + Name: "tome_files", + Columns: TomeFilesColumns, + PrimaryKey: []*schema.Column{TomeFilesColumns[0], TomeFilesColumns[1]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "tome_files_tome_id", + Columns: []*schema.Column{TomeFilesColumns[0]}, + RefColumns: []*schema.Column{TomesColumns[0]}, + OnDelete: schema.Cascade, + }, + { + Symbol: "tome_files_file_id", + Columns: []*schema.Column{TomeFilesColumns[1]}, + RefColumns: []*schema.Column{FilesColumns[0]}, + OnDelete: schema.Cascade, + }, + }, + } // Tables holds all the tables in the schema. Tables = []*schema.Table{ BeaconsTable, @@ -225,12 +241,12 @@ var ( TomesTable, UsersTable, HostTagsTable, + TomeFilesTable, } ) func init() { BeaconsTable.ForeignKeys[0].RefTable = HostsTable - FilesTable.ForeignKeys[0].RefTable = TomesTable QuestsTable.ForeignKeys[0].RefTable = TomesTable QuestsTable.ForeignKeys[1].RefTable = FilesTable QuestsTable.ForeignKeys[2].RefTable = UsersTable @@ -238,4 +254,6 @@ func init() { TasksTable.ForeignKeys[1].RefTable = BeaconsTable HostTagsTable.ForeignKeys[0].RefTable = HostsTable HostTagsTable.ForeignKeys[1].RefTable = TagsTable + TomeFilesTable.ForeignKeys[0].RefTable = TomesTable + TomeFilesTable.ForeignKeys[1].RefTable = FilesTable } diff --git a/tavern/internal/ent/mutation.go b/tavern/internal/ent/mutation.go index 6ddbfa74e..5b39b4e24 100644 --- a/tavern/internal/ent/mutation.go +++ b/tavern/internal/ent/mutation.go @@ -919,6 +919,9 @@ type FileMutation struct { hash *string content *[]byte clearedFields map[string]struct{} + tomes map[int]struct{} + removedtomes map[int]struct{} + clearedtomes bool done bool oldValue func(context.Context) (*File, error) predicates []predicate.File @@ -1258,6 +1261,60 @@ func (m *FileMutation) ResetContent() { m.content = nil } +// AddTomeIDs adds the "tomes" edge to the Tome entity by ids. +func (m *FileMutation) AddTomeIDs(ids ...int) { + if m.tomes == nil { + m.tomes = make(map[int]struct{}) + } + for i := range ids { + m.tomes[ids[i]] = struct{}{} + } +} + +// ClearTomes clears the "tomes" edge to the Tome entity. +func (m *FileMutation) ClearTomes() { + m.clearedtomes = true +} + +// TomesCleared reports if the "tomes" edge to the Tome entity was cleared. +func (m *FileMutation) TomesCleared() bool { + return m.clearedtomes +} + +// RemoveTomeIDs removes the "tomes" edge to the Tome entity by IDs. +func (m *FileMutation) RemoveTomeIDs(ids ...int) { + if m.removedtomes == nil { + m.removedtomes = make(map[int]struct{}) + } + for i := range ids { + delete(m.tomes, ids[i]) + m.removedtomes[ids[i]] = struct{}{} + } +} + +// RemovedTomes returns the removed IDs of the "tomes" edge to the Tome entity. +func (m *FileMutation) RemovedTomesIDs() (ids []int) { + for id := range m.removedtomes { + ids = append(ids, id) + } + return +} + +// TomesIDs returns the "tomes" edge IDs in the mutation. +func (m *FileMutation) TomesIDs() (ids []int) { + for id := range m.tomes { + ids = append(ids, id) + } + return +} + +// ResetTomes resets all changes to the "tomes" edge. +func (m *FileMutation) ResetTomes() { + m.tomes = nil + m.clearedtomes = false + m.removedtomes = nil +} + // Where appends a list predicates to the FileMutation builder. func (m *FileMutation) Where(ps ...predicate.File) { m.predicates = append(m.predicates, ps...) @@ -1491,49 +1548,85 @@ func (m *FileMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *FileMutation) AddedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.tomes != nil { + edges = append(edges, file.EdgeTomes) + } return edges } // AddedIDs returns all IDs (to other nodes) that were added for the given edge // name in this mutation. func (m *FileMutation) AddedIDs(name string) []ent.Value { + switch name { + case file.EdgeTomes: + ids := make([]ent.Value, 0, len(m.tomes)) + for id := range m.tomes { + ids = append(ids, id) + } + return ids + } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *FileMutation) RemovedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.removedtomes != nil { + edges = append(edges, file.EdgeTomes) + } return edges } // RemovedIDs returns all IDs (to other nodes) that were removed for the edge with // the given name in this mutation. func (m *FileMutation) RemovedIDs(name string) []ent.Value { + switch name { + case file.EdgeTomes: + ids := make([]ent.Value, 0, len(m.removedtomes)) + for id := range m.removedtomes { + ids = append(ids, id) + } + return ids + } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *FileMutation) ClearedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.clearedtomes { + edges = append(edges, file.EdgeTomes) + } return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. func (m *FileMutation) EdgeCleared(name string) bool { + switch name { + case file.EdgeTomes: + return m.clearedtomes + } return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. func (m *FileMutation) ClearEdge(name string) error { + switch name { + } return fmt.Errorf("unknown File unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. func (m *FileMutation) ResetEdge(name string) error { + switch name { + case file.EdgeTomes: + m.ResetTomes() + return nil + } return fmt.Errorf("unknown File edge %s", name) } diff --git a/tavern/internal/ent/schema/file.go b/tavern/internal/ent/schema/file.go index 4806e7d7c..60d21459d 100644 --- a/tavern/internal/ent/schema/file.go +++ b/tavern/internal/ent/schema/file.go @@ -10,6 +10,7 @@ import ( "entgo.io/contrib/entgql" "entgo.io/ent" "entgo.io/ent/schema" + "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" ) @@ -48,7 +49,10 @@ func (File) Fields() []ent.Field { // Edges of the File. func (File) Edges() []ent.Edge { - return []ent.Edge{} + return []ent.Edge{ + edge.From("tomes", Tome.Type). + Ref("files"), + } } // Annotations describes additional information for the ent. diff --git a/tavern/internal/ent/schema/file_test.go b/tavern/internal/ent/schema/file_test.go index 3a2226a13..7c8a3bf19 100644 --- a/tavern/internal/ent/schema/file_test.go +++ b/tavern/internal/ent/schema/file_test.go @@ -35,6 +35,28 @@ func TestFileHooks(t *testing.T) { assert.NotZero(t, testFile.LastModifiedAt) } +func TestMultipleTomes(t *testing.T) { + ctx := context.Background() + graph := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer graph.Close() + + files := []*ent.File{ + newFile(graph, "TestFile001", []byte("Test Content")), + } + graph.Tome.Create(). + SetName("TestTome001"). + SetEldritch(`print("hello world")`). + SetDescription("Hello World"). + AddFiles(files...). + SaveX(ctx) + graph.Tome.Create(). + SetName("TestTome002"). + SetEldritch(`print("hello world")`). + SetDescription("Hello World"). + AddFiles(files...). + SaveX(ctx) +} + // newFile is a helper to create files directly via ent func newFile(graph *ent.Client, name string, content []byte) *ent.File { return graph.File.Create(). diff --git a/tavern/internal/ent/tome/tome.go b/tavern/internal/ent/tome/tome.go index f81c9ef41..fb87a1186 100644 --- a/tavern/internal/ent/tome/tome.go +++ b/tavern/internal/ent/tome/tome.go @@ -33,13 +33,11 @@ const ( EdgeFiles = "files" // Table holds the table name of the tome in the database. Table = "tomes" - // FilesTable is the table that holds the files relation/edge. - FilesTable = "files" + // FilesTable is the table that holds the files relation/edge. The primary key declared below. + FilesTable = "tome_files" // FilesInverseTable is the table name for the File entity. // It exists in this package in order to avoid circular dependency with the "file" package. FilesInverseTable = "files" - // FilesColumn is the table column denoting the files relation/edge. - FilesColumn = "tome_files" ) // Columns holds all SQL columns for tome fields. @@ -54,6 +52,12 @@ var Columns = []string{ FieldEldritch, } +var ( + // FilesPrimaryKey and FilesColumn2 are the table columns denoting the + // primary key for the files relation (M2M). + FilesPrimaryKey = []string{"tome_id", "file_id"} +) + // ValidColumn reports if the column name is valid (part of the table columns). func ValidColumn(column string) bool { for i := range Columns { @@ -143,6 +147,6 @@ func newFilesStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), sqlgraph.To(FilesInverseTable, FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, FilesTable, FilesColumn), + sqlgraph.Edge(sqlgraph.M2M, false, FilesTable, FilesPrimaryKey...), ) } diff --git a/tavern/internal/ent/tome/where.go b/tavern/internal/ent/tome/where.go index ebfd8deff..91714ae88 100644 --- a/tavern/internal/ent/tome/where.go +++ b/tavern/internal/ent/tome/where.go @@ -510,7 +510,7 @@ func HasFiles() predicate.Tome { return predicate.Tome(func(s *sql.Selector) { step := sqlgraph.NewStep( sqlgraph.From(Table, FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, FilesTable, FilesColumn), + sqlgraph.Edge(sqlgraph.M2M, false, FilesTable, FilesPrimaryKey...), ) sqlgraph.HasNeighbors(s, step) }) diff --git a/tavern/internal/ent/tome_create.go b/tavern/internal/ent/tome_create.go index ce48f4cc0..5742431e4 100644 --- a/tavern/internal/ent/tome_create.go +++ b/tavern/internal/ent/tome_create.go @@ -245,10 +245,10 @@ func (tc *TomeCreate) createSpec() (*Tome, *sqlgraph.CreateSpec) { } if nodes := tc.mutation.FilesIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), diff --git a/tavern/internal/ent/tome_query.go b/tavern/internal/ent/tome_query.go index 03945c7fa..02c90e02c 100644 --- a/tavern/internal/ent/tome_query.go +++ b/tavern/internal/ent/tome_query.go @@ -77,7 +77,7 @@ func (tq *TomeQuery) QueryFiles() *FileQuery { step := sqlgraph.NewStep( sqlgraph.From(tome.Table, tome.FieldID, selector), sqlgraph.To(file.Table, file.FieldID), - sqlgraph.Edge(sqlgraph.O2M, false, tome.FilesTable, tome.FilesColumn), + sqlgraph.Edge(sqlgraph.M2M, false, tome.FilesTable, tome.FilesPrimaryKey...), ) fromU = sqlgraph.SetNeighbors(tq.driver.Dialect(), step) return fromU, nil @@ -421,33 +421,63 @@ func (tq *TomeQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Tome, e } func (tq *TomeQuery) loadFiles(ctx context.Context, query *FileQuery, nodes []*Tome, init func(*Tome), assign func(*Tome, *File)) error { - fks := make([]driver.Value, 0, len(nodes)) - nodeids := make(map[int]*Tome) - for i := range nodes { - fks = append(fks, nodes[i].ID) - nodeids[nodes[i].ID] = nodes[i] + edgeIDs := make([]driver.Value, len(nodes)) + byID := make(map[int]*Tome) + nids := make(map[int]map[*Tome]struct{}) + for i, node := range nodes { + edgeIDs[i] = node.ID + byID[node.ID] = node if init != nil { - init(nodes[i]) + init(node) } } - query.withFKs = true - query.Where(predicate.File(func(s *sql.Selector) { - s.Where(sql.InValues(s.C(tome.FilesColumn), fks...)) - })) - neighbors, err := query.All(ctx) + query.Where(func(s *sql.Selector) { + joinT := sql.Table(tome.FilesTable) + s.Join(joinT).On(s.C(file.FieldID), joinT.C(tome.FilesPrimaryKey[1])) + s.Where(sql.InValues(joinT.C(tome.FilesPrimaryKey[0]), edgeIDs...)) + columns := s.SelectedColumns() + s.Select(joinT.C(tome.FilesPrimaryKey[0])) + s.AppendSelect(columns...) + s.SetDistinct(false) + }) + if err := query.prepareQuery(ctx); err != nil { + return err + } + qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) { + return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) { + assign := spec.Assign + values := spec.ScanValues + spec.ScanValues = func(columns []string) ([]any, error) { + values, err := values(columns[1:]) + if err != nil { + return nil, err + } + return append([]any{new(sql.NullInt64)}, values...), nil + } + spec.Assign = func(columns []string, values []any) error { + outValue := int(values[0].(*sql.NullInt64).Int64) + inValue := int(values[1].(*sql.NullInt64).Int64) + if nids[inValue] == nil { + nids[inValue] = map[*Tome]struct{}{byID[outValue]: {}} + return assign(columns[1:], values[1:]) + } + nids[inValue][byID[outValue]] = struct{}{} + return nil + } + }) + }) + neighbors, err := withInterceptors[[]*File](ctx, query, qr, query.inters) if err != nil { return err } for _, n := range neighbors { - fk := n.tome_files - if fk == nil { - return fmt.Errorf(`foreign-key "tome_files" is nil for node %v`, n.ID) - } - node, ok := nodeids[*fk] + nodes, ok := nids[n.ID] if !ok { - return fmt.Errorf(`unexpected referenced foreign-key "tome_files" returned %v for node %v`, *fk, n.ID) + return fmt.Errorf(`unexpected "files" node returned %v`, n.ID) + } + for kn := range nodes { + assign(kn, n) } - assign(node, n) } return nil } diff --git a/tavern/internal/ent/tome_update.go b/tavern/internal/ent/tome_update.go index 2319a170a..ebaa87d93 100644 --- a/tavern/internal/ent/tome_update.go +++ b/tavern/internal/ent/tome_update.go @@ -212,10 +212,10 @@ func (tu *TomeUpdate) sqlSave(ctx context.Context) (n int, err error) { } if tu.mutation.FilesCleared() { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), @@ -225,10 +225,10 @@ func (tu *TomeUpdate) sqlSave(ctx context.Context) (n int, err error) { } if nodes := tu.mutation.RemovedFilesIDs(); len(nodes) > 0 && !tu.mutation.FilesCleared() { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), @@ -241,10 +241,10 @@ func (tu *TomeUpdate) sqlSave(ctx context.Context) (n int, err error) { } if nodes := tu.mutation.FilesIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), @@ -488,10 +488,10 @@ func (tuo *TomeUpdateOne) sqlSave(ctx context.Context) (_node *Tome, err error) } if tuo.mutation.FilesCleared() { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), @@ -501,10 +501,10 @@ func (tuo *TomeUpdateOne) sqlSave(ctx context.Context) (_node *Tome, err error) } if nodes := tuo.mutation.RemovedFilesIDs(); len(nodes) > 0 && !tuo.mutation.FilesCleared() { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), @@ -517,10 +517,10 @@ func (tuo *TomeUpdateOne) sqlSave(ctx context.Context) (_node *Tome, err error) } if nodes := tuo.mutation.FilesIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ - Rel: sqlgraph.O2M, + Rel: sqlgraph.M2M, Inverse: false, Table: tome.FilesTable, - Columns: []string{tome.FilesColumn}, + Columns: tome.FilesPrimaryKey, Bidi: false, Target: &sqlgraph.EdgeTarget{ IDSpec: sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt), diff --git a/tavern/internal/graphql/generated/ent.generated.go b/tavern/internal/graphql/generated/ent.generated.go index 5e86aa4bf..742d45809 100644 --- a/tavern/internal/graphql/generated/ent.generated.go +++ b/tavern/internal/graphql/generated/ent.generated.go @@ -945,6 +945,65 @@ func (ec *executionContext) fieldContext_File_hash(ctx context.Context, field gr return fc, nil } +func (ec *executionContext) _File_tomes(ctx context.Context, field graphql.CollectedField, obj *ent.File) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_File_tomes(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Tomes(ctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*ent.Tome) + fc.Result = res + return ec.marshalOTome2ᚕᚖrealmᚗpubᚋtavernᚋinternalᚋentᚐTomeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_File_tomes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "File", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Tome_id(ctx, field) + case "createdAt": + return ec.fieldContext_Tome_createdAt(ctx, field) + case "lastModifiedAt": + return ec.fieldContext_Tome_lastModifiedAt(ctx, field) + case "name": + return ec.fieldContext_Tome_name(ctx, field) + case "description": + return ec.fieldContext_Tome_description(ctx, field) + case "paramDefs": + return ec.fieldContext_Tome_paramDefs(ctx, field) + case "eldritch": + return ec.fieldContext_Tome_eldritch(ctx, field) + case "files": + return ec.fieldContext_Tome_files(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Tome", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Host_id(ctx context.Context, field graphql.CollectedField, obj *ent.Host) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Host_id(ctx, field) if err != nil { @@ -1664,6 +1723,8 @@ func (ec *executionContext) fieldContext_Query_files(ctx context.Context, field return ec.fieldContext_File_size(ctx, field) case "hash": return ec.fieldContext_File_hash(ctx, field) + case "tomes": + return ec.fieldContext_File_tomes(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type File", field.Name) }, @@ -2853,6 +2914,8 @@ func (ec *executionContext) fieldContext_Quest_bundle(ctx context.Context, field return ec.fieldContext_File_size(ctx, field) case "hash": return ec.fieldContext_File_hash(ctx, field) + case "tomes": + return ec.fieldContext_File_tomes(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type File", field.Name) }, @@ -4285,6 +4348,8 @@ func (ec *executionContext) fieldContext_Tome_files(ctx context.Context, field g return ec.fieldContext_File_size(ctx, field) case "hash": return ec.fieldContext_File_hash(ctx, field) + case "tomes": + return ec.fieldContext_File_tomes(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type File", field.Name) }, @@ -5605,7 +5670,7 @@ func (ec *executionContext) unmarshalInputFileWhereInput(ctx context.Context, ob asMap[k] = v } - fieldsInOrder := [...]string{"not", "and", "or", "id", "idNEQ", "idIn", "idNotIn", "idGT", "idGTE", "idLT", "idLTE", "createdAt", "createdAtNEQ", "createdAtIn", "createdAtNotIn", "createdAtGT", "createdAtGTE", "createdAtLT", "createdAtLTE", "lastModifiedAt", "lastModifiedAtNEQ", "lastModifiedAtIn", "lastModifiedAtNotIn", "lastModifiedAtGT", "lastModifiedAtGTE", "lastModifiedAtLT", "lastModifiedAtLTE", "name", "nameNEQ", "nameIn", "nameNotIn", "nameGT", "nameGTE", "nameLT", "nameLTE", "nameContains", "nameHasPrefix", "nameHasSuffix", "nameEqualFold", "nameContainsFold", "size", "sizeNEQ", "sizeIn", "sizeNotIn", "sizeGT", "sizeGTE", "sizeLT", "sizeLTE", "hash", "hashNEQ", "hashIn", "hashNotIn", "hashGT", "hashGTE", "hashLT", "hashLTE", "hashContains", "hashHasPrefix", "hashHasSuffix", "hashEqualFold", "hashContainsFold"} + fieldsInOrder := [...]string{"not", "and", "or", "id", "idNEQ", "idIn", "idNotIn", "idGT", "idGTE", "idLT", "idLTE", "createdAt", "createdAtNEQ", "createdAtIn", "createdAtNotIn", "createdAtGT", "createdAtGTE", "createdAtLT", "createdAtLTE", "lastModifiedAt", "lastModifiedAtNEQ", "lastModifiedAtIn", "lastModifiedAtNotIn", "lastModifiedAtGT", "lastModifiedAtGTE", "lastModifiedAtLT", "lastModifiedAtLTE", "name", "nameNEQ", "nameIn", "nameNotIn", "nameGT", "nameGTE", "nameLT", "nameLTE", "nameContains", "nameHasPrefix", "nameHasSuffix", "nameEqualFold", "nameContainsFold", "size", "sizeNEQ", "sizeIn", "sizeNotIn", "sizeGT", "sizeGTE", "sizeLT", "sizeLTE", "hash", "hashNEQ", "hashIn", "hashNotIn", "hashGT", "hashGTE", "hashLT", "hashLTE", "hashContains", "hashHasPrefix", "hashHasSuffix", "hashEqualFold", "hashContainsFold", "hasTomes", "hasTomesWith"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -6161,6 +6226,24 @@ func (ec *executionContext) unmarshalInputFileWhereInput(ctx context.Context, ob return it, err } it.HashContainsFold = data + case "hasTomes": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hasTomes")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.HasTomes = data + case "hasTomesWith": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hasTomesWith")) + data, err := ec.unmarshalOTomeWhereInput2ᚕᚖrealmᚗpubᚋtavernᚋinternalᚋentᚐTomeWhereInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.HasTomesWith = data } } @@ -10428,33 +10511,66 @@ func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj case "id": out.Values[i] = ec._File_id(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "createdAt": out.Values[i] = ec._File_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "lastModifiedAt": out.Values[i] = ec._File_lastModifiedAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "name": out.Values[i] = ec._File_name(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "size": out.Values[i] = ec._File_size(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "hash": out.Values[i] = ec._File_hash(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } + case "tomes": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._File_tomes(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -13015,6 +13131,53 @@ func (ec *executionContext) unmarshalOTaskWhereInput2ᚖrealmᚗpubᚋtavernᚋi return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalOTome2ᚕᚖrealmᚗpubᚋtavernᚋinternalᚋentᚐTomeᚄ(ctx context.Context, sel ast.SelectionSet, v []*ent.Tome) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNTome2ᚖrealmᚗpubᚋtavernᚋinternalᚋentᚐTome(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) unmarshalOTomeWhereInput2ᚕᚖrealmᚗpubᚋtavernᚋinternalᚋentᚐTomeWhereInputᚄ(ctx context.Context, v interface{}) ([]*ent.TomeWhereInput, error) { if v == nil { return nil, nil diff --git a/tavern/internal/graphql/generated/root_.generated.go b/tavern/internal/graphql/generated/root_.generated.go index 534c2918b..a27fe9e9f 100644 --- a/tavern/internal/graphql/generated/root_.generated.go +++ b/tavern/internal/graphql/generated/root_.generated.go @@ -63,6 +63,7 @@ type ComplexityRoot struct { LastModifiedAt func(childComplexity int) int Name func(childComplexity int) int Size func(childComplexity int) int + Tomes func(childComplexity int) int } Host struct { @@ -297,6 +298,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.File.Size(childComplexity), true + case "File.tomes": + if e.complexity.File.Tomes == nil { + break + } + + return e.complexity.File.Tomes(childComplexity), true + case "Host.beacons": if e.complexity.Host.Beacons == nil { break @@ -1238,6 +1246,7 @@ type File implements Node { size: Int! """A SHA3 digest of the content field""" hash: String! + tomes: [Tome!] } """Ordering options for File connections""" input FileOrder { @@ -1325,6 +1334,9 @@ input FileWhereInput { hashHasSuffix: String hashEqualFold: String hashContainsFold: String + """tomes edge predicates""" + hasTomes: Boolean + hasTomesWith: [TomeWhereInput!] } type Host implements Node { id: ID! diff --git a/tavern/internal/graphql/schema.graphql b/tavern/internal/graphql/schema.graphql index 654f0b661..26f3e5769 100644 --- a/tavern/internal/graphql/schema.graphql +++ b/tavern/internal/graphql/schema.graphql @@ -196,6 +196,7 @@ type File implements Node { size: Int! """A SHA3 digest of the content field""" hash: String! + tomes: [Tome!] } """Ordering options for File connections""" input FileOrder { @@ -283,6 +284,9 @@ input FileWhereInput { hashHasSuffix: String hashEqualFold: String hashContainsFold: String + """tomes edge predicates""" + hasTomes: Boolean + hasTomesWith: [TomeWhereInput!] } type Host implements Node { id: ID! diff --git a/tavern/internal/graphql/schema/ent.graphql b/tavern/internal/graphql/schema/ent.graphql index f049210fb..e2e8e3fe5 100644 --- a/tavern/internal/graphql/schema/ent.graphql +++ b/tavern/internal/graphql/schema/ent.graphql @@ -191,6 +191,7 @@ type File implements Node { size: Int! """A SHA3 digest of the content field""" hash: String! + tomes: [Tome!] } """Ordering options for File connections""" input FileOrder { @@ -278,6 +279,9 @@ input FileWhereInput { hashHasSuffix: String hashEqualFold: String hashContainsFold: String + """tomes edge predicates""" + hasTomes: Boolean + hasTomesWith: [TomeWhereInput!] } type Host implements Node { id: ID! diff --git a/tavern/internal/graphql/testdata/mutations/createQuest/WithFiles.yml b/tavern/internal/graphql/testdata/mutations/createQuest/WithFiles.yml index 1469b7ea4..bc3be0772 100644 --- a/tavern/internal/graphql/testdata/mutations/createQuest/WithFiles.yml +++ b/tavern/internal/graphql/testdata/mutations/createQuest/WithFiles.yml @@ -7,11 +7,14 @@ state: | VALUES (1337,"delightful-lich","ABCDEFG-123456",1010); INSERT INTO `tomes` (id, name, description, eldritch, hash, created_at, last_modified_at) VALUES (2000,"Test Tome","Used in a unit test :D", "print('Hello World!')", "abcdefg", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); - INSERT INTO `files` (id, tome_files, name, content, hash, created_at, last_modified_at) - VALUES (3000,2000,"TestFile1", "hello world", "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); - INSERT INTO `files` (id, tome_files, name, content, hash, created_at, last_modified_at) - VALUES (3001,2000,"TestFile2", "some test", "a9d9e8df0488c7e7e9236e43fe0c9385d7ea6920700db55d305f55dca76ddb0b", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); - + INSERT INTO `files` (id, name, content, hash, created_at, last_modified_at) + VALUES (3000, "TestFile1", "hello world", "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `files` (id, name, content, hash, created_at, last_modified_at) + VALUES (3001, "TestFile2", "some test", "a9d9e8df0488c7e7e9236e43fe0c9385d7ea6920700db55d305f55dca76ddb0b", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `tome_files` (tome_id, file_id) + VALUES (2000, 3000); + INSERT INTO `tome_files` (tome_id, file_id) + VALUES (2000, 3001); requestor: session_token: secretToken query: | diff --git a/tavern/internal/www/schema.graphql b/tavern/internal/www/schema.graphql index 654f0b661..26f3e5769 100644 --- a/tavern/internal/www/schema.graphql +++ b/tavern/internal/www/schema.graphql @@ -196,6 +196,7 @@ type File implements Node { size: Int! """A SHA3 digest of the content field""" hash: String! + tomes: [Tome!] } """Ordering options for File connections""" input FileOrder { @@ -283,6 +284,9 @@ input FileWhereInput { hashHasSuffix: String hashEqualFold: String hashContainsFold: String + """tomes edge predicates""" + hasTomes: Boolean + hasTomesWith: [TomeWhereInput!] } type Host implements Node { id: ID!