forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "maps"
7 "slices"
8 "strings"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "tangled.org/core/appview/models"
13)
14
15// no updating type for now
16func AddLabelDefinition(e Execer, l *models.LabelDefinition) (int64, error) {
17 result, err := e.Exec(
18 `insert into label_definitions (
19 did,
20 rkey,
21 name,
22 value_type,
23 value_format,
24 value_enum,
25 scope,
26 color,
27 multiple,
28 created
29 )
30 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
31 on conflict(did, rkey) do update set
32 name = excluded.name,
33 scope = excluded.scope,
34 color = excluded.color,
35 multiple = excluded.multiple`,
36 l.Did,
37 l.Rkey,
38 l.Name,
39 l.ValueType.Type,
40 l.ValueType.Format,
41 strings.Join(l.ValueType.Enum, ","),
42 strings.Join(l.Scope, ","),
43 l.Color,
44 l.Multiple,
45 l.Created.Format(time.RFC3339),
46 time.Now().Format(time.RFC3339),
47 )
48 if err != nil {
49 return 0, err
50 }
51
52 id, err := result.LastInsertId()
53 if err != nil {
54 return 0, err
55 }
56
57 l.Id = id
58
59 return id, nil
60}
61
62func DeleteLabelDefinition(e Execer, filters ...filter) error {
63 var conditions []string
64 var args []any
65 for _, filter := range filters {
66 conditions = append(conditions, filter.Condition())
67 args = append(args, filter.Arg()...)
68 }
69 whereClause := ""
70 if conditions != nil {
71 whereClause = " where " + strings.Join(conditions, " and ")
72 }
73 query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
74 _, err := e.Exec(query, args...)
75 return err
76}
77
78func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
79 var labelDefinitions []models.LabelDefinition
80 var conditions []string
81 var args []any
82
83 for _, filter := range filters {
84 conditions = append(conditions, filter.Condition())
85 args = append(args, filter.Arg()...)
86 }
87
88 whereClause := ""
89 if conditions != nil {
90 whereClause = " where " + strings.Join(conditions, " and ")
91 }
92
93 query := fmt.Sprintf(
94 `
95 select
96 id,
97 did,
98 rkey,
99 name,
100 value_type,
101 value_format,
102 value_enum,
103 scope,
104 color,
105 multiple,
106 created
107 from label_definitions
108 %s
109 order by created
110 `,
111 whereClause,
112 )
113
114 rows, err := e.Query(query, args...)
115 if err != nil {
116 return nil, err
117 }
118 defer rows.Close()
119
120 for rows.Next() {
121 var labelDefinition models.LabelDefinition
122 var createdAt, enumVariants, scopes string
123 var color sql.Null[string]
124 var multiple int
125
126 if err := rows.Scan(
127 &labelDefinition.Id,
128 &labelDefinition.Did,
129 &labelDefinition.Rkey,
130 &labelDefinition.Name,
131 &labelDefinition.ValueType.Type,
132 &labelDefinition.ValueType.Format,
133 &enumVariants,
134 &scopes,
135 &color,
136 &multiple,
137 &createdAt,
138 ); err != nil {
139 return nil, err
140 }
141
142 labelDefinition.Created, err = time.Parse(time.RFC3339, createdAt)
143 if err != nil {
144 labelDefinition.Created = time.Now()
145 }
146
147 if color.Valid {
148 labelDefinition.Color = &color.V
149 }
150
151 if multiple != 0 {
152 labelDefinition.Multiple = true
153 }
154
155 if enumVariants != "" {
156 labelDefinition.ValueType.Enum = strings.Split(enumVariants, ",")
157 }
158
159 for s := range strings.SplitSeq(scopes, ",") {
160 labelDefinition.Scope = append(labelDefinition.Scope, s)
161 }
162
163 labelDefinitions = append(labelDefinitions, labelDefinition)
164 }
165
166 return labelDefinitions, nil
167}
168
169// helper to get exactly one label def
170func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
171 labels, err := GetLabelDefinitions(e, filters...)
172 if err != nil {
173 return nil, err
174 }
175
176 if labels == nil {
177 return nil, sql.ErrNoRows
178 }
179
180 if len(labels) != 1 {
181 return nil, fmt.Errorf("too many rows returned")
182 }
183
184 return &labels[0], nil
185}
186
187func AddLabelOp(e Execer, l *models.LabelOp) (int64, error) {
188 now := time.Now()
189 result, err := e.Exec(
190 `insert into label_ops (
191 did,
192 rkey,
193 subject,
194 operation,
195 operand_key,
196 operand_value,
197 performed,
198 indexed
199 )
200 values (?, ?, ?, ?, ?, ?, ?, ?)
201 on conflict(did, rkey, subject, operand_key, operand_value) do update set
202 operation = excluded.operation,
203 operand_value = excluded.operand_value,
204 performed = excluded.performed,
205 indexed = excluded.indexed`,
206 l.Did,
207 l.Rkey,
208 l.Subject.String(),
209 string(l.Operation),
210 l.OperandKey,
211 l.OperandValue,
212 l.PerformedAt.Format(time.RFC3339),
213 now.Format(time.RFC3339),
214 )
215 if err != nil {
216 return 0, err
217 }
218
219 id, err := result.LastInsertId()
220 if err != nil {
221 return 0, err
222 }
223
224 l.Id = id
225 l.IndexedAt = now
226
227 return id, nil
228}
229
230func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
231 var labelOps []models.LabelOp
232 var conditions []string
233 var args []any
234
235 for _, filter := range filters {
236 conditions = append(conditions, filter.Condition())
237 args = append(args, filter.Arg()...)
238 }
239
240 whereClause := ""
241 if conditions != nil {
242 whereClause = " where " + strings.Join(conditions, " and ")
243 }
244
245 query := fmt.Sprintf(
246 `
247 select
248 id,
249 did,
250 rkey,
251 subject,
252 operation,
253 operand_key,
254 operand_value,
255 performed,
256 indexed
257 from label_ops
258 %s
259 order by indexed
260 `,
261 whereClause,
262 )
263
264 rows, err := e.Query(query, args...)
265 if err != nil {
266 return nil, err
267 }
268 defer rows.Close()
269
270 for rows.Next() {
271 var labelOp models.LabelOp
272 var performedAt, indexedAt string
273
274 if err := rows.Scan(
275 &labelOp.Id,
276 &labelOp.Did,
277 &labelOp.Rkey,
278 &labelOp.Subject,
279 &labelOp.Operation,
280 &labelOp.OperandKey,
281 &labelOp.OperandValue,
282 &performedAt,
283 &indexedAt,
284 ); err != nil {
285 return nil, err
286 }
287
288 labelOp.PerformedAt, err = time.Parse(time.RFC3339, performedAt)
289 if err != nil {
290 labelOp.PerformedAt = time.Now()
291 }
292
293 labelOp.IndexedAt, err = time.Parse(time.RFC3339, indexedAt)
294 if err != nil {
295 labelOp.IndexedAt = time.Now()
296 }
297
298 labelOps = append(labelOps, labelOp)
299 }
300
301 return labelOps, nil
302}
303
304// get labels for a given list of subject URIs
305func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
306 ops, err := GetLabelOps(e, filters...)
307 if err != nil {
308 return nil, err
309 }
310
311 // group ops by subject
312 opsBySubject := make(map[syntax.ATURI][]models.LabelOp)
313 for _, op := range ops {
314 subject := syntax.ATURI(op.Subject)
315 opsBySubject[subject] = append(opsBySubject[subject], op)
316 }
317
318 // get all unique labelats for creating the context
319 labelAtSet := make(map[string]bool)
320 for _, op := range ops {
321 labelAtSet[op.OperandKey] = true
322 }
323 labelAts := slices.Collect(maps.Keys(labelAtSet))
324
325 actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
326 if err != nil {
327 return nil, err
328 }
329
330 // apply label ops for each subject and collect results
331 results := make(map[syntax.ATURI]models.LabelState)
332 for subject, subjectOps := range opsBySubject {
333 state := models.NewLabelState()
334 actx.ApplyLabelOps(state, subjectOps)
335 results[subject] = state
336 }
337
338 return results, nil
339}
340
341func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
342 labels, err := GetLabelDefinitions(e, filters...)
343 if err != nil {
344 return nil, err
345 }
346
347 defs := make(map[string]*models.LabelDefinition)
348 for _, l := range labels {
349 defs[l.AtUri().String()] = &l
350 }
351
352 return &models.LabelApplicationCtx{Defs: defs}, nil
353}