Skip to content

Commit

Permalink
implement property schema and mutations APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
Southclaws committed Feb 7, 2025
1 parent 6ef9ba0 commit 0f227d7
Show file tree
Hide file tree
Showing 26 changed files with 1,483 additions and 522 deletions.
43 changes: 34 additions & 9 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1634,14 +1634,18 @@ paths:
/nodes/{node_slug}/properties:
patch:
operationId: NodeUpdateProperties
description: Update the properties of a node.
description: |
Update the properties of a node. New schema fields will result in the
schema of the node being updated before values are assigned. This will
also propagate to all sibling nodes as they all share the same schema.
tags: [nodes]
parameters: [$ref: "#/components/parameters/NodeSlugParam"]
requestBody: {$ref: "#/components/requestBodies/NodeUpdateProperties"}
responses:
default: {$ref: "#/components/responses/InternalServerError"}
"404": {$ref: "#/components/responses/NotFound"}
"401": {$ref: "#/components/responses/Unauthorised"}
"400": {$ref: "#/components/responses/BadRequest"}
"200": {$ref: "#/components/responses/NodeUpdatePropertiesOK"}

/nodes/{node_slug}/visibility:
Expand Down Expand Up @@ -3015,6 +3019,7 @@ components:
application/json:
schema:
type: object
required: [properties]
properties:
properties:
$ref: "#/components/schemas/PropertyList"
Expand Down Expand Up @@ -5247,14 +5252,15 @@ components:
description: |
Note: Properties are replace-all and are not merged with existing.
type: object
required: [properties]
properties:
properties: {$ref: "#/components/schemas/PropertyList"}
properties: {$ref: "#/components/schemas/PropertyMutationList"}

Property:
type: object
required: [id, name, type, sort]
required: [fid, name, type, sort]
properties:
id: {$ref: "#/components/schemas/Identifier"}
fid: {$ref: "#/components/schemas/Identifier"}
name: {$ref: "#/components/schemas/PropertyName"}
type: {$ref: "#/components/schemas/PropertyType"}
value: {$ref: "#/components/schemas/PropertyValue"}
Expand All @@ -5270,16 +5276,35 @@ components:

PropertyType:
type: string
# TODO: Enumerate this type.

PropertyValue:
type: string
# TODO: Enumerate this type.

PropertySortKey:
type: string

PropertyMutation:
description: |
A property mutation is a change to a property on a node. It can be used
to update existing properties or add new properties to a node. When a
property already exists by name, the type and sort columns are optional.
type: object
required: [name, value]
properties:
name: {$ref: "#/components/schemas/PropertyName"}
value: {$ref: "#/components/schemas/PropertyValue"}
type: {$ref: "#/components/schemas/PropertyType"}
sort: {$ref: "#/components/schemas/PropertySortKey"}

PropertyMutationList:
type: array
items: {$ref: "#/components/schemas/PropertyMutation"}

PropertySchema:
type: object
required: [id, name, type, sort]
required: [fid, name, type, sort]
properties:
id: {$ref: "#/components/schemas/Identifier"}
fid: {$ref: "#/components/schemas/Identifier"}
name: {$ref: "#/components/schemas/PropertyName"}
type: {$ref: "#/components/schemas/PropertyType"}
sort:
Expand All @@ -5300,7 +5325,7 @@ components:
type: object
required: [name, type, sort]
properties:
id: {$ref: "#/components/schemas/Identifier"}
fid: {$ref: "#/components/schemas/Identifier"}
name: {$ref: "#/components/schemas/PropertyName"}
type: {$ref: "#/components/schemas/PropertyType"}
sort:
Expand Down
12 changes: 7 additions & 5 deletions app/resources/library/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,14 @@ func MapNode(isRoot bool, ps *PropertySchemaTable) func(c *ent.Node) (*Node, err
Metadata: c.Metadata,
}

