github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/sys/builtin/impl_refintegrity.go (about)

     1  /*
     2   * Copyright (c) 2020-present unTill Pro, Ltd.
     3   * @author Denis Gribanov
     4   */
     5  
     6  package builtin
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"net/http"
    13  
    14  	"github.com/voedger/voedger/pkg/appdef"
    15  	"github.com/voedger/voedger/pkg/goutils/iterate"
    16  	"github.com/voedger/voedger/pkg/istructs"
    17  	"github.com/voedger/voedger/pkg/istructsmem"
    18  	"github.com/voedger/voedger/pkg/state"
    19  	coreutils "github.com/voedger/voedger/pkg/utils"
    20  )
    21  
    22  func provideRefIntegrityValidation(cfg *istructsmem.AppConfigType) {
    23  	cfg.AddSyncProjectors(istructs.Projector{
    24  		Name: qNameRecordsRegistryProjector,
    25  		Func: provideRecordsRegistryProjector(cfg),
    26  	})
    27  	cfg.AddCUDValidators(provideRefIntegrityValidator())
    28  }
    29  
    30  func CheckRefIntegrity(obj istructs.IRowReader, appStructs istructs.IAppStructs, wsid istructs.WSID) (err error) {
    31  	appDef := appStructs.AppDef()
    32  	objQName := obj.AsQName(appdef.SystemField_QName)
    33  	fields := appDef.Type(objQName).(appdef.IFields)
    34  
    35  	for _, refField := range fields.RefFields() {
    36  		targetID := obj.AsRecordID(refField.Name())
    37  		if targetID == istructs.NullRecordID || targetID.IsRaw() {
    38  			continue
    39  		}
    40  		allowedTargetQNames := refField.Refs()
    41  		kb := appStructs.ViewRecords().KeyBuilder(QNameViewRecordsRegistry)
    42  		idHi := CrackID(targetID)
    43  		kb.PutInt64(Field_IDHi, int64(idHi))
    44  		kb.PutRecordID(Field_ID, targetID)
    45  		registryRecord, err := appStructs.ViewRecords().Get(wsid, kb)
    46  		if err == nil {
    47  			if len(allowedTargetQNames) > 0 && !allowedTargetQNames.Contains(registryRecord.AsQName(field_QName)) {
    48  				return wrongQName(targetID, objQName, refField.Name(), registryRecord.AsQName(field_QName), allowedTargetQNames)
    49  			}
    50  			continue
    51  		}
    52  		if !errors.Is(err, istructsmem.ErrRecordNotFound) {
    53  			// notest
    54  			return err
    55  		}
    56  		return fmt.Errorf("%w: record ID %d referenced by %s.%s does not exist", ErrReferentialIntegrityViolation, targetID, objQName, refField.Name())
    57  	}
    58  
    59  	return nil
    60  }
    61  
    62  func wrongQName(targetID istructs.RecordID, srcQName appdef.QName, srcField string, actualQName appdef.QName, allowedQNames appdef.QNames) error {
    63  	return fmt.Errorf("%w: record ID %d referenced by %s.%s is of QName %s whereas %v QNames are only allowed", ErrReferentialIntegrityViolation,
    64  		targetID, srcQName, srcField, actualQName, allowedQNames)
    65  }
    66  
    67  func provideRecordsRegistryProjector(cfg *istructsmem.AppConfigType) func(event istructs.IPLogEvent, st istructs.IState, intents istructs.IIntents) (err error) {
    68  	return func(event istructs.IPLogEvent, st istructs.IState, intents istructs.IIntents) (err error) {
    69  		argType := cfg.AppDef.Type(event.ArgumentObject().QName())
    70  		if argType.Kind() == appdef.TypeKind_ODoc || argType.Kind() == appdef.TypeKind_ORecord {
    71  			if err := writeObjectToRegistry(event.ArgumentObject(), cfg.AppDef, st, intents, event.WLogOffset()); err != nil {
    72  				// notest
    73  				return err
    74  			}
    75  		}
    76  		return iterate.ForEachError(event.CUDs, func(rec istructs.ICUDRow) error {
    77  			if !rec.IsNew() {
    78  				return nil
    79  			}
    80  			return writeObjectToRegistry(rec, cfg.AppDef, st, intents, event.WLogOffset())
    81  		})
    82  	}
    83  }
    84  
    85  func writeObjectToRegistry(root istructs.IRowReader, appDef appdef.IAppDef, st istructs.IState, intents istructs.IIntents, wLogOffsetToStore istructs.Offset) error {
    86  	if err := writeRegistry(st, intents, root.AsRecordID(appdef.SystemField_ID), wLogOffsetToStore, root.AsQName(appdef.SystemField_QName)); err != nil {
    87  		// notest
    88  		return err
    89  	}
    90  	object, ok := root.(istructs.IObject)
    91  	if !ok {
    92  		return nil
    93  	}
    94  	return iterate.ForEachError(object.Containers, func(container string) (err error) {
    95  		return iterate.ForEachError1Arg(object.Children, container, func(child istructs.IObject) error {
    96  			elType := appDef.Type(child.QName())
    97  			if elType.Kind() != appdef.TypeKind_ODoc && elType.Kind() != appdef.TypeKind_ORecord {
    98  				return nil
    99  			}
   100  			return writeObjectToRegistry(child, appDef, st, intents, wLogOffsetToStore)
   101  		})
   102  	})
   103  }
   104  
   105  func writeRegistry(st istructs.IState, intents istructs.IIntents, idToStore istructs.RecordID, wLogOffsetToStore istructs.Offset, qNameToStore appdef.QName) error {
   106  	kb, err := st.KeyBuilder(state.View, QNameViewRecordsRegistry)
   107  	if err != nil {
   108  		// notest
   109  		return err
   110  	}
   111  	idHi := CrackID(idToStore)
   112  	kb.PutInt64(Field_IDHi, int64(idHi))
   113  	kb.PutRecordID(Field_ID, idToStore)
   114  	recordsRegistryRecBuilder, err := intents.NewValue(kb)
   115  	if err != nil {
   116  		// notest
   117  		return err
   118  	}
   119  	recordsRegistryRecBuilder.PutInt64(Field_WLogOffset, int64(wLogOffsetToStore))
   120  	recordsRegistryRecBuilder.PutQName(field_QName, qNameToStore)
   121  	return nil
   122  }
   123  
   124  func provideRefIntegrityValidator() istructs.CUDValidator {
   125  	return istructs.CUDValidator{
   126  		Match: func(cud istructs.ICUDRow, wsid istructs.WSID, cmdQName appdef.QName) bool {
   127  			return cmdQName != QNameCommandInit
   128  		},
   129  		Validate: func(ctx context.Context, appStructs istructs.IAppStructs, cudRow istructs.ICUDRow, wsid istructs.WSID, cmdQName appdef.QName) (err error) {
   130  			if err = CheckRefIntegrity(cudRow, appStructs, wsid); err == nil {
   131  				return nil
   132  			}
   133  			status := http.StatusInternalServerError
   134  			if errors.Is(err, ErrReferentialIntegrityViolation) {
   135  				status = http.StatusBadRequest
   136  			}
   137  			return coreutils.WrapSysError(err, status)
   138  		},
   139  	}
   140  }