feat: use postgres
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package mailstore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"galaxy/mail/internal/domain/common"
|
||||
"galaxy/mail/internal/service/acceptgenericdelivery"
|
||||
)
|
||||
|
||||
// attachmentRow stores the on-disk JSONB encoding of one
|
||||
// `common.AttachmentMetadata` entry. The encoding is intentionally explicit
|
||||
// (named JSON keys) so the on-disk shape stays decoupled from accidental Go
|
||||
// struct renames.
|
||||
type attachmentRow struct {
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// marshalAttachments returns the JSONB bytes for the attachments column. A
|
||||
// nil/empty slice round-trips as `[]` to keep the column NOT NULL across
|
||||
// equality tests.
|
||||
func marshalAttachments(attachments []common.AttachmentMetadata) ([]byte, error) {
|
||||
rows := make([]attachmentRow, 0, len(attachments))
|
||||
for _, attachment := range attachments {
|
||||
rows = append(rows, attachmentRow{
|
||||
Filename: attachment.Filename,
|
||||
ContentType: attachment.ContentType,
|
||||
SizeBytes: attachment.SizeBytes,
|
||||
})
|
||||
}
|
||||
payload, err := json.Marshal(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal attachments: %w", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// unmarshalAttachments decodes the attachments JSONB column into a
|
||||
// domain-friendly slice. nil/empty payloads decode to a nil slice.
|
||||
func unmarshalAttachments(payload []byte) ([]common.AttachmentMetadata, error) {
|
||||
if len(payload) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var rows []attachmentRow
|
||||
if err := json.Unmarshal(payload, &rows); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal attachments: %w", err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]common.AttachmentMetadata, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, common.AttachmentMetadata{
|
||||
Filename: row.Filename,
|
||||
ContentType: row.ContentType,
|
||||
SizeBytes: row.SizeBytes,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// marshalTemplateVariables returns the JSONB bytes for the template_variables
|
||||
// column. nil maps round-trip as SQL NULL.
|
||||
func marshalTemplateVariables(variables map[string]any) ([]byte, error) {
|
||||
if variables == nil {
|
||||
return nil, nil
|
||||
}
|
||||
payload, err := json.Marshal(variables)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal template variables: %w", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// unmarshalTemplateVariables decodes the template_variables JSONB column.
|
||||
// SQL NULL payloads decode to a nil map.
|
||||
func unmarshalTemplateVariables(payload []byte) (map[string]any, error) {
|
||||
if len(payload) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var variables map[string]any
|
||||
if err := json.Unmarshal(payload, &variables); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal template variables: %w", err)
|
||||
}
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
// payloadAttachmentRow stores the on-disk JSONB encoding of one
|
||||
// `acceptgenericdelivery.AttachmentPayload`. The base64 body stays inline so
|
||||
// the entire payload bundle round-trips as one JSONB value.
|
||||
type payloadAttachmentRow struct {
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
ContentBase64 string `json:"content_base64"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// payloadRow stores the on-disk JSONB encoding of one
|
||||
// `acceptgenericdelivery.DeliveryPayload`. delivery_id is intentionally
|
||||
// excluded — the row is keyed by it via the `delivery_payloads` PRIMARY KEY.
|
||||
type payloadRow struct {
|
||||
Attachments []payloadAttachmentRow `json:"attachments"`
|
||||
}
|
||||
|
||||
// marshalDeliveryPayload returns the JSONB bytes for the delivery_payloads
|
||||
// row.
|
||||
func marshalDeliveryPayload(payload acceptgenericdelivery.DeliveryPayload) ([]byte, error) {
|
||||
rows := make([]payloadAttachmentRow, 0, len(payload.Attachments))
|
||||
for _, attachment := range payload.Attachments {
|
||||
rows = append(rows, payloadAttachmentRow{
|
||||
Filename: attachment.Filename,
|
||||
ContentType: attachment.ContentType,
|
||||
ContentBase64: attachment.ContentBase64,
|
||||
SizeBytes: attachment.SizeBytes,
|
||||
})
|
||||
}
|
||||
encoded, err := json.Marshal(payloadRow{Attachments: rows})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal delivery payload: %w", err)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
// unmarshalDeliveryPayload decodes the delivery_payloads row into a
|
||||
// domain-friendly DeliveryPayload using deliveryID as the owning identifier.
|
||||
func unmarshalDeliveryPayload(deliveryID common.DeliveryID, encoded []byte) (acceptgenericdelivery.DeliveryPayload, error) {
|
||||
if len(encoded) == 0 {
|
||||
return acceptgenericdelivery.DeliveryPayload{}, fmt.Errorf("unmarshal delivery payload: empty")
|
||||
}
|
||||
var row payloadRow
|
||||
if err := json.Unmarshal(encoded, &row); err != nil {
|
||||
return acceptgenericdelivery.DeliveryPayload{}, fmt.Errorf("unmarshal delivery payload: %w", err)
|
||||
}
|
||||
out := acceptgenericdelivery.DeliveryPayload{DeliveryID: deliveryID}
|
||||
if len(row.Attachments) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
out.Attachments = make([]acceptgenericdelivery.AttachmentPayload, 0, len(row.Attachments))
|
||||
for _, attachment := range row.Attachments {
|
||||
out.Attachments = append(out.Attachments, acceptgenericdelivery.AttachmentPayload{
|
||||
Filename: attachment.Filename,
|
||||
ContentType: attachment.ContentType,
|
||||
ContentBase64: attachment.ContentBase64,
|
||||
SizeBytes: attachment.SizeBytes,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// marshalRawFields returns the JSONB bytes for the malformed_commands.raw_fields
|
||||
// column. The map is serialised verbatim so future operator queries can match
|
||||
// arbitrary keys.
|
||||
func marshalRawFields(fields map[string]any) ([]byte, error) {
|
||||
if fields == nil {
|
||||
fields = map[string]any{}
|
||||
}
|
||||
payload, err := json.Marshal(fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal raw fields: %w", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// unmarshalRawFields decodes the malformed_commands.raw_fields column.
|
||||
func unmarshalRawFields(payload []byte) (map[string]any, error) {
|
||||
out := map[string]any{}
|
||||
if len(payload) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
if err := json.Unmarshal(payload, &out); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal raw fields: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user