// Sibling properties may contain values, so we pass in the edge.
n.Properties = ps.BuildPropertyTable(c.Edges.Properties, isRoot)
if ps != nil {
// Sibling properties may contain values, so we pass in the edge.
n.Properties = opt.NewPtr(ps.BuildPropertyTable(c.Edges.Properties, isRoot))

if isRoot {
// Child properties don't contain values, only the property schemas.
n.ChildProperties = ps.ChildSchemas()
if isRoot {
// Child properties don't contain values, only the property schemas.
n.ChildProperties = opt.NewPtr(ps.ChildSchemas())
}
}

return n, nil
Expand Down
4 changes: 2 additions & 2 deletions app/resources/library/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ type Node struct {
PrimaryImage opt.Optional[asset.Asset]
Owner profile.Public
Parent opt.Optional[Node]
Properties PropertyTable
ChildProperties PropertySchemas
Properties opt.Optional[PropertyTable]
ChildProperties opt.Optional[PropertySchema]
Tags tag_ref.Tags
Collections collection_item_status.Status // NOTE: Not done yet
Visibility visibility.Visibility
Expand Down
58 changes: 58 additions & 0 deletions app/resources/library/node_properties/properties_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package node_properties

import (
"context"

"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fctx"
"github.com/Southclaws/opt"
"github.com/Southclaws/storyden/app/resources/library"
"github.com/Southclaws/storyden/internal/ent"
"github.com/Southclaws/storyden/internal/ent/property"
"github.com/rs/xid"
)

type Writer struct {
db *ent.Client
}

func (w *Writer) Update(ctx context.Context, nid library.NodeID, schema library.PropertySchema, props library.ExistingPropertyMutations) (*library.PropertyTable, error) {
tx, err := w.db.Debug().Tx(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

defer func() {
err = tx.Rollback()
}()

updated := library.PropertyTable{
Schema: schema,
}

for _, prop := range props {
create := tx.Property.Create().
SetValue(prop.Value).
SetFieldID(prop.ID).
SetNodeID(xid.ID(nid))

create.OnConflictColumns(property.FieldFieldID, property.FieldNodeID).UpdateNewValues()

r, err := create.Save(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

updated.Properties = append(updated.Properties, &library.Property{
Field: prop.PropertySchemaField,
Value: opt.New(r.Value),
})
}

err = tx.Commit()
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

return &updated, nil
}
109 changes: 81 additions & 28 deletions app/resources/library/node_properties/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,53 @@ import (
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fctx"
"github.com/Southclaws/opt"
"github.com/rs/xid"
"github.com/samber/lo"

"github.com/Southclaws/storyden/app/resources/library"
"github.com/Southclaws/storyden/internal/ent"
"github.com/Southclaws/storyden/internal/ent/node"
"github.com/Southclaws/storyden/internal/ent/propertyschemafield"
"github.com/rs/xid"
"github.com/samber/lo"
)

type SchemaWriter struct {
db *ent.Client
}

func New(db *ent.Client) *SchemaWriter {
func New(db *ent.Client) (*SchemaWriter, *Writer) {
return &SchemaWriter{
db: db,
}
db: db,
}, &Writer{
db: db,
}
}

type SchemaMutation struct {
type SchemaFieldMutation struct {
ID opt.Optional[xid.ID]
Name string
Type string
Sort string
}

type SchemaMutations []*SchemaMutation
type FieldSchemaMutations []*SchemaFieldMutation

func (w SchemaWriter) CreateForNode(ctx context.Context, nodeID library.NodeID, schemas FieldSchemaMutations) (*library.PropertySchema, error) {
node, err := w.db.Node.Get(ctx, xid.ID(nodeID))
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

schemaID, err := w.doSchemaUpdates(ctx, node.Edges.PropertySchemas, schemas, node)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

func (w *SchemaWriter) UpdateChildren(ctx context.Context, qk library.QueryKey, schemas SchemaMutations) (library.PropertySchemas, error) {
return &library.PropertySchema{
ID: *schemaID,
}, nil
}

func (w *SchemaWriter) UpdateChildren(ctx context.Context, qk library.QueryKey, schemas FieldSchemaMutations) (*library.PropertySchema, error) {
parent, err := w.db.Node.Query().Where(qk.Predicate()).WithNodes(func(nq *ent.NodeQuery) {
nq.WithPropertySchemas(func(psq *ent.PropertySchemaQuery) {
psq.WithFields()
Expand All @@ -46,8 +65,8 @@ func (w *SchemaWriter) UpdateChildren(ctx context.Context, qk library.QueryKey,

children := parent.Edges.Nodes
if len(children) == 0 {
// no children to update, no-op.w
return library.PropertySchemas{}, nil
// no children to update, no-op.
return &library.PropertySchema{}, nil
}

grouping := lo.GroupBy(children, func(n *ent.Node) string {
Expand All @@ -61,8 +80,57 @@ func (w *SchemaWriter) UpdateChildren(ctx context.Context, qk library.QueryKey,

currentSchema := children[0].Edges.PropertySchemas

creates := SchemaMutations{}
updates := SchemaMutations{}
schemaID, err := w.doSchemaUpdates(ctx, currentSchema, schemas, children...)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

// Mutations finished, query the final result for returning.

return w.Get(ctx, *schemaID)
}

func (w *SchemaWriter) Get(ctx context.Context, schemaID xid.ID) (*library.PropertySchema, error) {
schemaFields, err := w.db.PropertySchemaField.Query().
Where(propertyschemafield.SchemaID(schemaID)).
Order(ent.Asc(propertyschemafield.FieldSort)).
All(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

updatedSchemas := dt.Map(schemaFields, func(f *ent.PropertySchemaField) *library.PropertySchemaField {
return &library.PropertySchemaField{
ID: f.ID,
Name: f.Name,
Type: f.Type,
Sort: f.Sort,
}
})

return &library.PropertySchema{
ID: schemaID,
Fields: updatedSchemas,
}, nil
}

func (w *SchemaWriter) AddFields(ctx context.Context, schemaID xid.ID, schemas FieldSchemaMutations) (*library.PropertySchema, error) {
fields := []*ent.PropertySchemaFieldCreate{}
for _, s := range schemas {
fields = append(fields, w.db.PropertySchemaField.Create().SetName(s.Name).SetSort(s.Sort).SetType(s.Type).SetSchemaID(schemaID))
}

err := w.db.PropertySchemaField.CreateBulk(fields...).Exec(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

return w.Get(ctx, schemaID)
}

func (w *SchemaWriter) doSchemaUpdates(ctx context.Context, currentSchema *ent.PropertySchema, schemas FieldSchemaMutations, children ...*ent.Node) (*xid.ID, error) {
creates := FieldSchemaMutations{}
updates := FieldSchemaMutations{}
deletes := map[xid.ID]*ent.PropertySchemaField{}

if currentSchema != nil {
Expand Down Expand Up @@ -142,20 +210,5 @@ func (w *SchemaWriter) UpdateChildren(ctx context.Context, qk library.QueryKey,
return nil, fault.Wrap(err, fctx.With(ctx))
}

// Mutations finished, query the final result for returning.

schemaFields, err := w.db.PropertySchemaField.Query().Where(propertyschemafield.SchemaID(currentSchema.ID)).All(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

updatedSchemas := dt.Map(schemaFields, func(f *ent.PropertySchemaField) *library.PropertySchema {
return &library.PropertySchema{
Name: f.Name,
Type: f.Type,
Sort: f.Sort,
}
})

return updatedSchemas, nil
return &currentSchema.ID, nil
}
Loading

0 comments on commit 0f227d7

Please sign in to comment.