Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions db/.gen/imgdd/public/model/image_parent_table.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions db/.gen/imgdd/public/table/image_parent_table.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions db/.gen/imgdd/public/table/table_use_schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions db/migrations/000004_add_image_lineage_indexes.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BEGIN;
DROP INDEX IF EXISTS image_table_root_id_idx;
DROP INDEX IF EXISTS image_table_parent_id_idx;
COMMIT;
4 changes: 4 additions & 0 deletions db/migrations/000004_add_image_lineage_indexes.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BEGIN;
CREATE INDEX image_table_root_id_idx ON image_table(root_id);
CREATE INDEX image_table_parent_id_idx ON image_table(parent_id);
COMMIT;
1 change: 1 addition & 0 deletions db/migrations/000005_create_image_parent_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS image_parent_table;
39 changes: 39 additions & 0 deletions db/migrations/000005_create_image_parent_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
BEGIN;

CREATE TABLE image_parent_table (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
image_id UUID NOT NULL REFERENCES image_table(id) ON DELETE RESTRICT,
parent_image_id UUID NOT NULL REFERENCES image_table(id) ON DELETE RESTRICT,
relationship_type CHARACTER VARYING(50) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),

CONSTRAINT image_parent_unique_relationship UNIQUE(image_id, parent_image_id, relationship_type)
);

CREATE INDEX image_parent_table_image_id_idx ON image_parent_table(image_id);
CREATE INDEX image_parent_table_parent_image_id_idx ON image_parent_table(parent_image_id);

-- Backfill from existing parent_id column
INSERT INTO image_parent_table (image_id, parent_image_id, relationship_type)
SELECT id, parent_id, 'base'
FROM image_table
WHERE parent_id IS NOT NULL AND deleted_at IS NULL;

-- Backfill overlay relationships from changes JSON
INSERT INTO image_parent_table (image_id, parent_image_id, relationship_type)
SELECT
it.id,
(it.changes->'params'->>'overlay_image_id')::UUID,
'overlay'
FROM image_table it
WHERE it.changes != '{}'
AND it.changes->>'type' = 'watermark'
AND it.changes->'params'->>'overlay_image_id' IS NOT NULL
AND it.deleted_at IS NULL
AND EXISTS (
SELECT 1 FROM image_table ot
WHERE ot.id = (it.changes->'params'->>'overlay_image_id')::UUID
AND ot.deleted_at IS NULL
);

COMMIT;
1 change: 1 addition & 0 deletions domainmodels/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Image struct {
Identifier string
RootId string
ParentId string
Changes string
UploaderIP string
MIMEType string
NominalWidth int32
Expand Down
70 changes: 70 additions & 0 deletions editing/changeset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package editing

import (
"encoding/json"
"fmt"
)

type ChangeSet struct {
Type string `json:"type"`
Params json.RawMessage `json:"params"`
}

// FetchImageFunc retrieves image bytes by image ID.
type FetchImageFunc func(id string) ([]byte, error)

// Editor applies a ChangeSet to base image bytes, producing new image bytes.
type Editor interface {
Apply(base []byte, cs ChangeSet, fetchImage FetchImageFunc) ([]byte, string, error)
}

// ApplyResult holds the output of applying a ChangeSet.
type ApplyResult struct {
Bytes []byte
MIMEType string
ChangesJSON []byte
}

// ApplyChangeSet orchestrates applying a change set: looks up the editor,
// fetches the base image bytes, applies the edit, and serializes the changes.
func ApplyChangeSet(cs ChangeSet, baseImageId string, fetchImage FetchImageFunc) (*ApplyResult, error) {
editor, err := GetEditor(cs.Type)
if err != nil {
return nil, err
}

baseBytes, err := fetchImage(baseImageId)
if err != nil {
return nil, fmt.Errorf("failed to fetch base image: %w", err)
}

resultBytes, resultMime, err := editor.Apply(baseBytes, cs, fetchImage)
if err != nil {
return nil, fmt.Errorf("failed to apply %s: %w", cs.Type, err)
}

changesJSON, err := json.Marshal(cs)
if err != nil {
return nil, fmt.Errorf("failed to serialize changes: %w", err)
}

return &ApplyResult{
Bytes: resultBytes,
MIMEType: resultMime,
ChangesJSON: changesJSON,
}, nil
}

var registry = map[string]Editor{}

func Register(changeType string, editor Editor) {
registry[changeType] = editor
}

func GetEditor(changeType string) (Editor, error) {
e, ok := registry[changeType]
if !ok {
return nil, fmt.Errorf("unknown change type: %s", changeType)
}
return e, nil
}
66 changes: 66 additions & 0 deletions editing/changeset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package editing

import (
"encoding/json"
"testing"
)

func TestChangeSetSerialize(t *testing.T) {
params := WatermarkParams{
OverlayImageID: "abc-123",
Position: WatermarkPosition{X: 0.9, Y: 0.9},
Anchor: AnchorBottomRight,
Opacity: 0.5,
Scale: 0.15,
}
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Fatal(err)
}

cs := ChangeSet{
Type: "watermark",
Params: paramsJSON,
}

data, err := json.Marshal(cs)
if err != nil {
t.Fatal(err)
}

var decoded ChangeSet
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if decoded.Type != "watermark" {
t.Fatalf("expected type 'watermark', got '%s'", decoded.Type)
}

var decodedParams WatermarkParams
if err := json.Unmarshal(decoded.Params, &decodedParams); err != nil {
t.Fatal(err)
}
if decodedParams.OverlayImageID != "abc-123" {
t.Fatalf("expected overlay_image_id 'abc-123', got '%s'", decodedParams.OverlayImageID)
}
if decodedParams.Opacity != 0.5 {
t.Fatalf("expected opacity 0.5, got %f", decodedParams.Opacity)
}
}

func TestRegistryGetEditor(t *testing.T) {
editor, err := GetEditor("watermark")
if err != nil {
t.Fatal(err)
}
if editor == nil {
t.Fatal("expected non-nil editor for 'watermark'")
}
}

func TestRegistryGetUnknownEditor(t *testing.T) {
_, err := GetEditor("nonexistent")
if err == nil {
t.Fatal("expected error for unknown editor type")
}
}
Loading