Skip to content

Commit 2e9bf30

Browse files
committed
feat(toml): add created_at/updated_at timestamps to entity files
- Store timestamps directly in TOML (RFC3339 format) - entity add: sets both created_at and updated_at to now - entity update/deprecate: updates only updated_at - Sync passes timestamps through to index - Migration preserves timestamps from old SQLite DB - Writer outputs timestamps in canonical order (after status, before metadata)
1 parent ab4b446 commit 2e9bf30

4 files changed

Lines changed: 60 additions & 19 deletions

File tree

internal/cli/entity.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ var entityAddCmd = &cobra.Command{
7575
Title: title,
7676
Description: description,
7777
Status: entityStatus,
78+
CreatedAt: time.Now(),
79+
UpdatedAt: time.Now(),
7880
Metadata: meta,
7981
}
8082

@@ -96,15 +98,11 @@ var entityAddCmd = &cobra.Command{
9698
handleError(cmd, fmt.Errorf("append history: %w", err))
9799
}
98100

99-
entity := model.Entity{
100-
ID: id,
101-
Type: et,
102-
Layer: model.LayerForEntityType(et),
103-
Title: title,
104-
Description: description,
105-
Status: entityStatus,
106-
Metadata: metadata,
101+
entity, err := ef.ToEntity()
102+
if err != nil {
103+
handleError(cmd, fmt.Errorf("convert entity: %w", err))
107104
}
105+
entity.Metadata = metadata
108106

109107
writeJSON(cmd, jsoncontract.EntityResponse{Entity: entity})
110108
return nil
@@ -235,6 +233,8 @@ var entityUpdateCmd = &cobra.Command{
235233
ef.Metadata = meta
236234
}
237235

236+
ef.UpdatedAt = time.Now()
237+
238238
if err := tomlStore.WriteEntity(ef); err != nil {
239239
handleError(cmd, fmt.Errorf("write entity: %w", err))
240240
}
@@ -275,6 +275,7 @@ var entityDeprecateCmd = &cobra.Command{
275275
}
276276

277277
ef.Status = model.EntityStatusDeprecated
278+
ef.UpdatedAt = time.Now()
278279

279280
if err := tomlStore.WriteEntity(ef); err != nil {
280281
handleError(cmd, fmt.Errorf("write entity: %w", err))
@@ -410,6 +411,8 @@ var entityImportCmd = &cobra.Command{
410411
Title: item.Title,
411412
Description: item.Description,
412413
Status: entityStatus,
414+
CreatedAt: time.Now(),
415+
UpdatedAt: time.Now(),
413416
Metadata: meta,
414417
}
415418

internal/sync/sync.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ func (s *Syncer) ForceRebuild() error {
8888
Description: entity.Description,
8989
Metadata: metaStr,
9090
FilePath: s.store.EntityPath(ef.ID, ef.Type),
91+
CreatedAt: entity.CreatedAt,
92+
UpdatedAt: entity.UpdatedAt,
9193
})
9294

9395
relations, err := ef.ToRelations()

internal/toml/model.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ import (
99

1010
// EntityFile represents the TOML structure for a single entity file.
1111
// Layer is NOT stored — it is derived from entity type at load time.
12-
// Timestamps are NOT stored — they are derived from git/filesystem.
1312
type EntityFile struct {
14-
Schema int `toml:"schema"`
15-
ID string `toml:"id"`
16-
Type model.EntityType `toml:"type"`
17-
Title string `toml:"title"`
18-
Description string `toml:"description,omitempty"`
13+
Schema int `toml:"schema"`
14+
ID string `toml:"id"`
15+
Type model.EntityType `toml:"type"`
16+
Title string `toml:"title"`
17+
Description string `toml:"description,omitempty"`
1918
Status model.EntityStatus `toml:"status"`
20-
Metadata map[string]any `toml:"metadata,omitempty"`
21-
Relations []RelationEntry `toml:"relations,omitempty"`
19+
CreatedAt time.Time `toml:"created_at,omitempty"`
20+
UpdatedAt time.Time `toml:"updated_at,omitempty"`
21+
Metadata map[string]any `toml:"metadata,omitempty"`
22+
Relations []RelationEntry `toml:"relations,omitempty"`
2223
}
2324

2425
// RelationEntry represents a single relation within an entity's TOML file.
@@ -47,8 +48,7 @@ type HistoryEntry struct {
4748
}
4849

4950
// ToEntity converts an EntityFile to a model.Entity.
50-
// Layer is derived from the entity type. Timestamps are left empty
51-
// (to be populated by the caller from git/filesystem).
51+
// Layer is derived from the entity type. Timestamps are formatted as RFC3339.
5252
func (ef *EntityFile) ToEntity() (model.Entity, error) {
5353
var meta json.RawMessage
5454
if len(ef.Metadata) > 0 {
@@ -59,6 +59,14 @@ func (ef *EntityFile) ToEntity() (model.Entity, error) {
5959
meta = b
6060
}
6161

62+
var createdAt, updatedAt string
63+
if !ef.CreatedAt.IsZero() {
64+
createdAt = ef.CreatedAt.Format(time.RFC3339)
65+
}
66+
if !ef.UpdatedAt.IsZero() {
67+
updatedAt = ef.UpdatedAt.Format(time.RFC3339)
68+
}
69+
6270
return model.Entity{
6371
ID: ef.ID,
6472
Type: ef.Type,
@@ -67,6 +75,8 @@ func (ef *EntityFile) ToEntity() (model.Entity, error) {
6775
Description: ef.Description,
6876
Status: ef.Status,
6977
Metadata: meta,
78+
CreatedAt: createdAt,
79+
UpdatedAt: updatedAt,
7080
}, nil
7181
}
7282

@@ -129,13 +139,31 @@ func EntityFileFrom(e model.Entity, relations []model.Relation) (EntityFile, err
129139
entries = append(entries, re)
130140
}
131141

142+
var createdAt, updatedAt time.Time
143+
if e.CreatedAt != "" {
144+
if t, err := time.Parse(time.RFC3339, e.CreatedAt); err == nil {
145+
createdAt = t
146+
} else if t, err := time.Parse("2006-01-02 15:04:05", e.CreatedAt); err == nil {
147+
createdAt = t
148+
}
149+
}
150+
if e.UpdatedAt != "" {
151+
if t, err := time.Parse(time.RFC3339, e.UpdatedAt); err == nil {
152+
updatedAt = t
153+
} else if t, err := time.Parse("2006-01-02 15:04:05", e.UpdatedAt); err == nil {
154+
updatedAt = t
155+
}
156+
}
157+
132158
return EntityFile{
133159
Schema: 1,
134160
ID: e.ID,
135161
Type: e.Type,
136162
Title: e.Title,
137163
Description: e.Description,
138164
Status: e.Status,
165+
CreatedAt: createdAt,
166+
UpdatedAt: updatedAt,
139167
Metadata: meta,
140168
Relations: entries,
141169
}, nil

internal/toml/writer.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"fmt"
55
"sort"
66
"strings"
7+
"time"
78
"unicode"
89
"unicode/utf8"
910
)
1011

1112
// MarshalEntityFile produces canonical TOML output for an EntityFile.
12-
// Key order: schema, id, type, title, description (if non-empty), status, [metadata], [[relations]].
13+
// Key order: schema, id, type, title, description (if non-empty), status, created_at, updated_at, [metadata], [[relations]].
1314
// Relations are sorted by (type ASC, to ASC).
1415
func MarshalEntityFile(ef EntityFile) string {
1516
var b strings.Builder
@@ -23,6 +24,13 @@ func MarshalEntityFile(ef EntityFile) string {
2324
}
2425
b.WriteString("status = " + tomlQuote(string(ef.Status)) + "\n")
2526

27+
if !ef.CreatedAt.IsZero() {
28+
b.WriteString(fmt.Sprintf("created_at = %s\n", ef.CreatedAt.Format(time.RFC3339)))
29+
}
30+
if !ef.UpdatedAt.IsZero() {
31+
b.WriteString(fmt.Sprintf("updated_at = %s\n", ef.UpdatedAt.Format(time.RFC3339)))
32+
}
33+
2634
if len(ef.Metadata) > 0 {
2735
b.WriteByte('\n')
2836
b.WriteString("[metadata]\n")

0 commit comments

Comments
 (0)