diff --git a/locale/en/corteza-server/compose/record.yaml b/locale/en/corteza-server/compose/record.yaml index 628f7e21d..79a80d649 100644 --- a/locale/en/corteza-server/compose/record.yaml +++ b/locale/en/corteza-server/compose/record.yaml @@ -22,3 +22,4 @@ errors: staleData: stale data unknownBulkOperation: unknown bulk operation {bulkOperation} valueInput: invalid record value input + maxRecordsReached: maximum number of records per namespace reached diff --git a/server/.env.example b/server/.env.example index d34888a1a..7f7089132 100644 --- a/server/.env.example +++ b/server/.env.example @@ -33,7 +33,7 @@ ############################################################################### # Allow insecure (invalid, expired TLS/SSL certificates) connections. -# +# # [IMPORTANT] # ==== # We strongly recommend keeping this value set to false except for local development or demos. @@ -203,7 +203,7 @@ ############################################################################### # Password for the web console endpoint. When running in dev environment, password is not required. -# +# # Corteza intentionally sets default password to random chars to prevent security incidents. # Type: string # Default: @@ -290,7 +290,7 @@ # Email sending # # Configure your local SMTP server or use one of the available providers. -# +# # These values are copied to settings when the server starts and can be managed from the administration console. # We recommend you remove these values after they are copied to settings. # If server detects difference between these options and settings, it shows a warning in the log on server start. @@ -430,7 +430,7 @@ ############################################################################### # Password security allows you to disable constraints to which passwords must conform to. -# +# # [CAUTION] # ==== # Disabling password security can be useful for development environments as it removes the need for complex passwords. @@ -442,12 +442,12 @@ ############################################################################### # Algoritm to be use for JWT signature. -# +# # Supported valus: # - HS256, HS384, HS512 # - PS256, PS384, PS512, # - RS256, RS384, RS512 -# +# # Provide shared secret string for HS256, HS384, HS512 and full private key or path to the file PS* and RS* algorithms. # Type: string # Default: HS512 @@ -456,7 +456,7 @@ ############################################################################### # Secret used for signing JWT tokens. # Value is used only when HS256, HS384 or HS512 algorithm is used. -# +# # [IMPORTANT] # ==== # If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables. @@ -480,7 +480,7 @@ ############################################################################### # Lifetime of the refresh token. Should be much longer than lifetime of the access token. -# +# # Refresh tokens are used to exchange expired access tokens with new ones. # Type: time.Duration # Default: 72h @@ -488,7 +488,7 @@ ############################################################################### # Redirect URL to be sent with OAuth2 authentication request to provider -# +# # `provider` placeholder is replaced with the actual value when used. # Type: string # Default: @@ -496,7 +496,7 @@ ############################################################################### # Secret used for securing cookies -# +# # [IMPORTANT] # ==== # If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables. @@ -539,21 +539,21 @@ ############################################################################### # Maximum time user is allowed to stay idle when logged in without "remember-me" option and before session is expired. -# +# # Recomended value is between an hour and a day. -# +# # [IMPORTANT] # ==== # This affects only profile (/auth) pages. Using applications (admin, compose, ...) does not prolong the session. # ==== -# +# # Type: time.Duration # Default: 24h # AUTH_SESSION_LIFETIME=24h ############################################################################### # Duration of the session in /auth lasts when user logs-in with "remember-me" option. -# +# # If set to 0, "remember-me" option is removed. # Type: time.Duration # Default: 8640h @@ -580,7 +580,7 @@ ############################################################################### # Secret used for securing CSRF protection -# +# # [IMPORTANT] # ==== # If secret is not set, system auto-generates one from DB_DSN and HOSTNAME environment variables. @@ -610,19 +610,19 @@ ############################################################################### # Handle for OAuth2 client used for automatic redirect from /auth/oauth2/go endpoint. -# +# # This simplifies configuration for OAuth2 flow for Corteza Web applications as it removes # the need to suply redirection URL and client ID (oauth2/go endpoint does that internally) -# +# # Type: string # Default: corteza-webapp # AUTH_DEFAULT_CLIENT=corteza-webapp ############################################################################### # Path to js, css, images and template source files -# +# # When corteza starts, if path exists it tries to load template files from it. -# +# # When empty path is set (default value), embedded files are used. # Type: string # Default: @@ -631,7 +631,7 @@ ############################################################################### # When enabled, corteza reloads template before every execution. # Enable this for debugging or when developing auth templates. -# +# # Should be disabled in production where templates do not change between server restarts. # Type: bool # Default: @@ -640,7 +640,7 @@ ############################################################################### # When set, Corteza creates one or more users with the configured values using provided email as a password. # It skips existing (email, handle). All new users are assigned to all bypass roles. -# +# # When set in production, Corteza stops and reports an error # Type: string # Default: @@ -810,6 +810,12 @@ # Default: # LIMIT_SYSTEM_USERS= +############################################################################### +# Maximum number of records per namespace +# Type: int +# Default: +# LIMIT_RECORD_COUNT_PER_NAMESPACE= + ############################################################################### ############################################################################### # locale @@ -818,16 +824,16 @@ ############################################################################### # List of compa delimited languages (language tags) to enable. # In case when an enabled language can not be loaded, error is logged. -# +# # When loading language configurations (config.xml) from the configured path(s). -# +# # Type: string # Default: en # LOCALE_LANGUAGES=en ############################################################################### # One or more paths to locale config and translation files, separated by colon -# +# # When with LOCALE_DEVELOPMENT_MODE=true, default value for path is ../../locale # Type: string # Default: @@ -837,7 +843,7 @@ # Name of the query string parameter used to pass the language tag (it overrides Accept-Language header). # Set it to empty string to disable detection from the query string. # This parameter is ignored if only one language is enabled -# +# # Type: string # Default: lng # LOCALE_QUERY_STRING_PARAM=lng @@ -868,9 +874,9 @@ ############################################################################### # Disables json format for logging and enables more human-readable output with colors. -# +# # Disable for production. -# +# # Type: bool # Default: # LOG_DEBUG= @@ -878,11 +884,11 @@ ############################################################################### # Minimum logging level. If set to "warn", # Levels warn, error, dpanic panic and fatal will be logged. -# +# # Recommended value for production: warn -# +# # Possible values: debug, info, warn, error, dpanic, panic, fatal -# +# # Type: string # Default: warn # LOG_LEVEL=warn @@ -890,25 +896,25 @@ ############################################################################### # Log filtering rules by level and name (log-level:log-namespace). # Please note that level (LOG_LEVEL) is applied before filter and it affects the final output! -# +# # Leave unset for production. -# +# # Example: # `warn+:* *:auth,workflow.*` # Log warnings, errors, panic, fatals. Everything from auth and workflow is logged. -# -# +# +# # See more examples and documentation here: https://github.com/moul/zapfilter -# +# # Type: string # Default: # LOG_FILTER= ############################################################################### # Set to true to see where the logging was called from. -# +# # Disable for production. -# +# # Type: bool # Default: # LOG_INCLUDE_CALLER= @@ -916,9 +922,9 @@ ############################################################################### # Include stack-trace when logging at a specified level or below. # Disable for production. -# +# # Possible values: debug, info, warn, error, dpanic, panic, fatal -# +# # Type: string # Default: dpanic # LOG_STACKTRACE_LEVEL=dpanic @@ -1013,11 +1019,11 @@ # # Provisioning allows you to configure a {PRODUCT_NAME} instance when deployed. # It occurs automatically after the {PRODUCT_NAME} server starts. -# +# # [IMPORTANT] # ==== # We recommend you to keep provisioning enabled as it simplifies version updates by updating the database and updating settings. -# +# # If you're doing local development or some debugging, you can disable this. # ==== # @@ -1043,7 +1049,7 @@ # ==== # These parameters help in the development and testing process. # When you are deploying to production, these should be disabled to improve performance and reduce storage usage. -# +# # You should configure external services such as Sentry or ELK to keep track of logs and error reports. # ==== # @@ -1149,7 +1155,7 @@ # Delay system startup # # You can configure these options to defer API execution until another external (HTTP) service is up and running. -# +# # [ TIP ] # ==== # Delaying API execution can come in handy in complex setups where execution order is important. @@ -1173,7 +1179,7 @@ ############################################################################### # Space delimited list of hosts and/or URLs to probe. # Host format: `host` or `host:443` (port will default to 80). -# +# # [NOTE] # ==== # Services are probed in parallel. diff --git a/server/app/boot_levels.go b/server/app/boot_levels.go index 58f99793c..0da12578a 100644 --- a/server/app/boot_levels.go +++ b/server/app/boot_levels.go @@ -1,53 +1,53 @@ package app import ( - "context" - "crypto/tls" - "fmt" - "net/url" - "os" - "regexp" - "strings" - "time" + "context" + "crypto/tls" + "fmt" + "net/url" + "os" + "regexp" + "strings" + "time" - authService "github.com/cortezaproject/corteza/server/auth" - "github.com/cortezaproject/corteza/server/auth/saml" - authSettings "github.com/cortezaproject/corteza/server/auth/settings" - autService "github.com/cortezaproject/corteza/server/automation/service" - cmpService "github.com/cortezaproject/corteza/server/compose/service" - cmpEvent "github.com/cortezaproject/corteza/server/compose/service/event" - discoveryService "github.com/cortezaproject/corteza/server/discovery/service" - fedService "github.com/cortezaproject/corteza/server/federation/service" - "github.com/cortezaproject/corteza/server/pkg/actionlog" - "github.com/cortezaproject/corteza/server/pkg/apigw" - apigwTypes "github.com/cortezaproject/corteza/server/pkg/apigw/types" - "github.com/cortezaproject/corteza/server/pkg/auth" - "github.com/cortezaproject/corteza/server/pkg/corredor" - "github.com/cortezaproject/corteza/server/pkg/eventbus" - "github.com/cortezaproject/corteza/server/pkg/healthcheck" - "github.com/cortezaproject/corteza/server/pkg/http" - "github.com/cortezaproject/corteza/server/pkg/id" - "github.com/cortezaproject/corteza/server/pkg/locale" - "github.com/cortezaproject/corteza/server/pkg/logger" - "github.com/cortezaproject/corteza/server/pkg/mail" - "github.com/cortezaproject/corteza/server/pkg/messagebus" - "github.com/cortezaproject/corteza/server/pkg/monitor" - "github.com/cortezaproject/corteza/server/pkg/options" - "github.com/cortezaproject/corteza/server/pkg/provision" - "github.com/cortezaproject/corteza/server/pkg/rbac" - "github.com/cortezaproject/corteza/server/pkg/scheduler" - "github.com/cortezaproject/corteza/server/pkg/sentry" - "github.com/cortezaproject/corteza/server/pkg/valuestore" - "github.com/cortezaproject/corteza/server/pkg/version" - "github.com/cortezaproject/corteza/server/pkg/websocket" - "github.com/cortezaproject/corteza/server/store" - "github.com/cortezaproject/corteza/server/system/service" - sysService "github.com/cortezaproject/corteza/server/system/service" - sysEvent "github.com/cortezaproject/corteza/server/system/service/event" - "github.com/cortezaproject/corteza/server/system/types" - "github.com/lestrrat-go/jwx/jwt" - "go.uber.org/zap" - gomail "gopkg.in/mail.v2" + authService "github.com/cortezaproject/corteza/server/auth" + "github.com/cortezaproject/corteza/server/auth/saml" + authSettings "github.com/cortezaproject/corteza/server/auth/settings" + autService "github.com/cortezaproject/corteza/server/automation/service" + cmpService "github.com/cortezaproject/corteza/server/compose/service" + cmpEvent "github.com/cortezaproject/corteza/server/compose/service/event" + discoveryService "github.com/cortezaproject/corteza/server/discovery/service" + fedService "github.com/cortezaproject/corteza/server/federation/service" + "github.com/cortezaproject/corteza/server/pkg/actionlog" + "github.com/cortezaproject/corteza/server/pkg/apigw" + apigwTypes "github.com/cortezaproject/corteza/server/pkg/apigw/types" + "github.com/cortezaproject/corteza/server/pkg/auth" + "github.com/cortezaproject/corteza/server/pkg/corredor" + "github.com/cortezaproject/corteza/server/pkg/eventbus" + "github.com/cortezaproject/corteza/server/pkg/healthcheck" + "github.com/cortezaproject/corteza/server/pkg/http" + "github.com/cortezaproject/corteza/server/pkg/id" + "github.com/cortezaproject/corteza/server/pkg/locale" + "github.com/cortezaproject/corteza/server/pkg/logger" + "github.com/cortezaproject/corteza/server/pkg/mail" + "github.com/cortezaproject/corteza/server/pkg/messagebus" + "github.com/cortezaproject/corteza/server/pkg/monitor" + "github.com/cortezaproject/corteza/server/pkg/options" + "github.com/cortezaproject/corteza/server/pkg/provision" + "github.com/cortezaproject/corteza/server/pkg/rbac" + "github.com/cortezaproject/corteza/server/pkg/scheduler" + "github.com/cortezaproject/corteza/server/pkg/sentry" + "github.com/cortezaproject/corteza/server/pkg/valuestore" + "github.com/cortezaproject/corteza/server/pkg/version" + "github.com/cortezaproject/corteza/server/pkg/websocket" + "github.com/cortezaproject/corteza/server/store" + "github.com/cortezaproject/corteza/server/system/service" + sysService "github.com/cortezaproject/corteza/server/system/service" + sysEvent "github.com/cortezaproject/corteza/server/system/service/event" + "github.com/cortezaproject/corteza/server/system/types" + "github.com/lestrrat-go/jwx/jwt" + "go.uber.org/zap" + gomail "gopkg.in/mail.v2" ) const ( @@ -416,6 +416,7 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { ActionLog: app.Opt.ActionLog, Discovery: app.Opt.Discovery, Storage: app.Opt.ObjStore, + Limit: app.Opt.Limit, UserFinder: sysService.DefaultUser, SchemaAltManager: sysService.DefaultDalSchemaAlteration, }) @@ -562,9 +563,9 @@ func (app *CortezaApp) Activate(ctx context.Context) (err error) { app.AuthService.Watch(ctx) - updateSassInstallSettings(ctx, sysService.DefaultStylesheet.SassInstalled(), app.Log) + updateSassInstallSettings(ctx, sysService.DefaultStylesheet.SassInstalled(), app.Log) //Generate CSS for webapps - if err = sysService.DefaultStylesheet.GenerateCSS(sysService.CurrentSettings, app.Opt.Webapp.ScssDirPath, app.Log); err != nil { + if err = sysService.DefaultStylesheet.GenerateCSS(sysService.CurrentSettings, app.Opt.Webapp.ScssDirPath, app.Log); err != nil { return fmt.Errorf("could not generate css for webapps: %w", err) } diff --git a/server/app/options/limit.cue b/server/app/options/limit.cue index 38293915b..1ea4689e7 100644 --- a/server/app/options/limit.cue +++ b/server/app/options/limit.cue @@ -11,6 +11,10 @@ limit: schema.#optionsGroup & { type: "int" description: "Maximum number of valid (not deleted, not suspended) users" } + record_count_per_namespace: { + type: "int" + description: "Maximum number of records per namespace" + } } title: "Limits" } diff --git a/server/app/servers.go b/server/app/servers.go index fe2b903cd..81d1e4a9c 100644 --- a/server/app/servers.go +++ b/server/app/servers.go @@ -94,7 +94,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { } if app.Opt.Federation.Enabled { - r.Route("/federation", federationRest.MountRoutes()) + r.Route("/federation", federationRest.MountRoutes(app.Opt.Limit)) } var fullpathDocs = options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl, "docs") diff --git a/server/compose/dalutils/records.go b/server/compose/dalutils/records.go index 680bcec8a..61e74537b 100644 --- a/server/compose/dalutils/records.go +++ b/server/compose/dalutils/records.go @@ -3,7 +3,6 @@ package dalutils import ( "context" "fmt" - "github.com/cortezaproject/corteza/server/compose/types" "github.com/cortezaproject/corteza/server/pkg/dal" "github.com/cortezaproject/corteza/server/pkg/filter" @@ -29,6 +28,10 @@ type ( deleter interface { Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error) } + + counter interface { + Count(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (uint, error) + } ) // ComposeRecordsList iterates over results and collects all available records @@ -65,6 +68,20 @@ func ComposeRecordsFind(ctx context.Context, l lookuper, mod *types.Module, reco return } +func ComposeRecordsCount(ctx context.Context, c counter, mod *types.Module, filter types.RecordFilter) (cnt uint, err error) { + constraints := map[string][]interface{}{ + "namespaceID": {mod.NamespaceID}, + } + + if filter.ModuleID != 0 { + constraints["moduleID"] = []interface{}{filter.ModuleID} + } + + dalFilter := filter.ToConstraintedFilter(constraints) + + return c.Count(ctx, mod.ModelRef(), recLookupOperations(mod), dalFilter) +} + func ComposeRecordCreate(ctx context.Context, c creator, mod *types.Module, records ...*types.Record) (err error) { return c.Create(ctx, mod.ModelRef(), recCreateOperations(mod), recToGetters(records...)...) } diff --git a/server/compose/service/dal_interfaces.go b/server/compose/service/dal_interfaces.go index e47f12d9e..90754fda1 100644 --- a/server/compose/service/dal_interfaces.go +++ b/server/compose/service/dal_interfaces.go @@ -12,6 +12,7 @@ type ( Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, rr ...dal.ValueGetter) (err error) Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error) + Count(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (uint, error) Run(ctx context.Context, pp dal.Pipeline) (dal.Iterator, error) Lookup(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, lookup dal.ValueGetter, dst dal.ValueSetter) (err error) Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error) diff --git a/server/compose/service/record.go b/server/compose/service/record.go index b588eb87e..c0874dcca 100644 --- a/server/compose/service/record.go +++ b/server/compose/service/record.go @@ -52,6 +52,8 @@ type ( namespace namespaceFinder module moduleFinder + opt RecordOptions + revisions *recordRevisions formatter recordValuesFormatter @@ -154,6 +156,10 @@ type ( Country() string } + RecordOptions struct { + LimitRecords int + } + RecordImportSession struct { Name string `json:"-"` SessionID uint64 `json:"sessionID,string"` @@ -198,7 +204,7 @@ type ( recordReportEntry map[string]any ) -func Record() *record { +func Record(opts RecordOptions) *record { svc := &record{ actionlog: DefaultActionlog, ac: DefaultAccessControl, @@ -209,6 +215,8 @@ func Record() *record { namespace: DefaultNamespace, module: DefaultModule, + opt: opts, + revisions: &recordRevisions{revisions.Service(dal.Service())}, formatter: values.Formatter(), @@ -817,6 +825,11 @@ func (svc record) create(ctx context.Context, new *types.Record) (rec *types.Rec aProps.setNamespace(ns) aProps.setModule(m) + // check the records limit per namespace + if err = svc.checkLimit(ctx, m); err != nil { + return nil, nil, err + } + if !svc.ac.CanCreateRecordOnModule(ctx, m) { return nil, dd, RecordErrNotAllowedToCreate() } @@ -887,6 +900,27 @@ func (svc record) create(ctx context.Context, new *types.Record) (rec *types.Rec return } +func (svc record) checkLimit(ctx context.Context, m *types.Module) (err error) { + if svc.opt.LimitRecords == 0 { + return nil + } + + // get the number of records count for the namespace + recordsCount, err := dalutils.ComposeRecordsCount(ctx, svc.dal, m, types.RecordFilter{ + NamespaceID: m.NamespaceID, + ModuleID: 0, + }) + if err != nil { + return err + } + + if recordsCount >= uint(svc.opt.LimitRecords) { + return RecordErrMaxRecordsReached() + } + + return nil +} + // RecordValueSanitization does basic field and format validation // // Received values must fit the data model: on unknown fields diff --git a/server/compose/service/record_actions.gen.go b/server/compose/service/record_actions.gen.go index 10e48f16c..df47f5c8e 100644 --- a/server/compose/service/record_actions.gen.go +++ b/server/compose/service/record_actions.gen.go @@ -62,7 +62,6 @@ var ( // setRecord updates recordActionProps's record // // This function is auto-generated. -// func (p *recordActionProps) setRecord(record *types.Record) *recordActionProps { p.record = record return p @@ -71,7 +70,6 @@ func (p *recordActionProps) setRecord(record *types.Record) *recordActionProps { // setChanged updates recordActionProps's changed // // This function is auto-generated. -// func (p *recordActionProps) setChanged(changed *types.Record) *recordActionProps { p.changed = changed return p @@ -80,7 +78,6 @@ func (p *recordActionProps) setChanged(changed *types.Record) *recordActionProps // setFilter updates recordActionProps's filter // // This function is auto-generated. -// func (p *recordActionProps) setFilter(filter *types.RecordFilter) *recordActionProps { p.filter = filter return p @@ -89,7 +86,6 @@ func (p *recordActionProps) setFilter(filter *types.RecordFilter) *recordActionP // setNamespace updates recordActionProps's namespace // // This function is auto-generated. -// func (p *recordActionProps) setNamespace(namespace *types.Namespace) *recordActionProps { p.namespace = namespace return p @@ -98,7 +94,6 @@ func (p *recordActionProps) setNamespace(namespace *types.Namespace) *recordActi // setModule updates recordActionProps's module // // This function is auto-generated. -// func (p *recordActionProps) setModule(module *types.Module) *recordActionProps { p.module = module return p @@ -107,7 +102,6 @@ func (p *recordActionProps) setModule(module *types.Module) *recordActionProps { // setBulkOperation updates recordActionProps's bulkOperation // // This function is auto-generated. -// func (p *recordActionProps) setBulkOperation(bulkOperation string) *recordActionProps { p.bulkOperation = bulkOperation return p @@ -116,7 +110,6 @@ func (p *recordActionProps) setBulkOperation(bulkOperation string) *recordAction // setField updates recordActionProps's field // // This function is auto-generated. -// func (p *recordActionProps) setField(field string) *recordActionProps { p.field = field return p @@ -125,7 +118,6 @@ func (p *recordActionProps) setField(field string) *recordActionProps { // setPositionField updates recordActionProps's positionField // // This function is auto-generated. -// func (p *recordActionProps) setPositionField(positionField *types.ModuleField) *recordActionProps { p.positionField = positionField return p @@ -134,7 +126,6 @@ func (p *recordActionProps) setPositionField(positionField *types.ModuleField) * // setGroupField updates recordActionProps's groupField // // This function is auto-generated. -// func (p *recordActionProps) setGroupField(groupField *types.ModuleField) *recordActionProps { p.groupField = groupField return p @@ -143,7 +134,6 @@ func (p *recordActionProps) setGroupField(groupField *types.ModuleField) *record // setValue updates recordActionProps's value // // This function is auto-generated. -// func (p *recordActionProps) setValue(value string) *recordActionProps { p.value = value return p @@ -152,7 +142,6 @@ func (p *recordActionProps) setValue(value string) *recordActionProps { // setValueErrors updates recordActionProps's valueErrors // // This function is auto-generated. -// func (p *recordActionProps) setValueErrors(valueErrors *types.RecordValueErrorSet) *recordActionProps { p.valueErrors = valueErrors return p @@ -161,7 +150,6 @@ func (p *recordActionProps) setValueErrors(valueErrors *types.RecordValueErrorSe // Serialize converts recordActionProps to actionlog.Meta // // This function is auto-generated. -// func (p recordActionProps) Serialize() actionlog.Meta { var ( m = make(actionlog.Meta) @@ -219,7 +207,6 @@ func (p recordActionProps) Serialize() actionlog.Meta { // tr translates string and replaces meta value placeholder with values // // This function is auto-generated. -// func (p recordActionProps) Format(in string, err error) string { var ( pairs = []string{"{{err}}"} @@ -385,7 +372,6 @@ func (p recordActionProps) Format(in string, err error) string { // String returns loggable description as string // // This function is auto-generated. -// func (a *recordAction) String() string { var props = &recordActionProps{} @@ -413,7 +399,6 @@ func (e *recordAction) ToAction() *actionlog.Action { // RecordActionSearch returns "compose:record.search" action // // This function is auto-generated. -// func RecordActionSearch(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -433,7 +418,6 @@ func RecordActionSearch(props ...*recordActionProps) *recordAction { // RecordActionSearchSensitive returns "compose:record.searchSensitive" action // // This function is auto-generated. -// func RecordActionSearchSensitive(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -453,7 +437,6 @@ func RecordActionSearchSensitive(props ...*recordActionProps) *recordAction { // RecordActionLookup returns "compose:record.lookup" action // // This function is auto-generated. -// func RecordActionLookup(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -473,7 +456,6 @@ func RecordActionLookup(props ...*recordActionProps) *recordAction { // RecordActionReport returns "compose:record.report" action // // This function is auto-generated. -// func RecordActionReport(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -493,7 +475,6 @@ func RecordActionReport(props ...*recordActionProps) *recordAction { // RecordActionBulk returns "compose:record.bulk" action // // This function is auto-generated. -// func RecordActionBulk(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -513,7 +494,6 @@ func RecordActionBulk(props ...*recordActionProps) *recordAction { // RecordActionCreate returns "compose:record.create" action // // This function is auto-generated. -// func RecordActionCreate(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -533,7 +513,6 @@ func RecordActionCreate(props ...*recordActionProps) *recordAction { // RecordActionUpdate returns "compose:record.update" action // // This function is auto-generated. -// func RecordActionUpdate(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -553,7 +532,6 @@ func RecordActionUpdate(props ...*recordActionProps) *recordAction { // RecordActionDelete returns "compose:record.delete" action // // This function is auto-generated. -// func RecordActionDelete(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -573,7 +551,6 @@ func RecordActionDelete(props ...*recordActionProps) *recordAction { // RecordActionPatch returns "compose:record.patch" action // // This function is auto-generated. -// func RecordActionPatch(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -593,7 +570,6 @@ func RecordActionPatch(props ...*recordActionProps) *recordAction { // RecordActionUndelete returns "compose:record.undelete" action // // This function is auto-generated. -// func RecordActionUndelete(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -613,7 +589,6 @@ func RecordActionUndelete(props ...*recordActionProps) *recordAction { // RecordActionImport returns "compose:record.import" action // // This function is auto-generated. -// func RecordActionImport(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -633,7 +608,6 @@ func RecordActionImport(props ...*recordActionProps) *recordAction { // RecordActionSearchRevisions returns "compose:record.searchRevisions" action // // This function is auto-generated. -// func RecordActionSearchRevisions(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -653,7 +627,6 @@ func RecordActionSearchRevisions(props ...*recordActionProps) *recordAction { // RecordActionExport returns "compose:record.export" action // // This function is auto-generated. -// func RecordActionExport(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -673,7 +646,6 @@ func RecordActionExport(props ...*recordActionProps) *recordAction { // RecordActionOrganize returns "compose:record.organize" action // // This function is auto-generated. -// func RecordActionOrganize(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -693,7 +665,6 @@ func RecordActionOrganize(props ...*recordActionProps) *recordAction { // RecordActionIteratorInvoked returns "compose:record.iteratorInvoked" action // // This function is auto-generated. -// func RecordActionIteratorInvoked(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -713,7 +684,6 @@ func RecordActionIteratorInvoked(props ...*recordActionProps) *recordAction { // RecordActionIteratorIteration returns "compose:record.iteratorIteration" action // // This function is auto-generated. -// func RecordActionIteratorIteration(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -733,7 +703,6 @@ func RecordActionIteratorIteration(props ...*recordActionProps) *recordAction { // RecordActionIteratorClone returns "compose:record.iteratorClone" action // // This function is auto-generated. -// func RecordActionIteratorClone(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -753,7 +722,6 @@ func RecordActionIteratorClone(props ...*recordActionProps) *recordAction { // RecordActionIteratorUpdate returns "compose:record.iteratorUpdate" action // // This function is auto-generated. -// func RecordActionIteratorUpdate(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -773,7 +741,6 @@ func RecordActionIteratorUpdate(props ...*recordActionProps) *recordAction { // RecordActionIteratorDelete returns "compose:record.iteratorDelete" action // // This function is auto-generated. -// func RecordActionIteratorDelete(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -793,7 +760,6 @@ func RecordActionIteratorDelete(props ...*recordActionProps) *recordAction { // RecordActionIteratorUndelete returns "compose:record.iteratorUndelete" action // // This function is auto-generated. -// func RecordActionIteratorUndelete(props ...*recordActionProps) *recordAction { a := &recordAction{ timestamp: time.Now(), @@ -816,9 +782,7 @@ func RecordActionIteratorUndelete(props ...*recordActionProps) *recordAction { // RecordErrGeneric returns "compose:record.generic" as *errors.Error // -// // This function is auto-generated. -// func RecordErrGeneric(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -852,9 +816,7 @@ func RecordErrGeneric(mm ...*recordActionProps) *errors.Error { // RecordErrNotFound returns "compose:record.notFound" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotFound(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -886,9 +848,7 @@ func RecordErrNotFound(mm ...*recordActionProps) *errors.Error { // RecordErrNamespaceNotFound returns "compose:record.namespaceNotFound" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNamespaceNotFound(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -920,9 +880,7 @@ func RecordErrNamespaceNotFound(mm ...*recordActionProps) *errors.Error { // RecordErrModuleNotFoundModule returns "compose:record.moduleNotFoundModule" as *errors.Error // -// // This function is auto-generated. -// func RecordErrModuleNotFoundModule(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -954,9 +912,7 @@ func RecordErrModuleNotFoundModule(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidID returns "compose:record.invalidID" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidID(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -988,9 +944,7 @@ func RecordErrInvalidID(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidNamespaceID returns "compose:record.invalidNamespaceID" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidNamespaceID(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1022,9 +976,7 @@ func RecordErrInvalidNamespaceID(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidModuleID returns "compose:record.invalidModuleID" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidModuleID(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1056,9 +1008,7 @@ func RecordErrInvalidModuleID(mm ...*recordActionProps) *errors.Error { // RecordErrStaleData returns "compose:record.staleData" as *errors.Error // -// // This function is auto-generated. -// func RecordErrStaleData(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1090,9 +1040,7 @@ func RecordErrStaleData(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToRead returns "compose:record.notAllowedToRead" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToRead(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1126,9 +1074,7 @@ func RecordErrNotAllowedToRead(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToSearch returns "compose:record.notAllowedToSearch" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToSearch(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1162,9 +1108,7 @@ func RecordErrNotAllowedToSearch(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToSearchRevisions returns "compose:record.notAllowedToSearchRevisions" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToSearchRevisions(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1198,9 +1142,7 @@ func RecordErrNotAllowedToSearchRevisions(mm ...*recordActionProps) *errors.Erro // RecordErrRevisionsDisabledOnModule returns "compose:record.revisionsDisabledOnModule" as *errors.Error // -// // This function is auto-generated. -// func RecordErrRevisionsDisabledOnModule(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1234,9 +1176,7 @@ func RecordErrRevisionsDisabledOnModule(mm ...*recordActionProps) *errors.Error // RecordErrNotAllowedToReadNamespace returns "compose:record.notAllowedToReadNamespace" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToReadNamespace(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1270,9 +1210,7 @@ func RecordErrNotAllowedToReadNamespace(mm ...*recordActionProps) *errors.Error // RecordErrNotAllowedToReadModule returns "compose:record.notAllowedToReadModule" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToReadModule(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1306,9 +1244,7 @@ func RecordErrNotAllowedToReadModule(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToListRecords returns "compose:record.notAllowedToListRecords" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToListRecords(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1342,9 +1278,7 @@ func RecordErrNotAllowedToListRecords(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToCreate returns "compose:record.notAllowedToCreate" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToCreate(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1378,9 +1312,7 @@ func RecordErrNotAllowedToCreate(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToUpdate returns "compose:record.notAllowedToUpdate" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToUpdate(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1414,9 +1346,7 @@ func RecordErrNotAllowedToUpdate(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToDelete returns "compose:record.notAllowedToDelete" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToDelete(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1450,9 +1380,7 @@ func RecordErrNotAllowedToDelete(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToUndelete returns "compose:record.notAllowedToUndelete" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToUndelete(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1486,9 +1414,7 @@ func RecordErrNotAllowedToUndelete(mm ...*recordActionProps) *errors.Error { // RecordErrNotAllowedToChangeFieldValue returns "compose:record.notAllowedToChangeFieldValue" as *errors.Error // -// // This function is auto-generated. -// func RecordErrNotAllowedToChangeFieldValue(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1520,11 +1446,43 @@ func RecordErrNotAllowedToChangeFieldValue(mm ...*recordActionProps) *errors.Err return e } -// RecordErrMissingPositionField returns "compose:record.missingPositionField" as *errors.Error -// +// RecordErrMaxRecordsReached returns "compose:record.maxRecordsReached" as *errors.Error // // This function is auto-generated. +func RecordErrMaxRecordsReached(mm ...*recordActionProps) *errors.Error { + var p = &recordActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("maximum number of records per namespace reached", nil), + + errors.Meta("type", "maxRecordsReached"), + errors.Meta("resource", "compose:record"), + + // action log entry; no formatting, it will be applied inside recordAction fn. + errors.Meta(recordLogMetaKey{}, "maximum number of records per namespace reached"), + errors.Meta(recordPropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "compose"), + errors.Meta(locale.ErrorMetaKey{}, "record.errors.maxRecordsReached"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + +// RecordErrMissingPositionField returns "compose:record.missingPositionField" as *errors.Error // +// This function is auto-generated. func RecordErrMissingPositionField(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1558,9 +1516,7 @@ func RecordErrMissingPositionField(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidPositionFieldKind returns "compose:record.invalidPositionFieldKind" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidPositionFieldKind(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1594,9 +1550,7 @@ func RecordErrInvalidPositionFieldKind(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidPositionFieldConfigMultiValue returns "compose:record.invalidPositionFieldConfigMultiValue" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidPositionFieldConfigMultiValue(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1630,9 +1584,7 @@ func RecordErrInvalidPositionFieldConfigMultiValue(mm ...*recordActionProps) *er // RecordErrInvalidPositionValueType returns "compose:record.invalidPositionValueType" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidPositionValueType(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1666,9 +1618,7 @@ func RecordErrInvalidPositionValueType(mm ...*recordActionProps) *errors.Error { // RecordErrMissingGroupField returns "compose:record.missingGroupField" as *errors.Error // -// // This function is auto-generated. -// func RecordErrMissingGroupField(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1702,9 +1652,7 @@ func RecordErrMissingGroupField(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidGroupFieldConfigMultiValue returns "compose:record.invalidGroupFieldConfigMultiValue" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidGroupFieldConfigMultiValue(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1738,9 +1686,7 @@ func RecordErrInvalidGroupFieldConfigMultiValue(mm ...*recordActionProps) *error // RecordErrImportSessionAlreadActive returns "compose:record.importSessionAlreadActive" as *errors.Error // -// // This function is auto-generated. -// func RecordErrImportSessionAlreadActive(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1774,9 +1720,7 @@ func RecordErrImportSessionAlreadActive(mm ...*recordActionProps) *errors.Error // RecordErrFieldNotFound returns "compose:record.fieldNotFound" as *errors.Error // -// // This function is auto-generated. -// func RecordErrFieldNotFound(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1808,9 +1752,7 @@ func RecordErrFieldNotFound(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidValueStructure returns "compose:record.invalidValueStructure" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidValueStructure(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1842,9 +1784,7 @@ func RecordErrInvalidValueStructure(mm ...*recordActionProps) *errors.Error { // RecordErrUnknownBulkOperation returns "compose:record.unknownBulkOperation" as *errors.Error // -// // This function is auto-generated. -// func RecordErrUnknownBulkOperation(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1876,9 +1816,7 @@ func RecordErrUnknownBulkOperation(mm ...*recordActionProps) *errors.Error { // RecordErrInvalidReferenceFormat returns "compose:record.invalidReferenceFormat" as *errors.Error // -// // This function is auto-generated. -// func RecordErrInvalidReferenceFormat(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1910,9 +1848,7 @@ func RecordErrInvalidReferenceFormat(mm ...*recordActionProps) *errors.Error { // RecordErrValueInput returns "compose:record.valueInput" as *errors.Error // -// // This function is auto-generated. -// func RecordErrValueInput(mm ...*recordActionProps) *errors.Error { var p = &recordActionProps{} if len(mm) > 0 { @@ -1950,7 +1886,6 @@ func RecordErrValueInput(mm ...*recordActionProps) *errors.Error { // It will wrap unrecognized/internal errors with generic errors. // // This function is auto-generated. -// func (svc record) recordAction(ctx context.Context, props *recordActionProps, actionFn func(...*recordActionProps) *recordAction, err error) error { if svc.actionlog == nil || actionFn == nil { // action log disabled or no action fn passed, return error as-is diff --git a/server/compose/service/record_actions.yaml b/server/compose/service/record_actions.yaml index 90cdc8166..08227bb59 100644 --- a/server/compose/service/record_actions.yaml +++ b/server/compose/service/record_actions.yaml @@ -183,6 +183,10 @@ errors: message: "not allowed to change value of field {{field}}" log: "failed to change value of field {{field}}; insufficient permissions" + - error: maxRecordsReached + message: "maximum number of records per namespace reached" + log: "maximum number of records per namespace reached" + # Organizer - error: missingPositionField message: "position module field not found" diff --git a/server/compose/service/service.go b/server/compose/service/service.go index 1698ad88c..2d1f4517c 100644 --- a/server/compose/service/service.go +++ b/server/compose/service/service.go @@ -42,6 +42,7 @@ type ( ActionLog options.ActionLogOpt Discovery options.DiscoveryOpt Storage options.ObjectStoreOpt + Limit options.LimitOpt UserFinder userFinder SchemaAltManager schemaAltManager } @@ -185,7 +186,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config) DefaultModule = Module(c.SchemaAltManager) DefaultImportSession = ImportSession() - DefaultRecord = Record() + DefaultRecord = Record(RecordOptions{LimitRecords: c.Limit.RecordCountPerNamespace}) DefaultPage = Page() DefaultPageLayout = PageLayout() DefaultChart = Chart() diff --git a/server/federation/rest/router.go b/server/federation/rest/router.go index 42c02592d..1d3a4fcb9 100644 --- a/server/federation/rest/router.go +++ b/server/federation/rest/router.go @@ -1,13 +1,14 @@ package rest import ( + "github.com/cortezaproject/corteza/server/pkg/options" "github.com/go-chi/chi/v5" "github.com/cortezaproject/corteza/server/federation/rest/handlers" "github.com/cortezaproject/corteza/server/pkg/auth" ) -func MountRoutes() func(r chi.Router) { +func MountRoutes(opts options.LimitOpt) func(r chi.Router) { return func(r chi.Router) { r.Group(func(r chi.Router) { handlers.NewNodeHandshake(NodeHandshake{}.New()).MountRoutes(r) @@ -22,7 +23,7 @@ func MountRoutes() func(r chi.Router) { handlers.NewNode(Node{}.New()).MountRoutes(r) handlers.NewManageStructure((ManageStructure{}.New())).MountRoutes(r) - handlers.NewSyncData((SyncData{}.New())).MountRoutes(r) + handlers.NewSyncData((SyncData{}.New(opts))).MountRoutes(r) handlers.NewSyncStructure((SyncStructure{}.New())).MountRoutes(r) }) } diff --git a/server/federation/rest/sync_data.go b/server/federation/rest/sync_data.go index 6309d86a2..1a1df0bb7 100644 --- a/server/federation/rest/sync_data.go +++ b/server/federation/rest/sync_data.go @@ -3,6 +3,7 @@ package rest import ( "context" "fmt" + "github.com/cortezaproject/corteza/server/pkg/options" "net/http" "strings" "time" @@ -21,7 +22,9 @@ import ( ) type ( - SyncData struct{} + SyncData struct { + opts options.LimitOpt + } recordPayload struct { *ct.Record @@ -50,8 +53,10 @@ type ( } ) -func (SyncData) New() *SyncData { - return &SyncData{} +func (SyncData) New(opts options.LimitOpt) *SyncData { + return &SyncData{ + opts: opts, + } } func (ctrl SyncData) ReadExposedAll(ctx context.Context, r *request.SyncDataReadExposedAll) (interface{}, error) { @@ -95,7 +100,7 @@ func (ctrl SyncData) ReadExposedAll(ctx context.Context, r *request.SyncDataRead // todo - handle error properly // @todo !!! - if list, _, err := (cs.Record()).Find(ctx, rf); err != nil || len(list) == 0 { + if list, _, err := (cs.Record(cs.RecordOptions{LimitRecords: ctrl.opts.RecordCountPerNamespace})).Find(ctx, rf); err != nil || len(list) == 0 { continue } @@ -223,7 +228,7 @@ func (ctrl SyncData) readExposed(ctx context.Context, r *request.SyncDataReadExp } // @todo !!! - list, f, err := (cs.Record()).Find(ctx, f) + list, f, err := (cs.Record(cs.RecordOptions{LimitRecords: ctrl.opts.RecordCountPerNamespace})).Find(ctx, f) if err != nil { return nil, err diff --git a/server/pkg/dal/driver.go b/server/pkg/dal/driver.go index 16a95f5db..60c571425 100644 --- a/server/pkg/dal/driver.go +++ b/server/pkg/dal/driver.go @@ -49,6 +49,9 @@ type ( // Search returns an iterator which can be used to access all if the bits Search(context.Context, *Model, filter.Filter) (Iterator, error) + // Count returns the total number of rows matching the filter + Count(context.Context, *Model, filter.Filter) (uint, error) + // Analyze returns the operation analysis the connection can perform for the model Analyze(ctx context.Context, m *Model) (map[string]OpAnalysis, error) diff --git a/server/pkg/dal/service.go b/server/pkg/dal/service.go index 5c3dd06cb..0d3e6c146 100644 --- a/server/pkg/dal/service.go +++ b/server/pkg/dal/service.go @@ -53,6 +53,7 @@ type ( Update(ctx context.Context, mf ModelRef, operations OperationSet, rr ...ValueGetter) (err error) Search(ctx context.Context, mf ModelRef, operations OperationSet, f filter.Filter) (iter Iterator, err error) Lookup(ctx context.Context, mf ModelRef, operations OperationSet, lookup ValueGetter, dst ValueSetter) (err error) + Count(ctx context.Context, mf ModelRef, operations OperationSet, f filter.Filter) (uint, error) Delete(ctx context.Context, mf ModelRef, operations OperationSet, vv ...ValueGetter) (err error) Truncate(ctx context.Context, mf ModelRef, operations OperationSet) (err error) @@ -488,6 +489,21 @@ func (svc *service) Search(ctx context.Context, mf ModelRef, operations Operatio return cw.connection.Search(ctx, model, f) } +func (svc *service) Count(ctx context.Context, mf ModelRef, operations OperationSet, f filter.Filter) (cnt uint, err error) { + if err = svc.canOpData(mf); err != nil { + err = fmt.Errorf("cannot count data entry: %w", err) + return + } + + model, cw, err := svc.storeOpPrep(ctx, mf, operations) + if err != nil { + err = fmt.Errorf("cannot count data entry: %w", err) + return + } + + return cw.connection.Count(ctx, model, f) +} + // Run returns an iterator based on the provided Pipeline // @todo consider moving the Search method to utilize this also func (svc *service) Run(ctx context.Context, pp Pipeline) (iter Iterator, err error) { diff --git a/server/pkg/options/options.gen.go b/server/pkg/options/options.gen.go index 9eb9c82aa..083466551 100644 --- a/server/pkg/options/options.gen.go +++ b/server/pkg/options/options.gen.go @@ -162,7 +162,8 @@ type ( } LimitOpt struct { - SystemUsers int `env:"LIMIT_SYSTEM_USERS"` + SystemUsers int `env:"LIMIT_SYSTEM_USERS"` + RecordCountPerNamespace int `env:"LIMIT_RECORD_COUNT_PER_NAMESPACE"` } LocaleOpt struct { diff --git a/server/store/adapters/rdbms/dal/connection.go b/server/store/adapters/rdbms/dal/connection.go index d35f7eb2a..c55802989 100644 --- a/server/store/adapters/rdbms/dal/connection.go +++ b/server/store/adapters/rdbms/dal/connection.go @@ -109,6 +109,13 @@ func (c *connection) Search(ctx context.Context, m *dal.Model, f filter.Filter) }) } +func (c *connection) Count(ctx context.Context, m *dal.Model, f filter.Filter) (i uint, _ error) { + return i, c.withModel(m, func(m *model) (err error) { + i, err = m.Count(ctx, f) + return + }) +} + func (c *connection) Analyze(ctx context.Context, m *dal.Model) (a map[string]dal.OpAnalysis, err error) { // @todo somehow (probably operations) bring in the info what can be done // for now, since we're quite rigid on the drivers, this will do. diff --git a/server/store/adapters/rdbms/dal/model.go b/server/store/adapters/rdbms/dal/model.go index 7eb46c599..e1a9ee00b 100644 --- a/server/store/adapters/rdbms/dal/model.go +++ b/server/store/adapters/rdbms/dal/model.go @@ -268,6 +268,52 @@ func (d *model) Search(f filter.Filter) (i *iterator, err error) { return } +func (d *model) Count(ctx context.Context, f filter.Filter) (c uint, err error) { + var ( + aux = struct { + Count uint `db:"count"` + }{} + + rows *sql.Rows + exprs []exp.Expression + + q = d.dialect.GOQU(). + From(d.table.Ident()).Select(goqu.COUNT(goqu.Star()).As("count")) + ) + + for ident, val := range f.Constraints() { + attrExpr, err := d.table.AttributeExpression(ident) + if err != nil { + return 0, err + } + + exprs = append(exprs, exp.NewBooleanExpression(exp.EqOp, attrExpr, val)) + } + + query, args, err := q.Where(exprs...).Limit(1).ToSQL() + if err != nil { + return + } + + rows, err = d.conn.QueryContext(ctx, query, args...) + if err != nil { + return + } + + defer rows.Close() + if !rows.Next() { + return 0, errors.NotFound("not found") + } + + if err = rows.Scan(&aux.Count); err != nil { + return + } + + c = aux.Count + + return c, nil +} + // Aggregate constructs SELECT sql with group-by and an optional having CLAUSE // // All group-by attributes are prepended to aggregation diff --git a/server/tests/federation/main_test.go b/server/tests/federation/main_test.go index 8599a2a7a..8b4bfdf0a 100644 --- a/server/tests/federation/main_test.go +++ b/server/tests/federation/main_test.go @@ -64,7 +64,7 @@ func InitTestApp() { r = chi.NewRouter() r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) - r.Group(rest.MountRoutes()) + r.Group(rest.MountRoutes(testApp.Opt.Limit)) } }