feat: Updates to use swift-validations for database.
All checks were successful
CI / Linux Tests (push) Successful in 6m28s

This commit is contained in:
2026-02-01 00:55:44 -05:00
parent a3fb87f86e
commit 9276f88426
20 changed files with 667 additions and 361 deletions

View File

@@ -0,0 +1,48 @@
import Validations
extension Validator {
static func validate<Child: Validatable>(
_ toChild: KeyPath<Value, [Child]>
)
-> Self
{
self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator())
}
static func validate<Child: Validatable>(
_ toChild: KeyPath<Value, [Child]?>
)
-> Self
{
self.mapValue({ $0[keyPath: toChild] }, with: ArrayValidator().optional())
}
}
extension Array where Element: Validatable {
static func validator() -> some Validation<Self> {
ArrayValidator<Element>()
}
}
struct ArrayValidator<Element>: Validation where Element: Validatable {
func validate(_ value: [Element]) throws {
for item in value {
try item.validate()
}
}
}
struct ForEachValidator<T, E>: Validation where T: Validation, T.Value == E {
let validator: T
init(@ValidationBuilder<E> builder: () -> T) {
self.validator = builder()
}
func validate(_ value: [E]) throws {
for item in value {
try validator.validate(item)
}
}
}

View File

@@ -4,6 +4,7 @@ import Fluent
import Foundation
import ManualDCore
import SQLKit
import Validations
extension DatabaseClient.ComponentLosses: TestDependencyKey {
public static let testValue = Self()
@@ -13,8 +14,8 @@ extension DatabaseClient.ComponentLosses {
public static func live(database: any Database) -> Self {
.init(
create: { request in
let model = try request.toModel()
try await model.save(on: database)
let model = request.toModel()
try await model.validateAndSave(on: database)
return try model.toDTO()
},
delete: { id in
@@ -35,13 +36,13 @@ extension DatabaseClient.ComponentLosses {
try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() }
},
update: { id, updates in
try updates.validate()
// try updates.validate()
guard let model = try await ComponentLossModel.find(id, on: database) else {
throw NotFoundError()
}
model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
}
@@ -51,40 +52,9 @@ extension DatabaseClient.ComponentLosses {
extension ComponentPressureLoss.Create {
func toModel() throws(ValidationError) -> ComponentLossModel {
try validate()
func toModel() -> ComponentLossModel {
return .init(name: name, value: value, projectID: projectID)
}
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Component loss name should not be empty.")
}
guard value > 0 else {
throw ValidationError("Component loss value should be greater than 0.")
}
guard value < 1.0 else {
throw ValidationError("Component loss value should be less than 1.0.")
}
}
}
extension ComponentPressureLoss.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Component loss name should not be empty.")
}
}
if let value {
guard value > 0 else {
throw ValidationError("Component loss value should be greater than 0.")
}
guard value < 1.0 else {
throw ValidationError("Component loss value should be less than 1.0.")
}
}
}
}
extension ComponentPressureLoss {
@@ -171,3 +141,19 @@ final class ComponentLossModel: Model, @unchecked Sendable {
}
}
}
extension ComponentLossModel: Validatable {
var body: some Validation<ComponentLossModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(\.value) {
Double.greaterThan(0.0)
Double.lessThanOrEquals(1.0)
}
.errorLabel("Value", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Validations
extension DatabaseClient.Equipment: TestDependencyKey {
public static let testValue = Self()
@@ -10,8 +11,8 @@ extension DatabaseClient.Equipment: TestDependencyKey {
public static func live(database: any Database) -> Self {
.init(
create: { request in
let model = try request.toModel()
try await model.save(on: database)
let model = request.toModel()
try await model.validateAndSave(on: database)
return try model.toDTO()
},
delete: { id in
@@ -37,10 +38,9 @@ extension DatabaseClient.Equipment: TestDependencyKey {
guard let model = try await EquipmentModel.find(id, on: database) else {
throw NotFoundError()
}
try updates.validate()
model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
}
@@ -50,8 +50,7 @@ extension DatabaseClient.Equipment: TestDependencyKey {
extension EquipmentInfo.Create {
func toModel() throws(ValidationError) -> EquipmentModel {
try validate()
func toModel() -> EquipmentModel {
return .init(
staticPressure: staticPressure,
heatingCFM: heatingCFM,
@@ -60,44 +59,6 @@ extension EquipmentInfo.Create {
)
}
func validate() throws(ValidationError) {
guard staticPressure >= 0 else {
throw ValidationError("Equipment info static pressure should be greater than 0.")
}
guard staticPressure <= 1.0 else {
throw ValidationError("Equipment info static pressure should be less than 1.0.")
}
guard heatingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
guard coolingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
}
}
extension EquipmentInfo.Update {
var hasUpdates: Bool {
staticPressure != nil || heatingCFM != nil || coolingCFM != nil
}
func validate() throws(ValidationError) {
if let staticPressure {
guard staticPressure >= 0 else {
throw ValidationError("Equipment info static pressure should be greater than 0.")
}
}
if let heatingCFM {
guard heatingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
}
if let coolingCFM {
guard coolingCFM >= 0 else {
throw ValidationError("Equipment info heating CFM should be greater than 0.")
}
}
}
}
extension EquipmentInfo {
@@ -197,3 +158,22 @@ final class EquipmentModel: Model, @unchecked Sendable {
}
}
}
extension EquipmentModel: Validatable {
var body: some Validation<EquipmentModel> {
Validator.accumulating {
Validator.validate(\.staticPressure) {
Double.greaterThan(0.0)
Double.lessThan(1.0)
}
.errorLabel("Static Pressure", inline: true)
Validator.validate(\.heatingCFM, with: .greaterThan(0))
.errorLabel("Heating CFM", inline: true)
Validator.validate(\.coolingCFM, with: .greaterThan(0))
.errorLabel("Cooling CFM", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Validations
extension DatabaseClient.EquivalentLengths: TestDependencyKey {
public static let testValue = Self()
@@ -11,7 +12,7 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey {
.init(
create: { request in
let model = try request.toModel()
try await model.save(on: database)
try await model.validateAndSave(on: database)
return try model.toDTO()
},
delete: { id in
@@ -53,7 +54,7 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey {
}
try model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
}
@@ -64,7 +65,9 @@ extension DatabaseClient.EquivalentLengths: TestDependencyKey {
extension EquivalentLength.Create {
func toModel() throws -> EffectiveLengthModel {
try validate()
if groups.count > 0 {
try [EquivalentLength.FittingGroup].validator().validate(groups)
}
return try .init(
name: name,
type: type.rawValue,
@@ -73,12 +76,6 @@ extension EquivalentLength.Create {
projectID: projectID
)
}
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Effective length name can not be empty.")
}
}
}
extension EquivalentLength {
@@ -184,7 +181,51 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
self.straightLengths = straightLengths
}
if let groups = updates.groups {
if groups.count > 0 {
try [EquivalentLength.FittingGroup].validator().validate(groups)
}
self.groups = try JSONEncoder().encode(groups)
}
}
}
extension EffectiveLengthModel: Validatable {
var body: some Validation<EffectiveLengthModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(
\.straightLengths,
with: [Int].empty().or(
ForEachValidator {
Int.greaterThan(0)
})
)
.errorLabel("Straight Lengths", inline: true)
}
}
}
extension EquivalentLength.FittingGroup: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
Validator.validate(\.group) {
Int.greaterThanOrEquals(1)
Int.lessThanOrEquals(12)
}
.errorLabel("Group", inline: true)
Validator.validate(\.letter, with: .regex(matching: "[a-zA-Z]"))
.errorLabel("Letter", inline: true)
Validator.validate(\.value, with: .greaterThan(0))
.errorLabel("Value", inline: true)
Validator.validate(\.quantity, with: .greaterThanOrEquals(1))
.errorLabel("Quantity", inline: true)
}
}
}

View File

@@ -0,0 +1,10 @@
import Fluent
import Validations
extension Model where Self: Validations.Validatable {
func validateAndSave(on database: any Database) async throws {
try self.validate()
try await self.save(on: database)
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Validations
extension DatabaseClient.Projects: TestDependencyKey {
public static let testValue = Self()
@@ -10,8 +11,8 @@ extension DatabaseClient.Projects: TestDependencyKey {
public static func live(database: any Database) -> Self {
.init(
create: { userID, request in
let model = try request.toModel(userID: userID)
try await model.save(on: database)
let model = request.toModel(userID: userID)
try await model.validateAndSave(on: database)
return try model.toDTO()
},
delete: { id in
@@ -81,10 +82,9 @@ extension DatabaseClient.Projects: TestDependencyKey {
guard let model = try await ProjectModel.find(id, on: database) else {
throw NotFoundError()
}
try updates.validate()
model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
}
@@ -94,8 +94,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
extension Project.Create {
func toModel(userID: User.ID) throws -> ProjectModel {
try validate()
func toModel(userID: User.ID) -> ProjectModel {
return .init(
name: name,
streetAddress: streetAddress,
@@ -106,70 +105,6 @@ extension Project.Create {
)
}
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Project name should not be empty.")
}
guard !streetAddress.isEmpty else {
throw ValidationError("Project street address should not be empty.")
}
guard !city.isEmpty else {
throw ValidationError("Project city should not be empty.")
}
guard !state.isEmpty else {
throw ValidationError("Project state should not be empty.")
}
guard !zipCode.isEmpty else {
throw ValidationError("Project zipCode should not be empty.")
}
if let sensibleHeatRatio {
guard sensibleHeatRatio >= 0 else {
throw ValidationError("Project sensible heat ratio should be greater than 0.")
}
guard sensibleHeatRatio <= 1 else {
throw ValidationError("Project sensible heat ratio should be less than 1.")
}
}
}
}
extension Project.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Project name should not be empty.")
}
}
if let streetAddress {
guard !streetAddress.isEmpty else {
throw ValidationError("Project street address should not be empty.")
}
}
if let city {
guard !city.isEmpty else {
throw ValidationError("Project city should not be empty.")
}
}
if let state {
guard !state.isEmpty else {
throw ValidationError("Project state should not be empty.")
}
}
if let zipCode {
guard !zipCode.isEmpty else {
throw ValidationError("Project zipCode should not be empty.")
}
}
if let sensibleHeatRatio {
guard sensibleHeatRatio >= 0 else {
throw ValidationError("Project sensible heat ratio should be greater than 0.")
}
guard sensibleHeatRatio <= 1 else {
throw ValidationError("Project sensible heat ratio should be less than 1.")
}
}
}
}
extension Project {
@@ -342,3 +277,35 @@ final class ProjectModel: Model, @unchecked Sendable {
return model
}
}
extension ProjectModel: Validatable {
var body: some Validation<ProjectModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(\.streetAddress, with: .notEmpty())
.errorLabel("Address", inline: true)
Validator.validate(\.city, with: .notEmpty())
.errorLabel("City", inline: true)
Validator.validate(\.state, with: .notEmpty())
.errorLabel("State", inline: true)
Validator.validate(\.zipCode, with: .notEmpty())
.errorLabel("Zip", inline: true)
Validator.validate(\.sensibleHeatRatio) {
Validator {
Double.greaterThan(0)
Double.lessThanOrEquals(1.0)
}
.optional()
}
.errorLabel("Sensible Heat Ratio", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Validations
extension DatabaseClient.Rooms: TestDependencyKey {
public static let testValue = Self()
@@ -11,7 +12,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
.init(
create: { request in
let model = try request.toModel()
try await model.save(on: database)
try await model.validateAndSave(on: database)
return try model.toDTO()
},
delete: { id in
@@ -31,7 +32,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
model.rectangularSizes = nil
}
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
},
@@ -50,11 +51,9 @@ extension DatabaseClient.Rooms: TestDependencyKey {
guard let model = try await RoomModel.find(id, on: database) else {
throw NotFoundError()
}
try updates.validate()
model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
},
@@ -77,8 +76,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
extension Room.Create {
func toModel() throws(ValidationError) -> RoomModel {
try validate()
func toModel() throws -> RoomModel {
return .init(
name: name,
heatingLoad: heatingLoad,
@@ -88,57 +86,6 @@ extension Room.Create {
projectID: projectID
)
}
func validate() throws(ValidationError) {
guard !name.isEmpty else {
throw ValidationError("Room name should not be empty.")
}
guard heatingLoad >= 0 else {
throw ValidationError("Room heating load should not be less than 0.")
}
guard coolingTotal >= 0 else {
throw ValidationError("Room cooling total should not be less than 0.")
}
if let coolingSensible {
guard coolingSensible >= 0 else {
throw ValidationError("Room cooling sensible should not be less than 0.")
}
}
guard registerCount >= 1 else {
throw ValidationError("Room cooling sensible should not be less than 1.")
}
}
}
extension Room.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Room name should not be empty.")
}
}
if let heatingLoad {
guard heatingLoad >= 0 else {
throw ValidationError("Room heating load should not be less than 0.")
}
}
if let coolingTotal {
guard coolingTotal >= 0 else {
throw ValidationError("Room cooling total should not be less than 0.")
}
}
if let coolingSensible {
guard coolingSensible >= 0 else {
throw ValidationError("Room cooling sensible should not be less than 0.")
}
}
if let registerCount {
guard registerCount >= 1 else {
throw ValidationError("Room cooling sensible should not be less than 1.")
}
}
}
}
extension Room {
@@ -169,7 +116,7 @@ extension Room {
}
}
final class RoomModel: Model, @unchecked Sendable {
final class RoomModel: Model, @unchecked Sendable, Validatable {
static let schema = "room"
@@ -267,4 +214,38 @@ final class RoomModel: Model, @unchecked Sendable {
}
var body: some Validation<RoomModel> {
Validator.accumulating {
Validator.validate(\.name, with: .notEmpty())
.errorLabel("Name", inline: true)
Validator.validate(\.heatingLoad, with: .greaterThanOrEquals(0))
.errorLabel("Heating Load", inline: true)
Validator.validate(\.coolingTotal, with: .greaterThanOrEquals(0))
.errorLabel("Cooling Total", inline: true)
Validator.validate(\.coolingSensible, with: Double.greaterThanOrEquals(0).optional())
.errorLabel("Cooling Sensible", inline: true)
Validator.validate(\.registerCount, with: .greaterThanOrEquals(1))
.errorLabel("Register Count", inline: true)
Validator.validate(\.rectangularSizes)
}
}
}
extension Room.RectangularSize: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
Validator.validate(\.register, with: Int.greaterThanOrEquals(1).optional())
.errorLabel("Register", inline: true)
Validator.validate(\.height, with: Int.greaterThanOrEquals(1))
.errorLabel("Height", inline: true)
}
}
}

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Validations
extension DatabaseClient.TrunkSizes: TestDependencyKey {
public static let testValue = Self()
@@ -10,12 +11,12 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
public static func live(database: any Database) -> Self {
.init(
create: { request in
try request.validate()
// try request.validate()
let trunk = request.toModel()
var roomProxies = [TrunkSize.RoomProxy]()
try await trunk.save(on: database)
try await trunk.validateAndSave(on: database)
for (roomID, registers) in request.rooms {
guard let room = try await RoomModel.find(roomID, on: database) else {
@@ -27,7 +28,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
registers: registers,
type: request.type
)
try await model.save(on: database)
try await model.validateAndSave(on: database)
roomProxies.append(
.init(room: try room.toDTO(), registers: registers)
)
@@ -80,7 +81,7 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
else {
throw NotFoundError()
}
try updates.validate()
// try updates.validate()
try await model.applyUpdates(updates, on: database)
return try model.toDTO()
}
@@ -90,17 +91,6 @@ extension DatabaseClient.TrunkSizes: TestDependencyKey {
extension TrunkSize.Create {
func validate() throws(ValidationError) {
guard rooms.count > 0 else {
throw ValidationError("Trunk size should have associated rooms / registers.")
}
if let height {
guard height > 0 else {
throw ValidationError("Trunk size height should be greater than 0.")
}
}
}
func toModel() -> TrunkModel {
.init(
projectID: projectID,
@@ -111,21 +101,6 @@ extension TrunkSize.Create {
}
}
extension TrunkSize.Update {
func validate() throws(ValidationError) {
if let rooms {
guard rooms.count > 0 else {
throw ValidationError("Trunk size should have associated rooms / registers.")
}
}
if let height {
guard height > 0 else {
throw ValidationError("Trunk size height should be greater than 0.")
}
}
}
}
extension TrunkSize {
struct Migrate: AsyncMigration {
@@ -205,7 +180,17 @@ final class TrunkRoomModel: Model, @unchecked Sendable {
registers: registers
)
}
}
extension TrunkRoomModel: Validatable {
var body: some Validation<TrunkRoomModel> {
Validator.validate(\.registers) {
[Int].notEmpty()
ForEachValidator {
Int.greaterThanOrEquals(1)
}
}
}
}
final class TrunkModel: Model, @unchecked Sendable {
@@ -276,7 +261,7 @@ final class TrunkModel: Model, @unchecked Sendable {
self.name = name
}
if hasChanges {
try await self.save(on: database)
try await self.validateAndSave(on: database)
}
guard let updateRooms = updates.rooms else {
@@ -297,7 +282,7 @@ final class TrunkModel: Model, @unchecked Sendable {
currRoom.registers = registers
}
if currRoom.hasChanges {
try await currRoom.save(on: database)
try await currRoom.validateAndSave(on: database)
}
} else {
database.logger.debug("CREATING NEW TrunkRoomModel")
@@ -324,6 +309,21 @@ final class TrunkModel: Model, @unchecked Sendable {
}
}
extension TrunkModel: Validatable {
var body: some Validation<TrunkModel> {
Validator.accumulating {
Validator.validate(\.height, with: Int.greaterThan(0).optional())
.errorLabel("Height", inline: true)
Validator.validate(\.name, with: String.notEmpty().optional())
.errorLabel("Name", inline: true)
}
}
}
extension Array where Element == TrunkModel {
func toDTO() throws -> [TrunkSize] {

View File

@@ -0,0 +1,20 @@
import ManualDCore
import Validations
// Declaring this in seperate file because some Vapor imports
// have same name's and this was easiest solution.
extension User.Create: Validatable {
public var body: some Validation<Self> {
Validator.accumulating {
Validator.validate(\.email, with: .email())
.errorLabel("Email", inline: true)
Validator.validate(\.password.count, with: .greaterThanOrEquals(8))
.errorLabel("Password Count", inline: true)
Validator.validate(\.confirmPassword, with: .equals(password))
.mapError(ValidationError("Confirm password does not match."))
.errorLabel("Confirm Password", inline: true)
}
}
}

View File

@@ -1,8 +1,9 @@
import Dependencies
import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Vapor
import Validations
extension DatabaseClient.UserProfiles: TestDependencyKey {
@@ -11,9 +12,8 @@ extension DatabaseClient.UserProfiles: TestDependencyKey {
public static func live(database: any Database) -> Self {
.init(
create: { profile in
try profile.validate()
let model = profile.toModel()
try await model.save(on: database)
try await model.validateAndSave(on: database)
return try model.toDTO()
},
delete: { id in
@@ -37,10 +37,9 @@ extension DatabaseClient.UserProfiles: TestDependencyKey {
guard let model = try await UserProfileModel.find(id, on: database) else {
throw NotFoundError()
}
try updates.validate()
model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
try await model.validateAndSave(on: database)
}
return try model.toDTO()
}
@@ -50,30 +49,6 @@ extension DatabaseClient.UserProfiles: TestDependencyKey {
extension User.Profile.Create {
func validate() throws(ValidationError) {
guard !firstName.isEmpty else {
throw ValidationError("User first name should not be empty.")
}
guard !lastName.isEmpty else {
throw ValidationError("User last name should not be empty.")
}
guard !companyName.isEmpty else {
throw ValidationError("User company name should not be empty.")
}
guard !streetAddress.isEmpty else {
throw ValidationError("User street address should not be empty.")
}
guard !city.isEmpty else {
throw ValidationError("User city should not be empty.")
}
guard !state.isEmpty else {
throw ValidationError("User state should not be empty.")
}
guard !zipCode.isEmpty else {
throw ValidationError("User zip code should not be empty.")
}
}
func toModel() -> UserProfileModel {
.init(
userID: userID,
@@ -89,47 +64,6 @@ extension User.Profile.Create {
}
}
extension User.Profile.Update {
func validate() throws(ValidationError) {
if let firstName {
guard !firstName.isEmpty else {
throw ValidationError("User first name should not be empty.")
}
}
if let lastName {
guard !lastName.isEmpty else {
throw ValidationError("User last name should not be empty.")
}
}
if let companyName {
guard !companyName.isEmpty else {
throw ValidationError("User company name should not be empty.")
}
}
if let streetAddress {
guard !streetAddress.isEmpty else {
throw ValidationError("User street address should not be empty.")
}
}
if let city {
guard !city.isEmpty else {
throw ValidationError("User city should not be empty.")
}
}
if let state {
guard !state.isEmpty else {
throw ValidationError("User state should not be empty.")
}
}
if let zipCode {
guard !zipCode.isEmpty else {
throw ValidationError("User zip code should not be empty.")
}
}
}
}
extension User.Profile {
struct Migrate: AsyncMigration {
@@ -270,3 +204,31 @@ final class UserProfileModel: Model, @unchecked Sendable {
}
}
extension UserProfileModel: Validatable {
var body: some Validation<UserProfileModel> {
Validator.accumulating {
Validator.validate(\.firstName, with: .notEmpty())
.errorLabel("First Name", inline: true)
Validator.validate(\.lastName, with: .notEmpty())
.errorLabel("Last Name", inline: true)
Validator.validate(\.companyName, with: .notEmpty())
.errorLabel("Company", inline: true)
Validator.validate(\.streetAddress, with: .notEmpty())
.errorLabel("Address", inline: true)
Validator.validate(\.city, with: .notEmpty())
.errorLabel("City", inline: true)
Validator.validate(\.state, with: .notEmpty())
.errorLabel("State", inline: true)
Validator.validate(\.zipCode, with: .notEmpty())
.errorLabel("Zip", inline: true)
}
}
}

View File

@@ -1,6 +1,7 @@
import Dependencies
import DependenciesMacros
import Fluent
import Foundation
import ManualDCore
import Vapor
@@ -10,6 +11,7 @@ extension DatabaseClient.Users: TestDependencyKey {
public static func live(database: any Database) -> Self {
.init(
create: { request in
try request.validate()
let model = try request.toModel()
try await model.save(on: database)
return try model.toDTO()
@@ -118,21 +120,8 @@ extension User {
extension User.Create {
func toModel() throws -> UserModel {
try validate()
return try .init(email: email, passwordHash: User.hashPassword(password))
}
func validate() throws {
guard !email.isEmpty else {
throw ValidationError("Email should not be empty")
}
guard password.count > 8 else {
throw ValidationError("Password should be more than 8 characters long.")
}
guard password == confirmPassword else {
throw ValidationError("Passwords do not match.")
}
}
}
final class UserModel: Model, @unchecked Sendable {