/
entity.go
1827 lines (1598 loc) · 44.5 KB
/
entity.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package registry
import (
"fmt"
"maps"
"reflect"
"strconv"
"strings"
"time"
log "github.com/duglin/dlog"
_ "github.com/go-sql-driver/mysql"
)
type Object map[string]any
// type Map map[string]any
// type Array []any
type Entity struct {
tx *Tx
Registry *Registry `json:"-"`
DbSID string // Entity's SID
Plural string
UID string // Entity's UID
Object map[string]any `json:"-"`
NewObject map[string]any `json:"-"` // updated version, save() will store
// These were added just for convenience and so we can use the same
// struct for traversing the SQL results
Level int // 0=registry, 1=group, 2=resource, 3=version
Path string
Abstract string
EpochSet bool `json:"-"` // Has epoch been updated this transaction?
}
type EntitySetter interface {
Get(name string) any
SetCommit(name string, val any) error // Should never be used
SetSave(name string, val any) error
}
func GoToOurType(val any) string {
switch reflect.ValueOf(val).Kind() {
case reflect.Bool:
return BOOLEAN
case reflect.Int:
return INTEGER
case reflect.Interface:
return ANY
case reflect.Float64:
return DECIMAL
case reflect.String:
return STRING
case reflect.Uint64:
return UINTEGER
case reflect.Slice:
return ARRAY
case reflect.Map:
return MAP
case reflect.Struct:
return OBJECT
}
panic(fmt.Sprintf("Bad Kind: %v", reflect.ValueOf(val).Kind()))
}
func (e *Entity) Get(path string) any {
pp, err := PropPathFromUI(path)
PanicIf(err != nil, fmt.Sprintf("%s", err))
return e.GetPP(pp)
}
func (e *Entity) GetPP(pp *PropPath) any {
name := pp.DB()
if pp.Len() == 1 && pp.Top() == "#resource" {
results, err := Query(e.tx, `
SELECT Content
FROM ResourceContents
WHERE VersionSID=? OR
VersionSID=(SELECT eSID FROM FullTree WHERE ParentSID=? AND
PropName=? and PropValue='true')
`, e.DbSID, e.DbSID, NewPPP("isdefault").DB())
defer results.Close()
if err != nil {
return fmt.Errorf("Error finding contents %q: %s", e.DbSID, err)
}
row := results.NextRow()
if row == nil {
// No data so just return
return nil
}
if results.NextRow() != nil {
panic("too many results")
}
return (*(row[0])).([]byte)
}
// See if we have an updated value in NewObject, if not grab from Object
var val any
if e.NewObject != nil {
var ok bool
val, ok, _ = ObjectGetProp(e.NewObject, pp)
if !ok {
// TODO: DUG - we should not need this
// val, _, _ = ObjectGetProp(e.Object, pp)
}
} else {
val, _, _ = ObjectGetProp(e.Object, pp)
}
// We used to just grab from Object, not NewObject
/*
// An error from ObjectGetProp is ignored because if they tried to
// go into something incorrect/bad we should just return 'nil'.
// This may not be the best choice in the long-run - which in case we
// should return the 'error'
val, _ , _ := ObjectGetProp(e.Object, pp)
*/
log.VPrintf(4, "%s(%s).Get(%s) -> %v", e.Plural, e.UID, name, val)
return val
}
// Value, Found, Error
func ObjectGetProp(obj any, pp *PropPath) (any, bool, error) {
return NestedGetProp(obj, pp, NewPP())
}
// Value, Found, Error
func NestedGetProp(obj any, pp *PropPath, prev *PropPath) (any, bool, error) {
log.VPrintf(3, "ObjectGetProp: %q\nobj:\n%s", pp.UI(), ToJSON(obj))
if pp == nil || pp.Len() == 0 {
return obj, true, nil
}
if IsNil(obj) {
return nil, false,
fmt.Errorf("Can't traverse into nothing: %s", prev.UI())
}
objValue := reflect.ValueOf(obj)
part := pp.Parts[0]
if index := part.Index; index >= 0 {
// Is an array
if objValue.Kind() != reflect.Slice {
return nil, false,
fmt.Errorf("Can't index into non-array: %s", prev.UI())
}
if index < 0 || index >= objValue.Len() {
return nil, false,
fmt.Errorf("Array reference %q out of bounds: "+
"(max:%d-1)", prev.Append(pp.First()).UI(), objValue.Len())
}
objValue = objValue.Index(index)
if objValue.IsValid() {
obj = objValue.Interface()
} else {
panic("help") // Should never get here
obj = nil
}
return NestedGetProp(obj, pp.Next(), prev.Append(pp.First()))
}
// Is map/object
if objValue.Kind() != reflect.Map {
return nil, false, fmt.Errorf("Can't reference a non-map/object: %s",
prev.UI())
}
if objValue.Type().Key().Kind() != reflect.String {
return nil, false, fmt.Errorf("Key of %q must be a string, not %s",
prev.UI(), objValue.Type().Key().Kind())
}
objValue = objValue.MapIndex(reflect.ValueOf(pp.Top()))
if objValue.IsValid() {
obj = objValue.Interface()
} else {
if pp.Next().Len() == 0 {
return nil, false, nil
}
obj = nil
}
return NestedGetProp(obj, pp.Next(), prev.Append(pp.First()))
}
func RawEntityFromPath(tx *Tx, regID string, path string) (*Entity, error) {
log.VPrintf(3, ">Enter: RawEntityFromPath(%s)", path)
defer log.VPrintf(3, "<Exit: RawEntityFromPath")
// RegSID,Level,Plural,eSID,UID,PropName,PropValue,PropType,Path,Abstract
// 0 1 2 3 4 5 6 7 8 9
results, err := Query(tx, `
SELECT
e.RegSID as RegSID,
e.Level as Level,
e.Plural as Plural,
e.eSID as eSID,
e.UID as UID,
p.PropName as PropName,
p.PropValue as PropValue,
p.PropType as PropType,
e.Path as Path,
e.Abstract as Abstract
FROM Entities AS e
LEFT JOIN Props AS p ON (e.eSID=p.EntitySID)
WHERE e.RegSID=? AND e.Path=? ORDER BY Path`, regID, path)
defer results.Close()
if err != nil {
return nil, err
}
return readNextEntity(tx, results)
}
func RawEntitiesFromQuery(tx *Tx, regID string, query string, args ...any) ([]*Entity, error) {
log.VPrintf(3, ">Enter: RawEntititiesFromQuery(%s)", query)
defer log.VPrintf(3, "<Exit: RawEntitiesFromQuery")
// RegSID,Level,Plural,eSID,UID,PropName,PropValue,PropType,Path,Abstract
// 0 1 2 3 4 5 6 7 8 9
if query != "" {
query = "AND (" + query + ") "
}
args = append(append([]any{}, regID), args...)
results, err := Query(tx, `
SELECT
e.RegSID as RegSID,
e.Level as Level,
e.Plural as Plural,
e.eSID as eSID,
e.UID as UID,
p.PropName as PropName,
p.PropValue as PropValue,
p.PropType as PropType,
e.Path as Path,
e.Abstract as Abstract
FROM Entities AS e
LEFT JOIN Props AS p ON (e.eSID=p.EntitySID)
WHERE e.RegSID=? `+query+` ORDER BY Path`, args...)
defer results.Close()
if err != nil {
return nil, err
}
entities := []*Entity{}
for {
e, err := readNextEntity(tx, results)
if err != nil {
return nil, err
}
if e == nil {
break
}
entities = append(entities, e)
}
return entities, nil
}
// Update the entity's Object - not the other props in Entity. Similar to
// RawEntityFromPath
func (e *Entity) Refresh() error {
log.VPrintf(3, ">Enter: Refresh(%s)", e.DbSID)
defer log.VPrintf(3, "<Exit: Refresh")
results, err := Query(e.tx, `
SELECT PropName, PropValue, PropType
FROM Props WHERE EntitySID=? `, e.DbSID)
defer results.Close()
if err != nil {
log.Printf("Error refreshing props(%s): %s", e.DbSID, err)
return fmt.Errorf("Error refreshing props(%s): %s", e.DbSID, err)
}
// Erase all old props first
e.Object = map[string]any{}
e.NewObject = nil
for row := results.NextRow(); row != nil; row = results.NextRow() {
name := NotNilString(row[0])
val := NotNilString(row[1])
propType := NotNilString(row[2])
if err = e.SetFromDBName(name, &val, propType); err != nil {
return err
}
}
return nil
}
// All in one: Set, Validate, Save to DB and Commit (or Rollback on error)
// Should never be used because the act of committing should be done
// by the caller once all of the changes are done. This is a holdover from
// before we had transaction support - once we're sure, delete it
func (e *Entity) SetCommit(path string, val any) error {
log.VPrintf(3, ">Enter: SetCommit(%s=%v)", path, val)
defer log.VPrintf(3, "<Exit Set")
err := e.SetSave(path, val)
Must(e.tx.Conditional(err))
return err
}
// Set, Validate and Save to DB but not Commit
func (e *Entity) SetSave(path string, val any) error {
log.VPrintf(3, ">Enter: SetSave(%s=%v)", path, val)
defer log.VPrintf(3, "<Exit Set")
pp, err := PropPathFromUI(path)
if err == nil {
// Set, Validate and Save
err = e.SetPP(pp, val)
}
return err
}
// Set the prop in the Entity but don't Validate or Save to the DB
func (e *Entity) JustSet(pp *PropPath, val any) error {
log.VPrintf(3, ">Enter: JustSet(%s.%s=%v)", e.UID, pp.UI(), val)
defer log.VPrintf(3, "<Exit: JustSet")
// Assume no other edits are pending
// e.Refresh() // trying not to have this here
if e.NewObject == nil {
// If we don't have a NewObject yet then this is our first update
// so clone the current values before adding the new prop/val
if e.Object == nil {
e.NewObject = map[string]any{}
} else {
e.NewObject = maps.Clone(e.Object)
}
}
// Cheat a little just to make caller's life easier by converting
// empty structs and maps need to be of the type we like (meaning 'any's)
if !IsNil(val) {
if val == struct{}{} {
val = map[string]any{}
}
valValue := reflect.ValueOf(val)
if valValue.Kind() == reflect.Slice && valValue.Len() == 0 {
val = []any{}
}
if valValue.Kind() == reflect.Map && valValue.Len() == 0 {
val = map[string]any{}
}
}
// end of cheat
if pp.Top() == "epoch" {
e.EpochSet = true
}
log.VPrintf(3, "Abstract/ID: %s/%s", e.Abstract, e.UID)
log.VPrintf(3, "e.Object:\n%s", ToJSON(e.Object))
log.VPrintf(3, "e.NewObject:\n%s", ToJSON(e.NewObject))
return ObjectSetProp(e.NewObject, pp, val)
}
func (e *Entity) ValidateAndSave() error {
log.VPrintf(3, ">Enter: ValidateAndSave %s/%s", e.Abstract, e.UID)
defer log.VPrintf(3, "<Exit: ValidateAndSave")
// Make sure we have a tx since Validate assumes it
e.tx.NewTx()
// If nothing changed, just exit
// if e.NewObject == nil {
// return nil
// }
log.VPrintf(3, "Validating %s/%s e.Object:\n%s\n\ne.NewObject:\n%s",
e.Abstract, e.UID, ToJSON(e.Object), ToJSON(e.NewObject))
if err := e.Validate(); err != nil {
return err
}
if err := PrepUpdateEntity(e); err != nil {
return err
}
return e.Save()
}
// This is really just an internal Setter used for testing.
// It'sll set a property and then validate and save the entity in the DB
func (e *Entity) SetPP(pp *PropPath, val any) error {
log.VPrintf(3, ">Enter: SetPP(%s: %s=%v)", e.DbSID, pp.UI(), val)
defer log.VPrintf(3, "<Exit SetPP")
defer func() {
log.VPrintf(3, "SetPP exit: e.Object:\n%s", ToJSON(e.Object))
}()
if err := e.JustSet(pp, val); err != nil {
return err
}
err := e.ValidateAndSave()
if err != nil {
// If there's an error, and we're making the assumption that we're
// setting and saving all in one shot (and there are no other edits
// pending), go ahead and undo the changes since they're wrong.
// Otherwise the caller would need to call Refresh themselves.
// Not sure why setting it to nil isn't sufficient (todo)
// e.NewObject = nil
e.Refresh()
}
return err
}
// This will save a single property/value in the DB. This assumes
// the caller is traversing the Object and splitting it into individual props
func (e *Entity) SetDBProperty(pp *PropPath, val any) error {
log.VPrintf(3, ">Enter: SetDBProperty(%s=%v)", pp.UI(), val)
defer log.VPrintf(3, "<Exit SetDBProperty")
PanicIf(pp.UI() == "", "pp is empty")
var err error
name := pp.DB()
// Any prop with "dontStore"=true we skip
if sp, ok := SpecProps[pp.Top()]; ok && sp.internals.dontStore {
return nil
}
PanicIf(e.DbSID == "", "DbSID should not be empty")
PanicIf(e.Registry == nil, "Registry should not be nil")
// #resource is special and is saved in it's own table
// Need to explicitly set #resoure to nil to delete it.
if pp.Len() == 1 && pp.Top() == "#resource" {
if IsNil(val) {
err = Do(e.tx, `DELETE FROM ResourceContents WHERE VersionSID=?`,
e.DbSID)
return err
} else {
if val == "" {
return nil
}
// The actual contents
err = DoOneTwo(e.tx, `
REPLACE INTO ResourceContents(VersionSID, Content)
VALUES(?,?)`, e.DbSID, val)
if err != nil {
return err
}
val = ""
// Fall thru to normal processing so we save a placeholder
// attribute in the resource
}
}
if IsNil(val) {
// Should never use this but keeping it just in case
err = Do(e.tx, `DELETE FROM Props WHERE EntitySID=? and PropName=?`,
e.DbSID, name)
} else {
propType := GoToOurType(val)
// Convert booleans to true/false instead of 1/0 so filter works
// ...=true and not ...=1
dbVal := val
if propType == BOOLEAN {
if val == true {
dbVal = "true"
} else {
dbVal = "false"
}
}
switch reflect.ValueOf(val).Kind() {
case reflect.Slice:
if reflect.ValueOf(val).Len() > 0 {
return fmt.Errorf("Can't set non-empty arrays")
}
dbVal = ""
case reflect.Map:
if reflect.ValueOf(val).Len() > 0 {
return fmt.Errorf("Can't set non-empty maps")
}
dbVal = ""
case reflect.Struct:
if reflect.ValueOf(val).NumField() > 0 {
return fmt.Errorf("Can't set non-empty objects")
}
dbVal = ""
}
err = DoOneTwo(e.tx, `
REPLACE INTO Props(
RegistrySID, EntitySID, PropName, PropValue, PropType)
VALUES( ?,?,?,?,? )`,
e.Registry.DbSID, e.DbSID, name, dbVal, propType)
}
if err != nil {
log.Printf("Error updating prop(%s/%v): %s", pp.UI(), val, err)
return fmt.Errorf("Error updating prop(%s/%v): %s", pp.UI(), val, err)
}
return nil
}
// This is used to take a DB entry and update the current Entity's Object
func (e *Entity) SetFromDBName(name string, val *string, propType string) error {
pp := MustPropPathFromDB(name)
if val == nil {
return ObjectSetProp(e.Object, pp, val)
}
if e.Object == nil {
e.Object = map[string]any{}
}
if propType == STRING || propType == URI || propType == URI_REFERENCE ||
propType == URI_TEMPLATE || propType == URL || propType == TIMESTAMP {
return ObjectSetProp(e.Object, pp, *val)
} else if propType == BOOLEAN {
// Technically the "1" check shouldn't be needed, but just in case
return ObjectSetProp(e.Object, pp, (*val == "1" || (*val == "true")))
} else if propType == INTEGER || propType == UINTEGER {
tmpInt, err := strconv.Atoi(*val)
if err != nil {
panic(fmt.Sprintf("error parsing int: %s", *val))
}
return ObjectSetProp(e.Object, pp, tmpInt)
} else if propType == DECIMAL {
tmpFloat, err := strconv.ParseFloat(*val, 64)
if err != nil {
panic(fmt.Sprintf("error parsing float: %s", *val))
}
return ObjectSetProp(e.Object, pp, tmpFloat)
} else if propType == MAP {
if *val != "" {
panic(fmt.Sprintf("MAP value should be empty string"))
}
return ObjectSetProp(e.Object, pp, map[string]any{})
} else if propType == ARRAY {
if *val != "" {
panic(fmt.Sprintf("MAP value should be empty string"))
}
return ObjectSetProp(e.Object, pp, []any{})
} else if propType == OBJECT {
if *val != "" {
panic(fmt.Sprintf("MAP value should be empty string"))
}
return ObjectSetProp(e.Object, pp, map[string]any{})
} else {
panic(fmt.Sprintf("bad type(%s): %v", propType, name))
}
}
// Create a new Entity based on what's in the DB. Similar to Refresh()
func readNextEntity(tx *Tx, results *Result) (*Entity, error) {
entity := (*Entity)(nil)
// RegSID,Level,Plural,eSID,UID,PropName,PropValue,PropType,Path,Abstract
// 0 1 2 3 4 5 6 7 8 9
for row := results.NextRow(); row != nil; row = results.NextRow() {
// log.Printf("Row(%d): %#v", len(row), row)
if log.GetVerbose() >= 4 {
str := "("
for _, c := range row {
if IsNil(c) || IsNil(*c) {
str += "nil,"
} else {
str += fmt.Sprintf("%s,", *c)
}
}
log.Printf("Row: %s)", str)
}
level := int((*row[1]).(int64))
plural := NotNilString(row[2])
uid := NotNilString(row[4])
if entity == nil {
entity = &Entity{
tx: tx,
Registry: tx.Registry,
DbSID: NotNilString(row[3]),
Plural: plural,
UID: uid,
Level: level,
Path: NotNilString(row[8]),
Abstract: NotNilString(row[9]),
}
} else {
// If the next row isn't part of the current Entity then
// push it back into the result set so we'll grab it the next time
// we're called. And exit.
if entity.Level != level || entity.Plural != plural || entity.UID != uid {
results.Push()
break
}
}
propName := NotNilString(row[5])
propVal := NotNilString(row[6])
propType := NotNilString(row[7])
// Edge case - no props but entity is there
if propName == "" && propVal == "" && propType == "" {
continue
}
if err := entity.SetFromDBName(propName, &propVal, propType); err != nil {
return nil, err
}
}
return entity, nil
}
// This allows for us to choose the order and define custom logic per prop
var OrderedSpecProps = []*Attribute{
{
Name: "specversion",
Type: STRING,
ReadOnly: true,
Immutable: true,
ServerRequired: true,
internals: AttrInternals{
levels: "0",
dontStore: false,
getFn: func(e *Entity, info *RequestInfo) any {
return SPECVERSION
},
checkFn: func(e *Entity) error {
tmp := e.NewObject["specversion"]
if !IsNil(tmp) && tmp != "" && tmp != SPECVERSION {
return fmt.Errorf("Invalid 'specversion': %s", tmp)
}
return nil
},
updateFn: nil,
},
},
{
Name: "id",
Type: STRING,
Immutable: true,
ServerRequired: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: func(e *Entity) error {
oldID := any(e.UID)
newID := any(e.NewObject["id"])
if IsNil(newID) {
return nil // Not trying to be updated, so skip it
}
if newID == "" {
return fmt.Errorf("ID can't be an empty string")
}
if oldID != "" && newID != oldID {
return fmt.Errorf("Can't change the ID of an "+
"entity(%s->%s)", oldID, newID)
}
return nil
},
updateFn: func(e *Entity) error {
// Make sure the ID is always set
e.NewObject["id"] = e.UID
return nil
},
},
},
{
Name: "name",
Type: STRING,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "epoch",
Type: UINTEGER,
ServerRequired: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: func(e *Entity) error {
// If we explicitly setEpoch via internal API then don't check
if e.EpochSet {
return nil
}
val := e.NewObject["epoch"]
if IsNil(val) {
return nil
}
tmp := e.Object["epoch"]
oldEpoch := NotNilInt(&tmp)
if oldEpoch < 0 {
oldEpoch = 0
}
newEpoch, err := AnyToUInt(val)
if err != nil {
return fmt.Errorf("Attribute \"epoch\" must be a uinteger")
}
if oldEpoch != 0 && newEpoch != oldEpoch {
return fmt.Errorf("Attribute %q(%d) doesn't match "+
"existing value (%d)", "epoch", newEpoch, oldEpoch)
}
return nil
},
updateFn: func(e *Entity) error {
// If we already set Epoch in this Tx, just exit
if e.EpochSet {
return nil
}
// This assumes that ALL entities must have an Epoch property
// that we wnt to set. At one point this wasn't true for
// Resources but hopefully that's no loner true
oldEpoch := e.Object["epoch"]
epoch := NotNilInt(&oldEpoch)
e.NewObject["epoch"] = epoch + 1
e.EpochSet = true
return nil
},
},
},
{
Name: "self",
Type: URL,
ReadOnly: true,
ServerRequired: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: func(e *Entity, info *RequestInfo) any {
base := ""
if info != nil {
base = info.BaseURL
}
if e.Level > 1 {
meta := info != nil && (info.ShowMeta || info.ResourceUID == "")
_, rm := e.GetModels()
if rm.GetHasDocument() == false {
meta = false
}
if meta {
return base + "/" + e.Path + "?meta"
} else {
return base + "/" + e.Path
}
}
return base + "/" + e.Path
},
checkFn: nil,
updateFn: nil,
},
},
{
Name: "isdefault",
Type: BOOLEAN,
ReadOnly: true,
internals: AttrInternals{
levels: "3",
dontStore: true,
getFn: nil,
checkFn: nil,
updateFn: func(e *Entity) error {
// TODO if set, set defaultversionid in the resource to this
// guy's UID
return nil
},
},
},
{
Name: "stickydefaultversion",
Type: BOOLEAN,
ReadOnly: true,
internals: AttrInternals{
levels: "2",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "defaultversionid",
Type: STRING,
ReadOnly: true,
// ServerRequired: true,
internals: AttrInternals{
levels: "2",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "defaultversionurl",
Type: URL,
ReadOnly: true,
// ServerRequired: true,
internals: AttrInternals{
levels: "2",
dontStore: false,
getFn: func(e *Entity, info *RequestInfo) any {
val := e.Object["defaultversionid"]
if IsNil(val) {
return nil
}
base := ""
if info != nil {
base = info.BaseURL
}
tmp := base + "/" + e.Path + "/versions/" + val.(string)
meta := info != nil && (info.ShowMeta || info.ResourceUID == "")
_, rm := e.GetModels()
if rm.GetHasDocument() == false {
meta = false
}
if meta {
tmp += "?meta"
}
return tmp
},
checkFn: nil,
updateFn: nil,
},
},
{
Name: "description",
Type: STRING,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "documentation",
Type: URL,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "labels",
Type: MAP,
Item: &Item{
Type: STRING,
},
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "origin",
Type: URI,
internals: AttrInternals{
levels: "123",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "createdby",
Type: STRING,
ReadOnly: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: func(e *Entity) error {
return nil
},
},
},
{
Name: "createdat",
Type: TIMESTAMP,
ReadOnly: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: func(e *Entity) error {
ct := e.Object["createdat"]
isNew := IsNil(e.Object["epoch"])
if IsNil(ct) && isNew {
if e.Registry.Get("#tracktimestamps") == true {
e.NewObject["createdat"] = e.tx.CreateTime
}
}
return nil
},
},
},
{
Name: "modifiedby",
Type: STRING,
ReadOnly: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: nil,
},
},
{
Name: "modifiedat",
Type: TIMESTAMP,
ReadOnly: true,
internals: AttrInternals{
levels: "",
dontStore: false,
getFn: nil,
checkFn: nil,
updateFn: func(e *Entity) error {
if e.Registry.Get("#tracktimestamps") == true {
e.NewObject["modifiedat"] = e.tx.CreateTime
}
return nil
},
},
},
{
Name: "contenttype",
Type: STRING,
internals: AttrInternals{
levels: "23",
dontStore: false,
httpHeader: "Content-Type",