| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | package core | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"regexp" | 
					
						
							|  |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	validation "github.com/go-ozzo/ozzo-validation/v4" | 
					
						
							|  |  |  | 	"github.com/pocketbase/dbx" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/core/validators" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/dbutils" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/list" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/search" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/types" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var collectionNameRegex = regexp.MustCompile(`^\w+$`) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func onCollectionValidate(e *CollectionEvent) error { | 
					
						
							|  |  |  | 	var original *Collection | 
					
						
							|  |  |  | 	if !e.Collection.IsNew() { | 
					
						
							|  |  |  | 		original = &Collection{} | 
					
						
							|  |  |  | 		if err := e.App.ModelQuery(original).Model(e.Collection.LastSavedPK(), original); err != nil { | 
					
						
							|  |  |  | 			return fmt.Errorf("failed to fetch old collection state: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	validator := newCollectionValidator( | 
					
						
							|  |  |  | 		e.Context, | 
					
						
							|  |  |  | 		e.App, | 
					
						
							|  |  |  | 		e.Collection, | 
					
						
							|  |  |  | 		original, | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return validator.run() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func newCollectionValidator(ctx context.Context, app App, new, original *Collection) *collectionValidator { | 
					
						
							|  |  |  | 	validator := &collectionValidator{ | 
					
						
							|  |  |  | 		ctx:      ctx, | 
					
						
							|  |  |  | 		app:      app, | 
					
						
							|  |  |  | 		new:      new, | 
					
						
							|  |  |  | 		original: original, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// load old/original collection
 | 
					
						
							|  |  |  | 	if validator.original == nil { | 
					
						
							|  |  |  | 		validator.original = NewCollection(validator.new.Type, "") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return validator | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type collectionValidator struct { | 
					
						
							|  |  |  | 	original *Collection | 
					
						
							|  |  |  | 	new      *Collection | 
					
						
							|  |  |  | 	app      App | 
					
						
							|  |  |  | 	ctx      context.Context | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type optionsValidator interface { | 
					
						
							|  |  |  | 	validate(cv *collectionValidator) error | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) run() error { | 
					
						
							| 
									
										
										
										
											2024-11-04 16:51:32 +08:00
										 |  |  | 	if validator.original.IsNew() { | 
					
						
							|  |  |  | 		validator.new.updateGeneratedIdIfExists(validator.app) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	// generate fields from the query (overwriting any explicit user defined fields)
 | 
					
						
							|  |  |  | 	if validator.new.IsView() { | 
					
						
							|  |  |  | 		validator.new.Fields, _ = validator.app.CreateViewFields(validator.new.ViewQuery) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// validate base fields
 | 
					
						
							|  |  |  | 	baseErr := validation.ValidateStruct(validator.new, | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.Id, | 
					
						
							|  |  |  | 			validation.Required, | 
					
						
							|  |  |  | 			validation.When( | 
					
						
							|  |  |  | 				validator.original.IsNew(), | 
					
						
							|  |  |  | 				validation.Length(1, 100), | 
					
						
							|  |  |  | 				validation.Match(DefaultIdRegex), | 
					
						
							|  |  |  | 				validation.By(validators.UniqueId(validator.app.DB(), validator.new.TableName())), | 
					
						
							|  |  |  | 			).Else( | 
					
						
							|  |  |  | 				validation.By(validators.Equal(validator.original.Id)), | 
					
						
							|  |  |  | 			), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.System, | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemFlagChange), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.Type, | 
					
						
							|  |  |  | 			validation.Required, | 
					
						
							|  |  |  | 			validation.In( | 
					
						
							|  |  |  | 				CollectionTypeBase, | 
					
						
							|  |  |  | 				CollectionTypeAuth, | 
					
						
							|  |  |  | 				CollectionTypeView, | 
					
						
							|  |  |  | 			), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoTypeChange), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.Name, | 
					
						
							|  |  |  | 			validation.Required, | 
					
						
							|  |  |  | 			validation.Length(1, 255), | 
					
						
							|  |  |  | 			validation.By(checkForVia), | 
					
						
							|  |  |  | 			validation.Match(collectionNameRegex), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemNameChange), | 
					
						
							|  |  |  | 			validation.By(validator.checkUniqueName), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.Fields, | 
					
						
							|  |  |  | 			validation.By(validator.checkFieldDuplicates), | 
					
						
							|  |  |  | 			validation.By(validator.checkMinFields), | 
					
						
							|  |  |  | 			validation.When( | 
					
						
							|  |  |  | 				!validator.new.IsView(), | 
					
						
							|  |  |  | 				validation.By(validator.ensureNoSystemFieldsChange), | 
					
						
							|  |  |  | 				validation.By(validator.ensureNoFieldsTypeChange), | 
					
						
							|  |  |  | 			), | 
					
						
							|  |  |  | 			validation.When(validator.new.IsAuth(), validation.By(validator.checkReservedAuthKeys)), | 
					
						
							|  |  |  | 			validation.By(validator.checkFieldValidators), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.ListRule, | 
					
						
							|  |  |  | 			validation.By(validator.checkRule), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemRuleChange(validator.original.ListRule)), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.ViewRule, | 
					
						
							|  |  |  | 			validation.By(validator.checkRule), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemRuleChange(validator.original.ViewRule)), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.CreateRule, | 
					
						
							|  |  |  | 			validation.When(validator.new.IsView(), validation.Nil), | 
					
						
							|  |  |  | 			validation.By(validator.checkRule), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemRuleChange(validator.original.CreateRule)), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.UpdateRule, | 
					
						
							|  |  |  | 			validation.When(validator.new.IsView(), validation.Nil), | 
					
						
							|  |  |  | 			validation.By(validator.checkRule), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemRuleChange(validator.original.UpdateRule)), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field( | 
					
						
							|  |  |  | 			&validator.new.DeleteRule, | 
					
						
							|  |  |  | 			validation.When(validator.new.IsView(), validation.Nil), | 
					
						
							|  |  |  | 			validation.By(validator.checkRule), | 
					
						
							|  |  |  | 			validation.By(validator.ensureNoSystemRuleChange(validator.original.DeleteRule)), | 
					
						
							|  |  |  | 		), | 
					
						
							|  |  |  | 		validation.Field(&validator.new.Indexes, validation.By(validator.checkIndexes)), | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	optionsErr := validator.validateOptions() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return validators.JoinValidationErrors(baseErr, optionsErr) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) checkUniqueName(value any) error { | 
					
						
							|  |  |  | 	v, _ := value.(string) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ensure unique collection name
 | 
					
						
							|  |  |  | 	if !validator.app.IsCollectionNameUnique(v, validator.original.Id) { | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ensure that the collection name doesn't collide with the id of any collection
 | 
					
						
							|  |  |  | 	dummyCollection := &Collection{} | 
					
						
							|  |  |  | 	if validator.app.ModelQuery(dummyCollection).Model(v, dummyCollection) == nil { | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_name_id_duplicate", "The name must not match an existing collection id.") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ensure that there is no existing internal table with the provided name
 | 
					
						
							|  |  |  | 	if validator.original.Name != v && // has changed
 | 
					
						
							|  |  |  | 		validator.app.IsCollectionNameUnique(v) && // is not a collection (in case it was presaved)
 | 
					
						
							|  |  |  | 		validator.app.HasTable(v) { | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_name_invalid", "The name shouldn't match with an existing internal table.") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) ensureNoSystemNameChange(value any) error { | 
					
						
							|  |  |  | 	v, _ := value.(string) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !validator.original.IsNew() && validator.original.System && v != validator.original.Name { | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_system_name_change", "System collection name cannot be changed.") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) ensureNoSystemFlagChange(value any) error { | 
					
						
							|  |  |  | 	v, _ := value.(bool) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !validator.original.IsNew() && v != validator.original.System { | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_system_flag_change", "System collection state cannot be changed.") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) ensureNoTypeChange(value any) error { | 
					
						
							|  |  |  | 	v, _ := value.(string) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !validator.original.IsNew() && v != validator.original.Type { | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_type_change", "Collection type cannot be changed.") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) ensureNoFieldsTypeChange(value any) error { | 
					
						
							|  |  |  | 	v, ok := value.(FieldsList) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	errs := validation.Errors{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, field := range v { | 
					
						
							|  |  |  | 		oldField := validator.original.Fields.GetById(field.GetId()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if oldField != nil && oldField.Type() != field.Type() { | 
					
						
							|  |  |  | 			errs[strconv.Itoa(i)] = validation.NewError( | 
					
						
							|  |  |  | 				"validation_field_type_change", | 
					
						
							|  |  |  | 				"Field type cannot be changed.", | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if len(errs) > 0 { | 
					
						
							|  |  |  | 		return errs | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) checkFieldDuplicates(value any) error { | 
					
						
							|  |  |  | 	fields, ok := value.(FieldsList) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	totalFields := len(fields) | 
					
						
							|  |  |  | 	ids := make([]string, 0, totalFields) | 
					
						
							|  |  |  | 	names := make([]string, 0, totalFields) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, field := range fields { | 
					
						
							|  |  |  | 		if list.ExistInSlice(field.GetId(), ids) { | 
					
						
							|  |  |  | 			return validation.Errors{ | 
					
						
							|  |  |  | 				strconv.Itoa(i): validation.Errors{ | 
					
						
							|  |  |  | 					"id": validation.NewError( | 
					
						
							|  |  |  | 						"validation_duplicated_field_id", | 
					
						
							|  |  |  | 						fmt.Sprintf("Duplicated or invalid field id %q", field.GetId()), | 
					
						
							|  |  |  | 					), | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// field names are used as db columns and should be case insensitive
 | 
					
						
							|  |  |  | 		nameLower := strings.ToLower(field.GetName()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if list.ExistInSlice(nameLower, names) { | 
					
						
							|  |  |  | 			return validation.Errors{ | 
					
						
							|  |  |  | 				strconv.Itoa(i): validation.Errors{ | 
					
						
							|  |  |  | 					"name": validation.NewError( | 
					
						
							|  |  |  | 						"validation_duplicated_field_name", | 
					
						
							|  |  |  | 						fmt.Sprintf("Duplicated or invalid field name %q", field.GetName()), | 
					
						
							|  |  |  | 					).SetParams(map[string]any{ | 
					
						
							|  |  |  | 						"fieldName": field.GetName(), | 
					
						
							|  |  |  | 					}), | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		ids = append(ids, field.GetId()) | 
					
						
							|  |  |  | 		names = append(names, nameLower) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) checkFieldValidators(value any) error { | 
					
						
							|  |  |  | 	fields, ok := value.(FieldsList) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	errs := validation.Errors{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, field := range fields { | 
					
						
							|  |  |  | 		if err := field.ValidateSettings(validator.ctx, validator.app, validator.new); err != nil { | 
					
						
							|  |  |  | 			errs[strconv.Itoa(i)] = err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(errs) > 0 { | 
					
						
							|  |  |  | 		return errs | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (cv *collectionValidator) checkViewQuery(value any) error { | 
					
						
							|  |  |  | 	v, _ := value.(string) | 
					
						
							|  |  |  | 	if v == "" { | 
					
						
							|  |  |  | 		return nil // nothing to check
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if _, err := cv.app.CreateViewFields(v); err != nil { | 
					
						
							|  |  |  | 		return validation.NewError( | 
					
						
							|  |  |  | 			"validation_invalid_view_query", | 
					
						
							|  |  |  | 			fmt.Sprintf("Invalid query - %s", err.Error()), | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var reservedAuthKeys = []string{"passwordConfirm", "oldPassword"} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (cv *collectionValidator) checkReservedAuthKeys(value any) error { | 
					
						
							|  |  |  | 	fields, ok := value.(FieldsList) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !cv.new.IsAuth() { | 
					
						
							|  |  |  | 		return nil // not an auth collection
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	errs := validation.Errors{} | 
					
						
							|  |  |  | 	for i, field := range fields { | 
					
						
							|  |  |  | 		if list.ExistInSlice(field.GetName(), reservedAuthKeys) { | 
					
						
							|  |  |  | 			errs[strconv.Itoa(i)] = validation.Errors{ | 
					
						
							|  |  |  | 				"name": validation.NewError( | 
					
						
							|  |  |  | 					"validation_reserved_field_name", | 
					
						
							|  |  |  | 					"The field name is reserved and cannot be used.", | 
					
						
							|  |  |  | 				), | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if len(errs) > 0 { | 
					
						
							|  |  |  | 		return errs | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (cv *collectionValidator) checkMinFields(value any) error { | 
					
						
							|  |  |  | 	fields, ok := value.(FieldsList) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(fields) == 0 { | 
					
						
							|  |  |  | 		return validation.ErrRequired | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// all collections must have an "id" PK field
 | 
					
						
							|  |  |  | 	idField, _ := fields.GetByName(FieldNameId).(*TextField) | 
					
						
							|  |  |  | 	if idField == nil || !idField.PrimaryKey { | 
					
						
							|  |  |  | 		return validation.NewError("validation_missing_primary_key", `Missing or invalid "id" PK field.`) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch cv.new.Type { | 
					
						
							|  |  |  | 	case CollectionTypeAuth: | 
					
						
							|  |  |  | 		passwordField, _ := fields.GetByName(FieldNamePassword).(*PasswordField) | 
					
						
							|  |  |  | 		if passwordField == nil { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_password_field", `System "password" field is required.`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !passwordField.Hidden || !passwordField.System { | 
					
						
							|  |  |  | 			return validation.Errors{FieldNamePassword: ErrMustBeSystemAndHidden} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		tokenKeyField, _ := fields.GetByName(FieldNameTokenKey).(*TextField) | 
					
						
							|  |  |  | 		if tokenKeyField == nil { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_tokenKey_field", `System "tokenKey" field is required.`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !tokenKeyField.Hidden || !tokenKeyField.System { | 
					
						
							|  |  |  | 			return validation.Errors{FieldNameTokenKey: ErrMustBeSystemAndHidden} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		emailField, _ := fields.GetByName(FieldNameEmail).(*EmailField) | 
					
						
							|  |  |  | 		if emailField == nil { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_email_field", `System "email" field is required.`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !emailField.System { | 
					
						
							|  |  |  | 			return validation.Errors{FieldNameEmail: ErrMustBeSystem} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		emailVisibilityField, _ := fields.GetByName(FieldNameEmailVisibility).(*BoolField) | 
					
						
							|  |  |  | 		if emailVisibilityField == nil { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_emailVisibility_field", `System "emailVisibility" field is required.`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !emailVisibilityField.System { | 
					
						
							|  |  |  | 			return validation.Errors{FieldNameEmailVisibility: ErrMustBeSystem} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		verifiedField, _ := fields.GetByName(FieldNameVerified).(*BoolField) | 
					
						
							|  |  |  | 		if verifiedField == nil { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_verified_field", `System "verified" field is required.`) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !verifiedField.System { | 
					
						
							|  |  |  | 			return validation.Errors{FieldNameVerified: ErrMustBeSystem} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) ensureNoSystemFieldsChange(value any) error { | 
					
						
							|  |  |  | 	fields, ok := value.(FieldsList) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-30 02:08:16 +08:00
										 |  |  | 	if validator.original.IsNew() { | 
					
						
							|  |  |  | 		return nil // not an update
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	for _, oldField := range validator.original.Fields { | 
					
						
							|  |  |  | 		if !oldField.GetSystem() { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		newField := fields.GetById(oldField.GetId()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if newField == nil || oldField.GetName() != newField.GetName() { | 
					
						
							|  |  |  | 			return validation.NewError("validation_system_field_change", "System fields cannot be deleted or renamed.") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (cv *collectionValidator) checkFieldsForUniqueIndex(value any) error { | 
					
						
							|  |  |  | 	names, ok := value.([]string) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(names) == 0 { | 
					
						
							|  |  |  | 		return nil // nothing to check
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, name := range names { | 
					
						
							|  |  |  | 		field := cv.new.Fields.GetByName(name) | 
					
						
							|  |  |  | 		if field == nil { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_field", fmt.Sprintf("Invalid or missing field %q", name)). | 
					
						
							|  |  |  | 				SetParams(map[string]any{"fieldName": name}) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if !dbutils.HasSingleColumnUniqueIndex(name, cv.new.Indexes) { | 
					
						
							|  |  |  | 			return validation.NewError("validation_missing_unique_constraint", fmt.Sprintf("The field %q doesn't have a UNIQUE constraint.", name)). | 
					
						
							|  |  |  | 				SetParams(map[string]any{"fieldName": name}) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // note: value could be either *string or string
 | 
					
						
							|  |  |  | func (validator *collectionValidator) checkRule(value any) error { | 
					
						
							|  |  |  | 	var vStr string | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	v, ok := value.(*string) | 
					
						
							|  |  |  | 	if ok { | 
					
						
							|  |  |  | 		if v != nil { | 
					
						
							|  |  |  | 			vStr = *v | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		vStr, ok = value.(string) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if vStr == "" { | 
					
						
							|  |  |  | 		return nil // nothing to check
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	r := NewRecordFieldResolver(validator.app, validator.new, nil, true) | 
					
						
							|  |  |  | 	_, err := search.FilterData(vStr).BuildExpr(r) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return validation.NewError("validation_invalid_rule", "Invalid rule. Raw error: "+err.Error()) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) ensureNoSystemRuleChange(oldRule *string) validation.RuleFunc { | 
					
						
							|  |  |  | 	return func(value any) error { | 
					
						
							|  |  |  | 		if validator.original.IsNew() || !validator.original.System { | 
					
						
							|  |  |  | 			return nil // not an update of a system collection
 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		rule, ok := value.(*string) | 
					
						
							|  |  |  | 		if !ok { | 
					
						
							|  |  |  | 			return validators.ErrUnsupportedValueType | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (rule == nil && oldRule == nil) || | 
					
						
							|  |  |  | 			(rule != nil && oldRule != nil && *rule == *oldRule) { | 
					
						
							|  |  |  | 			return nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return validation.NewError("validation_collection_system_rule_change", "System collection API rule cannot be changed.") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (cv *collectionValidator) checkIndexes(value any) error { | 
					
						
							|  |  |  | 	indexes, _ := value.(types.JSONArray[string]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if cv.new.IsView() && len(indexes) > 0 { | 
					
						
							|  |  |  | 		return validation.NewError( | 
					
						
							|  |  |  | 			"validation_indexes_not_supported", | 
					
						
							|  |  |  | 			"View collections don't support indexes.", | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 	duplicatedNames := make(map[string]struct{}, len(indexes)) | 
					
						
							|  |  |  | 	duplicatedDefinitions := make(map[string]struct{}, len(indexes)) | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	for i, rawIndex := range indexes { | 
					
						
							|  |  |  | 		parsed := dbutils.ParseIndex(rawIndex) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// always set a table name because it is ignored anyway in order to keep it in sync with the collection name
 | 
					
						
							|  |  |  | 		parsed.TableName = "validator" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if !parsed.IsValid() { | 
					
						
							|  |  |  | 			return validation.Errors{ | 
					
						
							|  |  |  | 				strconv.Itoa(i): validation.NewError( | 
					
						
							|  |  |  | 					"validation_invalid_index_expression", | 
					
						
							|  |  |  | 					"Invalid CREATE INDEX expression.", | 
					
						
							|  |  |  | 				), | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 		if _, isDuplicated := duplicatedNames[strings.ToLower(parsed.IndexName)]; isDuplicated { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			return validation.Errors{ | 
					
						
							|  |  |  | 				strconv.Itoa(i): validation.NewError( | 
					
						
							|  |  |  | 					"validation_duplicated_index_name", | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 					"The index name already exists.", | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				), | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 		duplicatedNames[strings.ToLower(parsed.IndexName)] = struct{}{} | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// ensure that the index name is not used in another collection
 | 
					
						
							|  |  |  | 		var usedTblName string | 
					
						
							|  |  |  | 		_ = cv.app.DB().Select("tbl_name"). | 
					
						
							|  |  |  | 			From("sqlite_master"). | 
					
						
							|  |  |  | 			AndWhere(dbx.HashExp{"type": "index"}). | 
					
						
							|  |  |  | 			AndWhere(dbx.NewExp("LOWER([[tbl_name]])!=LOWER({:oldName})", dbx.Params{"oldName": cv.original.Name})). | 
					
						
							|  |  |  | 			AndWhere(dbx.NewExp("LOWER([[tbl_name]])!=LOWER({:newName})", dbx.Params{"newName": cv.new.Name})). | 
					
						
							|  |  |  | 			AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:indexName})", dbx.Params{"indexName": parsed.IndexName})). | 
					
						
							|  |  |  | 			Limit(1). | 
					
						
							|  |  |  | 			Row(&usedTblName) | 
					
						
							|  |  |  | 		if usedTblName != "" { | 
					
						
							|  |  |  | 			return validation.Errors{ | 
					
						
							|  |  |  | 				strconv.Itoa(i): validation.NewError( | 
					
						
							|  |  |  | 					"validation_existing_index_name", | 
					
						
							|  |  |  | 					"The index name is already used in "+usedTblName+" collection.", | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 				).SetParams(map[string]any{"usedTableName": usedTblName}), | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// reset non-important identifiers
 | 
					
						
							|  |  |  | 		parsed.SchemaName = "validator" | 
					
						
							|  |  |  | 		parsed.IndexName = "validator" | 
					
						
							|  |  |  | 		parsedDef := parsed.Build() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if _, isDuplicated := duplicatedDefinitions[parsedDef]; isDuplicated { | 
					
						
							|  |  |  | 			return validation.Errors{ | 
					
						
							|  |  |  | 				strconv.Itoa(i): validation.NewError( | 
					
						
							|  |  |  | 					"validation_duplicated_index_definition", | 
					
						
							|  |  |  | 					"The index definition already exists.", | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				), | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 		duplicatedDefinitions[parsedDef] = struct{}{} | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// note: we don't check the index table name because it is always
 | 
					
						
							|  |  |  | 		// overwritten by the SyncRecordTableSchema to allow
 | 
					
						
							|  |  |  | 		// easier partial modifications (eg. changing only the collection name).
 | 
					
						
							|  |  |  | 		// if !strings.EqualFold(parsed.TableName, form.Name) {
 | 
					
						
							|  |  |  | 		// 	return validation.Errors{
 | 
					
						
							|  |  |  | 		// 		strconv.Itoa(i): validation.NewError(
 | 
					
						
							|  |  |  | 		// 			"validation_invalid_index_table",
 | 
					
						
							|  |  |  | 		// 			fmt.Sprintf("The index table must be the same as the collection name."),
 | 
					
						
							|  |  |  | 		// 		),
 | 
					
						
							|  |  |  | 		// 	}
 | 
					
						
							|  |  |  | 		// }
 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 	// ensure that unique indexes on system fields are not changed or removed
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	if !cv.original.IsNew() { | 
					
						
							|  |  |  | 	OLD_INDEXES_LOOP: | 
					
						
							|  |  |  | 		for _, oldIndex := range cv.original.Indexes { | 
					
						
							|  |  |  | 			oldParsed := dbutils.ParseIndex(oldIndex) | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 			if !oldParsed.Unique { | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-15 15:11:45 +08:00
										 |  |  | 			// reset collate and sort since they are not important for the unique constraint
 | 
					
						
							|  |  |  | 			for i := range oldParsed.Columns { | 
					
						
							|  |  |  | 				oldParsed.Columns[i].Collate = "" | 
					
						
							|  |  |  | 				oldParsed.Columns[i].Sort = "" | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 			oldParsedStr := oldParsed.Build() | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			for _, column := range oldParsed.Columns { | 
					
						
							|  |  |  | 				for _, f := range cv.original.Fields { | 
					
						
							|  |  |  | 					if !f.GetSystem() || !strings.EqualFold(column.Name, f.GetName()) { | 
					
						
							|  |  |  | 						continue | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 					var hasMatch bool | 
					
						
							|  |  |  | 					for _, newIndex := range cv.new.Indexes { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 						newParsed := dbutils.ParseIndex(newIndex) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 						// exclude the non-important identifiers from the check
 | 
					
						
							| 
									
										
										
										
											2024-11-15 15:11:45 +08:00
										 |  |  | 						newParsed.SchemaName = oldParsed.SchemaName | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 						newParsed.IndexName = oldParsed.IndexName | 
					
						
							|  |  |  | 						newParsed.TableName = oldParsed.TableName | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-15 15:11:45 +08:00
										 |  |  | 						// exclude partial constraints
 | 
					
						
							|  |  |  | 						newParsed.Where = oldParsed.Where | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// reset collate and sort
 | 
					
						
							|  |  |  | 						for i := range newParsed.Columns { | 
					
						
							|  |  |  | 							newParsed.Columns[i].Collate = "" | 
					
						
							|  |  |  | 							newParsed.Columns[i].Sort = "" | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 						if oldParsedStr == newParsed.Build() { | 
					
						
							|  |  |  | 							hasMatch = true | 
					
						
							| 
									
										
										
										
											2024-11-15 15:11:45 +08:00
										 |  |  | 							break | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 						} | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 					if !hasMatch { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 						return validation.NewError( | 
					
						
							| 
									
										
										
										
											2024-11-15 15:11:45 +08:00
										 |  |  | 							"validation_invalid_unique_system_field_index", | 
					
						
							|  |  |  | 							fmt.Sprintf("Unique index definition on system fields (%q) is invalid or missing.", f.GetName()), | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 						).SetParams(map[string]any{"fieldName": f.GetName()}) | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					continue OLD_INDEXES_LOOP | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// check for required indexes
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// note: this is in case the indexes were removed manually when creating/importing new auth collections
 | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 	// and technically it is not necessary because on app.Save() the missing indexes will be reinserted by the system collection hook
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	if cv.new.IsAuth() { | 
					
						
							|  |  |  | 		requiredNames := []string{FieldNameTokenKey, FieldNameEmail} | 
					
						
							|  |  |  | 		for _, name := range requiredNames { | 
					
						
							|  |  |  | 			if !dbutils.HasSingleColumnUniqueIndex(name, indexes) { | 
					
						
							|  |  |  | 				return validation.NewError( | 
					
						
							|  |  |  | 					"validation_missing_required_unique_index", | 
					
						
							|  |  |  | 					`Missing required unique index for field "`+name+`".`, | 
					
						
							| 
									
										
										
										
											2024-11-03 16:44:48 +08:00
										 |  |  | 				).SetParams(map[string]any{"fieldName": name}) | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (validator *collectionValidator) validateOptions() error { | 
					
						
							|  |  |  | 	switch validator.new.Type { | 
					
						
							|  |  |  | 	case CollectionTypeAuth: | 
					
						
							|  |  |  | 		return validator.new.collectionAuthOptions.validate(validator) | 
					
						
							|  |  |  | 	case CollectionTypeView: | 
					
						
							|  |  |  | 		return validator.new.collectionViewOptions.validate(validator) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } |