github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/processors/command/impl.go (about)

     1  /*
     2   * Copyright (c) 2021-present unTill Pro, Ltd.
     3   */
     4  
     5  package commandprocessor
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"net/http"
    14  	"strconv"
    15  	"time"
    16  
    17  	"github.com/voedger/voedger/pkg/goutils/iterate"
    18  	"github.com/voedger/voedger/pkg/goutils/logger"
    19  	"golang.org/x/exp/maps"
    20  
    21  	"github.com/voedger/voedger/pkg/appdef"
    22  	"github.com/voedger/voedger/pkg/appparts"
    23  	"github.com/voedger/voedger/pkg/iauthnz"
    24  	"github.com/voedger/voedger/pkg/in10n"
    25  	"github.com/voedger/voedger/pkg/istructs"
    26  	"github.com/voedger/voedger/pkg/istructsmem"
    27  	"github.com/voedger/voedger/pkg/pipeline"
    28  	"github.com/voedger/voedger/pkg/processors"
    29  	"github.com/voedger/voedger/pkg/projectors"
    30  	"github.com/voedger/voedger/pkg/sys/authnz"
    31  	"github.com/voedger/voedger/pkg/sys/blobber"
    32  	"github.com/voedger/voedger/pkg/sys/builtin"
    33  	workspacemgmt "github.com/voedger/voedger/pkg/sys/workspace"
    34  	coreutils "github.com/voedger/voedger/pkg/utils"
    35  	ibus "github.com/voedger/voedger/staging/src/github.com/untillpro/airs-ibus"
    36  )
    37  
    38  func (cm *implICommandMessage) Body() []byte                      { return cm.body }
    39  func (cm *implICommandMessage) AppQName() istructs.AppQName       { return cm.appQName }
    40  func (cm *implICommandMessage) WSID() istructs.WSID               { return cm.wsid }
    41  func (cm *implICommandMessage) Sender() ibus.ISender              { return cm.sender }
    42  func (cm *implICommandMessage) PartitionID() istructs.PartitionID { return cm.partitionID }
    43  func (cm *implICommandMessage) RequestCtx() context.Context       { return cm.requestCtx }
    44  func (cm *implICommandMessage) QName() appdef.QName               { return cm.qName }
    45  func (cm *implICommandMessage) Token() string                     { return cm.token }
    46  func (cm *implICommandMessage) Host() string                      { return cm.host }
    47  
    48  func NewCommandMessage(requestCtx context.Context, body []byte, appQName istructs.AppQName, wsid istructs.WSID, sender ibus.ISender,
    49  	partitionID istructs.PartitionID, qName appdef.QName, token string, host string) ICommandMessage {
    50  	return &implICommandMessage{
    51  		body:        body,
    52  		appQName:    appQName,
    53  		wsid:        wsid,
    54  		sender:      sender,
    55  		partitionID: partitionID,
    56  		requestCtx:  requestCtx,
    57  		qName:       qName,
    58  		token:       token,
    59  		host:        host,
    60  	}
    61  }
    62  
    63  // used in projectors.newSyncBranch()
    64  func (c *cmdWorkpiece) AppPartition() appparts.IAppPartition {
    65  	return c.appPart
    66  }
    67  
    68  // need for sync projectors which are using wsid.GetNextWSID()
    69  func (c *cmdWorkpiece) Context() context.Context {
    70  	return c.cmdMes.RequestCtx()
    71  }
    72  
    73  // used in projectors.NewSyncActualizerFactoryFactory
    74  func (c *cmdWorkpiece) Event() istructs.IPLogEvent {
    75  	return c.pLogEvent
    76  }
    77  
    78  // borrows app partition for command
    79  func (c *cmdWorkpiece) borrow() (err error) {
    80  	if c.appPart, err = c.appParts.Borrow(c.cmdMes.AppQName(), c.cmdMes.PartitionID(), appparts.ProcessorKind_Command); err != nil {
    81  		if errors.Is(err, appparts.ErrNotFound) || errors.Is(err, appparts.ErrNotAvailableEngines) { // partition is not deployed yet -> ErrNotFound
    82  			return coreutils.NewHTTPError(http.StatusServiceUnavailable, err)
    83  		}
    84  		// notest
    85  		return err
    86  	}
    87  	c.appStructs = c.appPart.AppStructs()
    88  	return nil
    89  }
    90  
    91  // releases resources:
    92  //   - borrowed app partition
    93  //   - plog event
    94  func (c *cmdWorkpiece) release() {
    95  	if ev := c.pLogEvent; ev != nil {
    96  		c.pLogEvent = nil
    97  		ev.Release()
    98  	}
    99  	if ap := c.appPart; ap != nil {
   100  		c.appStructs = nil
   101  		c.appPart = nil
   102  		ap.Release()
   103  	}
   104  }
   105  
   106  func borrowAppPart(_ context.Context, work interface{}) error {
   107  	return work.(*cmdWorkpiece).borrow()
   108  }
   109  
   110  func (ap *appPartition) getWorkspace(wsid istructs.WSID) *workspace {
   111  	ws, ok := ap.workspaces[wsid]
   112  	if !ok {
   113  		ws = &workspace{
   114  			NextWLogOffset: istructs.FirstOffset,
   115  			idGenerator:    istructsmem.NewIDGenerator(),
   116  		}
   117  		ap.workspaces[wsid] = ws
   118  	}
   119  	return ws
   120  }
   121  
   122  func (cmdProc *cmdProc) getAppPartition(ctx context.Context, work interface{}) (err error) {
   123  	cmd := work.(*cmdWorkpiece)
   124  	ap, ok := cmdProc.appPartitions[cmd.cmdMes.AppQName()]
   125  	if !ok {
   126  		if ap, err = cmdProc.recovery(ctx, cmd); err != nil {
   127  			return fmt.Errorf("partition %d recovery failed: %w", cmdProc.pNumber, err)
   128  		}
   129  		cmdProc.appPartitions[cmd.cmdMes.AppQName()] = ap
   130  	}
   131  	cmdProc.appPartition = ap
   132  	return nil
   133  }
   134  
   135  func getIWorkspace(_ context.Context, work interface{}) (err error) {
   136  	cmd := work.(*cmdWorkpiece)
   137  	if cmd.cmdMes.QName() != workspacemgmt.QNameCommandCreateWorkspace {
   138  		cmd.iWorkspace = cmd.appStructs.AppDef().WorkspaceByDescriptor(cmd.wsDesc.AsQName(authnz.Field_WSKind))
   139  	}
   140  	return nil
   141  }
   142  
   143  func getICommand(_ context.Context, work interface{}) (err error) {
   144  	cmd := work.(*cmdWorkpiece)
   145  	var cmdType appdef.IType
   146  	if cmd.iWorkspace == nil {
   147  		// DummyWS or c.sys.CreateWorkspace
   148  		cmdType = cmd.appStructs.AppDef().Type(cmd.cmdMes.QName())
   149  	} else {
   150  		if cmdType = cmd.iWorkspace.Type(cmd.cmdMes.QName()); cmdType.Kind() == appdef.TypeKind_null {
   151  			return fmt.Errorf("command %s does not exist in workspace %s", cmd.cmdMes.QName(), cmd.iWorkspace.QName())
   152  		}
   153  	}
   154  	ok := false
   155  	cmd.iCommand, ok = cmdType.(appdef.ICommand)
   156  	if !ok {
   157  		return fmt.Errorf("%s is not a command", cmd.cmdMes.QName())
   158  	}
   159  	return nil
   160  }
   161  
   162  func (cmdProc *cmdProc) getCmdResultBuilder(_ context.Context, work interface{}) (err error) {
   163  	cmd := work.(*cmdWorkpiece)
   164  	cmdResultType := cmd.iCommand.Result()
   165  	if cmdResultType != nil {
   166  		cmd.cmdResultBuilder = cmd.appStructs.ObjectBuilder(cmdResultType.QName())
   167  	}
   168  	return nil
   169  }
   170  
   171  func (cmdProc *cmdProc) buildCommandArgs(_ context.Context, work interface{}) (err error) {
   172  	cmd := work.(*cmdWorkpiece)
   173  	hs := cmd.hostStateProvider.get(cmd.appStructs, cmd.cmdMes.WSID(), cmd.reb.CUDBuilder(),
   174  		cmd.principals, cmd.cmdMes.Token(), cmd.cmdResultBuilder, cmd.workspace.NextWLogOffset)
   175  	hs.ClearIntents()
   176  	cmd.eca = istructs.ExecCommandArgs{
   177  		CommandPrepareArgs: istructs.CommandPrepareArgs{
   178  			PrepareArgs: istructs.PrepareArgs{
   179  				ArgumentObject: cmd.argsObject,
   180  				WSID:           cmd.cmdMes.WSID(),
   181  				Workpiece:      work,
   182  				Workspace:      cmd.iWorkspace,
   183  			},
   184  			ArgumentUnloggedObject: cmd.unloggedArgsObject,
   185  		},
   186  		State:   hs,
   187  		Intents: hs,
   188  	}
   189  	return
   190  }
   191  
   192  func updateIDGeneratorFromO(root istructs.IObject, types appdef.IWithTypes, idGen istructs.IIDGenerator) {
   193  	// new IDs only here because update is not allowed for ODocs in Args
   194  	idGen.UpdateOnSync(root.AsRecordID(appdef.SystemField_ID), types.Type(root.QName()))
   195  	root.Containers(func(container string) {
   196  		// order of containers here is the order in the schema
   197  		// but order in the request could be different
   198  		// that is not a problem because for ODocs/ORecords ID generator will bump next ID only if syncID is actually next
   199  		root.Children(container, func(c istructs.IObject) {
   200  			updateIDGeneratorFromO(c, types, idGen)
   201  		})
   202  	})
   203  }
   204  
   205  func (cmdProc *cmdProc) recovery(ctx context.Context, cmd *cmdWorkpiece) (*appPartition, error) {
   206  	ap := &appPartition{
   207  		workspaces:     map[istructs.WSID]*workspace{},
   208  		nextPLogOffset: istructs.FirstOffset,
   209  	}
   210  	var lastPLogEvent istructs.IPLogEvent
   211  	cb := func(plogOffset istructs.Offset, event istructs.IPLogEvent) (err error) {
   212  		ws := ap.getWorkspace(event.Workspace())
   213  
   214  		event.CUDs(func(rec istructs.ICUDRow) {
   215  			if rec.IsNew() {
   216  				t := cmd.appStructs.AppDef().Type(rec.QName())
   217  				ws.idGenerator.UpdateOnSync(rec.ID(), t)
   218  			}
   219  		})
   220  		ao := event.ArgumentObject()
   221  		if cmd.appStructs.AppDef().Type(ao.QName()).Kind() == appdef.TypeKind_ODoc {
   222  			updateIDGeneratorFromO(ao, cmd.appStructs.AppDef(), ws.idGenerator)
   223  		}
   224  		ws.NextWLogOffset = event.WLogOffset() + 1
   225  		ap.nextPLogOffset = plogOffset + 1
   226  		if lastPLogEvent != nil {
   227  			lastPLogEvent.Release() // TODO: eliminate if there will be a better solution, see https://github.com/voedger/voedger/issues/1348
   228  		}
   229  		lastPLogEvent = event
   230  		return nil
   231  	}
   232  
   233  	if err := cmd.appStructs.Events().ReadPLog(ctx, cmdProc.pNumber, istructs.FirstOffset, istructs.ReadToTheEnd, cb); err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	if lastPLogEvent != nil {
   238  		// re-apply the last event
   239  		cmd.pLogEvent = lastPLogEvent
   240  		cmd.workspace = ap.getWorkspace(lastPLogEvent.Workspace())
   241  		cmd.workspace.NextWLogOffset-- // cmdProc.storeOp will bump it
   242  		if err := cmdProc.storeOp.DoSync(ctx, cmd); err != nil {
   243  			return nil, err
   244  		}
   245  		cmd.pLogEvent = nil
   246  		cmd.workspace = nil
   247  		lastPLogEvent.Release() // TODO: eliminate if there will be a better solution, see https://github.com/voedger/voedger/issues/1348
   248  	}
   249  
   250  	worskapcesJSON, err := json.Marshal(ap.workspaces)
   251  	if err != nil {
   252  		// notest
   253  		return nil, err
   254  	}
   255  	logger.Info(fmt.Sprintf(`app "%s" partition %d recovered: nextPLogOffset %d, workspaces: %s`, cmd.cmdMes.AppQName(), cmdProc.pNumber, ap.nextPLogOffset, string(worskapcesJSON)))
   256  	return ap, nil
   257  }
   258  
   259  func getIDGenerator(_ context.Context, work interface{}) (err error) {
   260  	cmd := work.(*cmdWorkpiece)
   261  	cmd.idGenerator = &implIDGenerator{
   262  		IIDGenerator: cmd.workspace.idGenerator,
   263  		generatedIDs: map[istructs.RecordID]istructs.RecordID{},
   264  	}
   265  	return nil
   266  }
   267  
   268  func (cmdProc *cmdProc) putPLog(_ context.Context, work interface{}) (err error) {
   269  	cmd := work.(*cmdWorkpiece)
   270  	if cmd.pLogEvent, err = cmd.appStructs.Events().PutPlog(cmd.rawEvent, nil, cmd.idGenerator); err != nil {
   271  		cmd.appPartitionRestartScheduled = true
   272  	} else {
   273  		cmdProc.appPartition.nextPLogOffset++
   274  	}
   275  	return
   276  }
   277  
   278  func getWSDesc(_ context.Context, work interface{}) (err error) {
   279  	cmd := work.(*cmdWorkpiece)
   280  	cmd.wsDesc, err = cmd.appStructs.Records().GetSingleton(cmd.cmdMes.WSID(), authnz.QNameCDocWorkspaceDescriptor)
   281  	return err
   282  }
   283  
   284  func checkWSInitialized(_ context.Context, work interface{}) (err error) {
   285  	cmd := work.(*cmdWorkpiece)
   286  	wsDesc := work.(*cmdWorkpiece).wsDesc
   287  	cmdQName := cmd.cmdMes.QName()
   288  	if cmdQName == workspacemgmt.QNameCommandCreateWorkspace ||
   289  		cmdQName == workspacemgmt.QNameCommandCreateWorkspaceID || // happens on creating a child of an another workspace
   290  		cmdQName == builtin.QNameCommandInit {
   291  		return nil
   292  	}
   293  	if wsDesc.QName() != appdef.NullQName {
   294  		if cmdQName == blobber.QNameCommandUploadBLOBHelper {
   295  			return nil
   296  		}
   297  		if wsDesc.AsInt64(workspacemgmt.Field_InitCompletedAtMs) > 0 && len(wsDesc.AsString(workspacemgmt.Field_InitError)) == 0 {
   298  			cmd.wsInitialized = true
   299  			return nil
   300  		}
   301  		if cmdQName == istructs.QNameCommandCUD {
   302  			if iauthnz.IsSystemPrincipal(cmd.principals, cmd.cmdMes.WSID()) {
   303  				// system -> allow any CUD to upload template, see https://github.com/voedger/voedger/issues/648
   304  				return nil
   305  			}
   306  		}
   307  	}
   308  	return processors.ErrWSNotInited
   309  }
   310  
   311  func checkWSActive(_ context.Context, work interface{}) (err error) {
   312  	cmd := work.(*cmdWorkpiece)
   313  	if iauthnz.IsSystemPrincipal(cmd.principals, cmd.cmdMes.WSID()) {
   314  		// system -> allow to work in any case
   315  		return nil
   316  	}
   317  	if cmd.wsDesc.QName() == appdef.NullQName {
   318  		return nil
   319  	}
   320  	if cmd.wsDesc.AsInt32(authnz.Field_Status) == int32(authnz.WorkspaceStatus_Active) {
   321  		return nil
   322  	}
   323  	return processors.ErrWSInactive
   324  }
   325  
   326  func limitCallRate(_ context.Context, work interface{}) (err error) {
   327  	cmd := work.(*cmdWorkpiece)
   328  	if cmd.appStructs.IsFunctionRateLimitsExceeded(cmd.cmdMes.QName(), cmd.cmdMes.WSID()) {
   329  		return coreutils.NewHTTPErrorf(http.StatusTooManyRequests)
   330  	}
   331  	return nil
   332  }
   333  
   334  func (cmdProc *cmdProc) authenticate(_ context.Context, work interface{}) (err error) {
   335  	cmd := work.(*cmdWorkpiece)
   336  	req := iauthnz.AuthnRequest{
   337  		Host:        cmd.cmdMes.Host(),
   338  		RequestWSID: cmd.cmdMes.WSID(),
   339  		Token:       cmd.cmdMes.Token(),
   340  	}
   341  	if cmd.principals, cmd.principalPayload, err = cmdProc.authenticator.Authenticate(cmd.cmdMes.RequestCtx(), cmd.appStructs,
   342  		cmd.appStructs.AppTokens(), req); err != nil {
   343  		return coreutils.NewHTTPError(http.StatusUnauthorized, err)
   344  	}
   345  	return
   346  }
   347  
   348  func (cmdProc *cmdProc) authorizeRequest(_ context.Context, work interface{}) (err error) {
   349  	cmd := work.(*cmdWorkpiece)
   350  	req := iauthnz.AuthzRequest{
   351  		OperationKind: iauthnz.OperationKind_EXECUTE,
   352  		Resource:      cmd.cmdMes.QName(),
   353  	}
   354  	ok, err := cmdProc.authorizer.Authorize(cmd.appStructs, cmd.principals, req)
   355  	if err != nil {
   356  		return err
   357  	}
   358  	if !ok {
   359  		return coreutils.NewHTTPErrorf(http.StatusForbidden)
   360  	}
   361  	return nil
   362  }
   363  
   364  func getResources(_ context.Context, work interface{}) (err error) {
   365  	cmd := work.(*cmdWorkpiece)
   366  	cmd.resources = cmd.appStructs.Resources()
   367  	return nil
   368  }
   369  
   370  func getExec(_ context.Context, work interface{}) (err error) {
   371  	cmd := work.(*cmdWorkpiece)
   372  	iResource := cmd.resources.QueryResource(cmd.cmdMes.QName())
   373  	iCommandFunc := iResource.(istructs.ICommandFunction)
   374  	cmd.cmdExec = iCommandFunc.Exec
   375  	return nil
   376  }
   377  
   378  func unmarshalRequestBody(_ context.Context, work interface{}) (err error) {
   379  	cmd := work.(*cmdWorkpiece)
   380  	if cmd.iCommand.Param() != nil && cmd.iCommand.Param().QName() == istructs.QNameRaw {
   381  		cmd.requestData["args"] = map[string]interface{}{
   382  			processors.Field_RawObject_Body: string(cmd.cmdMes.Body()),
   383  		}
   384  	} else if err = json.Unmarshal(cmd.cmdMes.Body(), &cmd.requestData); err != nil {
   385  		err = fmt.Errorf("failed to unmarshal request body: %w", err)
   386  	}
   387  	return
   388  }
   389  
   390  func (cmdProc *cmdProc) getWorkspace(_ context.Context, work interface{}) (err error) {
   391  	cmd := work.(*cmdWorkpiece)
   392  	cmd.workspace = cmdProc.appPartition.getWorkspace(cmd.cmdMes.WSID())
   393  	return nil
   394  }
   395  
   396  func (cmdProc *cmdProc) getRawEventBuilder(_ context.Context, work interface{}) (err error) {
   397  	cmd := work.(*cmdWorkpiece)
   398  	grebp := istructs.GenericRawEventBuilderParams{
   399  		HandlingPartition: cmd.cmdMes.PartitionID(),
   400  		Workspace:         cmd.cmdMes.WSID(),
   401  		QName:             cmd.cmdMes.QName(),
   402  		RegisteredAt:      istructs.UnixMilli(cmdProc.now().UnixMilli()),
   403  		PLogOffset:        cmdProc.appPartition.nextPLogOffset,
   404  		WLogOffset:        cmd.workspace.NextWLogOffset,
   405  	}
   406  
   407  	switch cmd.cmdMes.QName() {
   408  	case builtin.QNameCommandInit: // nolint, kept to not to break existing events only
   409  		cmd.reb = cmd.appStructs.Events().GetSyncRawEventBuilder(
   410  			istructs.SyncRawEventBuilderParams{
   411  				SyncedAt:                     istructs.UnixMilli(cmdProc.now().UnixMilli()),
   412  				GenericRawEventBuilderParams: grebp,
   413  			},
   414  		)
   415  	default:
   416  		cmd.reb = cmd.appStructs.Events().GetNewRawEventBuilder(
   417  			istructs.NewRawEventBuilderParams{
   418  				GenericRawEventBuilderParams: grebp,
   419  			},
   420  		)
   421  	}
   422  	return nil
   423  }
   424  
   425  func getArgsObject(_ context.Context, work interface{}) (err error) {
   426  	cmd := work.(*cmdWorkpiece)
   427  	if cmd.iCommand.Param() == nil {
   428  		return nil
   429  	}
   430  	aob := cmd.reb.ArgumentObjectBuilder()
   431  	if argsIntf, exists := cmd.requestData["args"]; exists {
   432  		args, ok := argsIntf.(map[string]interface{})
   433  		if !ok {
   434  			return errors.New(`"args" field must be an object`)
   435  		}
   436  		aob.FillFromJSON(args)
   437  	}
   438  	if cmd.argsObject, err = aob.Build(); err != nil {
   439  		err = fmt.Errorf("argument object build failed: %w", err)
   440  	}
   441  	return
   442  }
   443  
   444  func getUnloggedArgsObject(_ context.Context, work interface{}) (err error) {
   445  	cmd := work.(*cmdWorkpiece)
   446  	if cmd.iCommand.UnloggedParam() == nil {
   447  		return nil
   448  	}
   449  	auob := cmd.reb.ArgumentUnloggedObjectBuilder()
   450  	if unloggedArgsIntf, exists := cmd.requestData["unloggedArgs"]; exists {
   451  		unloggedArgs, ok := unloggedArgsIntf.(map[string]interface{})
   452  		if !ok {
   453  			return errors.New(`"unloggedArgs" field must be an object`)
   454  		}
   455  		auob.FillFromJSON(unloggedArgs)
   456  	}
   457  	if cmd.unloggedArgsObject, err = auob.Build(); err != nil {
   458  		err = fmt.Errorf("unlogged argument object build failed: %w", err)
   459  	}
   460  	return
   461  }
   462  
   463  func (xp xPath) Errorf(mes string, args ...interface{}) error {
   464  	return fmt.Errorf(string(xp)+": "+mes, args...)
   465  }
   466  
   467  func (xp xPath) Error(err error) error {
   468  	return xp.Errorf("%w", err)
   469  }
   470  
   471  func execCommand(_ context.Context, work interface{}) (err error) {
   472  	cmd := work.(*cmdWorkpiece)
   473  	begin := time.Now()
   474  	err = cmd.cmdExec(cmd.eca)
   475  	work.(*cmdWorkpiece).metrics.increase(ExecSeconds, time.Since(begin).Seconds())
   476  	return err
   477  }
   478  
   479  func buildRawEvent(_ context.Context, work interface{}) (err error) {
   480  	cmd := work.(*cmdWorkpiece)
   481  	cmd.rawEvent, err = cmd.reb.BuildRawEvent()
   482  	status := http.StatusBadRequest
   483  	if errors.Is(err, istructsmem.ErrRecordIDUniqueViolation) {
   484  		status = http.StatusConflict
   485  	}
   486  	err = coreutils.WrapSysError(err, status)
   487  	return
   488  }
   489  
   490  func validateCmdResult(ctx context.Context, work interface{}) (err error) {
   491  	cmd := work.(*cmdWorkpiece)
   492  	if cmd.cmdResultBuilder != nil {
   493  		cmdResult, err := cmd.cmdResultBuilder.Build()
   494  		if err != nil {
   495  			return err
   496  		}
   497  		cmd.cmdResult = cmdResult
   498  	}
   499  	return nil
   500  }
   501  
   502  func (cmdProc *cmdProc) eventValidators(ctx context.Context, work interface{}) (err error) {
   503  	cmd := work.(*cmdWorkpiece)
   504  	for _, appEventValidator := range cmd.appStructs.EventValidators() {
   505  		if err = appEventValidator(ctx, cmd.rawEvent, cmd.appStructs, cmd.cmdMes.WSID()); err != nil {
   506  			return coreutils.WrapSysError(err, http.StatusForbidden)
   507  		}
   508  	}
   509  	return nil
   510  }
   511  
   512  func (cmdProc *cmdProc) cudsValidators(ctx context.Context, work interface{}) (err error) {
   513  	cmd := work.(*cmdWorkpiece)
   514  	for _, appCUDValidator := range cmd.appStructs.CUDValidators() {
   515  		err = iterate.ForEachError(cmd.rawEvent.CUDs, func(rec istructs.ICUDRow) error {
   516  			if appCUDValidator.Match(rec, cmd.cmdMes.WSID(), cmd.cmdMes.QName()) {
   517  				if err := appCUDValidator.Validate(ctx, cmd.appStructs, rec, cmd.cmdMes.WSID(), cmd.cmdMes.QName()); err != nil {
   518  					return coreutils.WrapSysError(err, http.StatusForbidden)
   519  				}
   520  			}
   521  			return nil
   522  		})
   523  		if err != nil {
   524  			return err
   525  		}
   526  	}
   527  	return nil
   528  }
   529  
   530  func (cmdProc *cmdProc) validateCUDsQNames(ctx context.Context, work interface{}) (err error) {
   531  	cmd := work.(*cmdWorkpiece)
   532  	if cmd.iWorkspace == nil {
   533  		// dummy or c.sys.CreateWorkspace
   534  		return nil
   535  	}
   536  	return iterate.ForEachError(cmd.rawEvent.CUDs, func(cud istructs.ICUDRow) error {
   537  		if cmd.iWorkspace.Type(cud.QName()) == appdef.NullType {
   538  			return coreutils.NewHTTPErrorf(http.StatusBadRequest, fmt.Errorf("doc %s mentioned in resulting CUDs does not exist in the workspace %s",
   539  				cud.QName(), cmd.wsDesc.AsQName(authnz.Field_WSKind)))
   540  		}
   541  		return nil
   542  	})
   543  }
   544  
   545  func parseCUDs(_ context.Context, work interface{}) (err error) {
   546  	cmd := work.(*cmdWorkpiece)
   547  	cuds, _, err := cmd.requestData.AsObjects("cuds")
   548  	if err != nil {
   549  		return err
   550  	}
   551  	if len(cuds) > builtin.MaxCUDs {
   552  		return coreutils.NewHTTPErrorf(http.StatusBadRequest, "too many cuds: ", len(cuds), " is in the request, max is ", builtin.MaxCUDs)
   553  	}
   554  	for cudNumber, cudIntf := range cuds {
   555  		cudXPath := xPath("cuds[" + strconv.Itoa(cudNumber) + "]")
   556  		cudDataMap, ok := cudIntf.(map[string]interface{})
   557  		if !ok {
   558  			return cudXPath.Errorf("not an object")
   559  		}
   560  		cudData := coreutils.MapObject(cudDataMap)
   561  
   562  		parsedCUD := parsedCUD{}
   563  
   564  		parsedCUD.fields, ok, err = cudData.AsObject("fields")
   565  		if err != nil {
   566  			return cudXPath.Error(err)
   567  		}
   568  		if !ok {
   569  			return cudXPath.Errorf(`"fields" missing`)
   570  		}
   571  		// sys.ID внутри -> create, снаружи -> update
   572  		isCreate := false
   573  		if parsedCUD.id, isCreate, err = parsedCUD.fields.AsInt64(appdef.SystemField_ID); err != nil {
   574  			return cudXPath.Error(err)
   575  		}
   576  		if isCreate {
   577  			parsedCUD.opKind = iauthnz.OperationKind_INSERT
   578  			qNameStr, _, err := parsedCUD.fields.AsString(appdef.SystemField_QName)
   579  			if err != nil {
   580  				return cudXPath.Error(err)
   581  			}
   582  			if parsedCUD.qName, err = appdef.ParseQName(qNameStr); err != nil {
   583  				return cudXPath.Error(err)
   584  			}
   585  		} else {
   586  			parsedCUD.opKind = iauthnz.OperationKind_UPDATE
   587  			if parsedCUD.id, ok, err = cudData.AsInt64(appdef.SystemField_ID); err != nil {
   588  				return cudXPath.Error(err)
   589  			}
   590  			if !ok {
   591  				return cudXPath.Errorf(`"sys.ID" missing`)
   592  			}
   593  			if parsedCUD.existingRecord, err = cmd.appStructs.Records().Get(cmd.cmdMes.WSID(), true, istructs.RecordID(parsedCUD.id)); err != nil {
   594  				return
   595  			}
   596  			if parsedCUD.qName = parsedCUD.existingRecord.QName(); parsedCUD.qName == appdef.NullQName {
   597  				return coreutils.NewHTTPError(http.StatusNotFound, cudXPath.Errorf("record with queried id %d does not exist", parsedCUD.id))
   598  			}
   599  		}
   600  		opStr := "UPDATE"
   601  		if isCreate {
   602  			opStr = "INSERT"
   603  		}
   604  		parsedCUD.xPath = xPath(fmt.Sprintf("%s %s %s", cudXPath, opStr, parsedCUD.qName))
   605  
   606  		cmd.parsedCUDs = append(cmd.parsedCUDs, parsedCUD)
   607  	}
   608  	return err
   609  }
   610  
   611  func checkArgsRefIntegrity(_ context.Context, work interface{}) (err error) {
   612  	cmd := work.(*cmdWorkpiece)
   613  	if cmd.argsObject != nil {
   614  		if err = builtin.CheckRefIntegrity(cmd.argsObject, cmd.appStructs, cmd.cmdMes.WSID()); err != nil {
   615  			return err
   616  		}
   617  	}
   618  	if cmd.unloggedArgsObject != nil {
   619  		return builtin.CheckRefIntegrity(cmd.unloggedArgsObject, cmd.appStructs, cmd.cmdMes.WSID())
   620  	}
   621  	return nil
   622  }
   623  
   624  // not a validator due of https://github.com/voedger/voedger/issues/1125
   625  func checkIsActiveInCUDs(_ context.Context, work interface{}) (err error) {
   626  	cmd := work.(*cmdWorkpiece)
   627  	for _, cud := range cmd.parsedCUDs {
   628  		if cud.opKind != iauthnz.OperationKind_UPDATE {
   629  			continue
   630  		}
   631  		hasOnlySystemFields := true
   632  		sysIsActiveUpdating := false
   633  		isActiveAndOtherFieldsMixedOnUpdate := false
   634  		for fieldName := range cud.fields {
   635  			if !appdef.IsSysField(fieldName) {
   636  				hasOnlySystemFields = false
   637  			} else if fieldName == appdef.SystemField_IsActive {
   638  				sysIsActiveUpdating = true
   639  			}
   640  			if isActiveAndOtherFieldsMixedOnUpdate = sysIsActiveUpdating && !hasOnlySystemFields; isActiveAndOtherFieldsMixedOnUpdate {
   641  				break
   642  			}
   643  		}
   644  		if isActiveAndOtherFieldsMixedOnUpdate {
   645  			return coreutils.NewHTTPError(http.StatusForbidden, errors.New("updating other fields is not allowed if sys.IsActive is updating"))
   646  		}
   647  	}
   648  	return nil
   649  }
   650  
   651  func (cmdProc *cmdProc) authorizeCUDs(_ context.Context, work interface{}) (err error) {
   652  	cmd := work.(*cmdWorkpiece)
   653  	for _, parsedCUD := range cmd.parsedCUDs {
   654  		req := iauthnz.AuthzRequest{
   655  			OperationKind: parsedCUD.opKind,
   656  			Resource:      parsedCUD.qName,
   657  			Fields:        maps.Keys(parsedCUD.fields),
   658  		}
   659  		ok, err := cmdProc.authorizer.Authorize(cmd.appStructs, cmd.principals, req)
   660  		if err != nil {
   661  			return parsedCUD.xPath.Error(err)
   662  		}
   663  		if !ok {
   664  			return coreutils.NewHTTPError(http.StatusForbidden, parsedCUD.xPath.Errorf("operation forbidden"))
   665  		}
   666  	}
   667  	return
   668  }
   669  
   670  func (cmdProc *cmdProc) writeCUDs(_ context.Context, work interface{}) (err error) {
   671  	cmd := work.(*cmdWorkpiece)
   672  	for _, parsedCUD := range cmd.parsedCUDs {
   673  		var cud istructs.IRowWriter
   674  		if parsedCUD.opKind == iauthnz.OperationKind_INSERT {
   675  			cud = cmd.reb.CUDBuilder().Create(parsedCUD.qName)
   676  			cud.PutRecordID(appdef.SystemField_ID, istructs.RecordID(parsedCUD.id))
   677  		} else {
   678  			cud = cmd.reb.CUDBuilder().Update(parsedCUD.existingRecord)
   679  		}
   680  		if err := coreutils.MapToObject(parsedCUD.fields, cud); err != nil {
   681  			return parsedCUD.xPath.Error(err)
   682  		}
   683  	}
   684  	return nil
   685  }
   686  
   687  func (osp *wrongArgsCatcher) OnErr(err error, _ interface{}, _ pipeline.IWorkpieceContext) (newErr error) {
   688  	return coreutils.WrapSysError(err, http.StatusBadRequest)
   689  }
   690  
   691  func (cmdProc *cmdProc) n10n(_ context.Context, work interface{}) (err error) {
   692  	cmd := work.(*cmdWorkpiece)
   693  	cmdProc.n10nBroker.Update(in10n.ProjectionKey{
   694  		App:        cmd.cmdMes.AppQName(),
   695  		Projection: projectors.PLogUpdatesQName,
   696  		WS:         istructs.WSID(cmdProc.pNumber),
   697  	}, cmd.rawEvent.PLogOffset())
   698  	logger.Verbose("updated plog event on offset ", cmd.rawEvent.PLogOffset(), ", pnumber ", cmdProc.pNumber)
   699  	return nil
   700  }
   701  
   702  func sendResponse(cmd *cmdWorkpiece, handlingError error) {
   703  	if handlingError != nil {
   704  		cmd.metrics.increase(ErrorsTotal, 1.0)
   705  		//if error occurred somewhere in syncProjectors we have to measure elapsed time
   706  		if !cmd.syncProjectorsStart.IsZero() {
   707  			cmd.metrics.increase(ProjectorsSeconds, time.Since(cmd.syncProjectorsStart).Seconds())
   708  		}
   709  		coreutils.ReplyErr(cmd.cmdMes.Sender(), handlingError)
   710  		return
   711  	}
   712  	body := bytes.NewBufferString(fmt.Sprintf(`{"CurrentWLogOffset":%d`, cmd.pLogEvent.WLogOffset()))
   713  	if len(cmd.idGenerator.generatedIDs) > 0 {
   714  		body.WriteString(`,"NewIDs":{`)
   715  		for rawID, generatedID := range cmd.idGenerator.generatedIDs {
   716  			body.WriteString(fmt.Sprintf(`"%d":%d,`, rawID, generatedID))
   717  		}
   718  		body.Truncate(body.Len() - 1)
   719  		body.WriteString("}")
   720  		if logger.IsVerbose() {
   721  			logger.Verbose("generated IDs:", cmd.idGenerator.generatedIDs)
   722  		}
   723  	}
   724  	if cmd.cmdResult != nil {
   725  		cmdResult := coreutils.ObjectToMap(cmd.cmdResult, cmd.appStructs.AppDef())
   726  		cmdResultBytes, err := json.Marshal(cmdResult)
   727  		if err != nil {
   728  			// notest
   729  			logger.Error("failed to marshal response: " + err.Error())
   730  			return
   731  		}
   732  		body.WriteString(`,"Result":`)
   733  		body.Write(cmdResultBytes)
   734  	}
   735  	body.WriteString("}")
   736  	coreutils.ReplyJSON(cmd.cmdMes.Sender(), http.StatusOK, body.String())
   737  }
   738  
   739  func (idGen *implIDGenerator) NextID(rawID istructs.RecordID, t appdef.IType) (storageID istructs.RecordID, err error) {
   740  	storageID, err = idGen.IIDGenerator.NextID(rawID, t)
   741  	idGen.generatedIDs[rawID] = storageID
   742  	return
   743  }