diff --git a/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift b/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift index cca60a2d50a..f75a6d51a27 100644 --- a/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift +++ b/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift @@ -10,6 +10,7 @@ struct V001InitialSchema { try createSiteTable(db) try createProductTable(db) try createProductAttributeTable(db) + try createImageTable(db) try createProductImageTable(db) try createProductVariationTable(db) try createProductVariationAttributeTable(db) @@ -74,23 +75,39 @@ struct V001InitialSchema { } } + private static func createImageTable(_ db: Database) throws { + // Single image table shared by products and variations + try db.create(table: "image") { imageTable in + imageTable.column("id", .integer).notNull() + imageTable.primaryKey(["siteID", "id"]) // SiteID column created by belongsTo relationship + imageTable.belongsTo("site", onDelete: .cascade) + + imageTable.column("dateCreated", .datetime).notNull() + imageTable.column("dateModified", .datetime) + + imageTable.column("src", .text).notNull() + imageTable.column("name", .text) + imageTable.column("alt", .text) + } + } + private static func createProductImageTable(_ db: Database) throws { + // Join table for many-to-many relationship between products and images try db.create(table: "productImage") { productImageTable in productImageTable.column("siteID", .integer).notNull() - productImageTable.column("id", .integer).notNull() - productImageTable.primaryKey(["siteID", "id"]) productImageTable.column("productID", .integer).notNull() + productImageTable.column("imageID", .integer).notNull() + productImageTable.primaryKey(["siteID", "productID", "imageID"]) + productImageTable.foreignKey(["siteID", "productID"], references: "product", columns: ["siteID", "id"], onDelete: .cascade) - productImageTable.column("dateCreated", .datetime).notNull() - productImageTable.column("dateModified", .datetime) - - productImageTable.column("src", .text).notNull() - productImageTable.column("name", .text) - productImageTable.column("alt", .text) + productImageTable.foreignKey(["siteID", "imageID"], + references: "image", + columns: ["siteID", "id"], + onDelete: .cascade) } } @@ -138,22 +155,22 @@ struct V001InitialSchema { } private static func createProductVariationImageTable(_ db: Database) throws { + // Join table for many-to-many relationship between product variations and images try db.create(table: "productVariationImage") { productVariationImageTable in productVariationImageTable.column("siteID", .integer).notNull() - productVariationImageTable.column("id", .integer).notNull() - productVariationImageTable.primaryKey(["siteID", "id"]) productVariationImageTable.column("productVariationID", .integer).notNull() + productVariationImageTable.column("imageID", .integer).notNull() + productVariationImageTable.primaryKey(["siteID", "productVariationID", "imageID"]) + productVariationImageTable.foreignKey(["siteID", "productVariationID"], references: "productVariation", columns: ["siteID", "id"], onDelete: .cascade) - productVariationImageTable.column("dateCreated", .datetime).notNull() - productVariationImageTable.column("dateModified", .datetime) - - productVariationImageTable.column("src", .text).notNull() - productVariationImageTable.column("name", .text) - productVariationImageTable.column("alt", .text) + productVariationImageTable.foreignKey(["siteID", "imageID"], + references: "image", + columns: ["siteID", "id"], + onDelete: .cascade) } } } diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedImage.swift b/Modules/Sources/Storage/GRDB/Model/PersistedImage.swift new file mode 100644 index 00000000000..ff916fab25d --- /dev/null +++ b/Modules/Sources/Storage/GRDB/Model/PersistedImage.swift @@ -0,0 +1,60 @@ +import Foundation +import GRDB + +// periphery:ignore +public struct PersistedImage: Codable { + public let siteID: Int64 + public let id: Int64 + public let dateCreated: Date + public let dateModified: Date? + public let src: String + public let name: String? + public let alt: String? + + public init(siteID: Int64, + id: Int64, + dateCreated: Date, + dateModified: Date?, + src: String, + name: String?, + alt: String?) { + self.siteID = siteID + self.id = id + self.dateCreated = dateCreated + self.dateModified = dateModified + self.src = src + self.name = name + self.alt = alt + } +} + +// periphery:ignore - TODO: remove ignore when populating database +extension PersistedImage: FetchableRecord, PersistableRecord { + public static var databaseTableName: String { "image" } + + public static var primaryKey: [String] { [CodingKeys.siteID.stringValue, CodingKeys.id.stringValue] } + + public enum Columns { + public static let siteID = Column(CodingKeys.siteID) + public static let id = Column(CodingKeys.id) + public static let dateCreated = Column(CodingKeys.dateCreated) + public static let dateModified = Column(CodingKeys.dateModified) + public static let src = Column(CodingKeys.src) + public static let name = Column(CodingKeys.name) + public static let alt = Column(CodingKeys.alt) + } +} + + +// periphery:ignore - TODO: remove ignore when populating database +extension PersistedImage { + enum CodingKeys: String, CodingKey { + case siteID + case id + case dateCreated + case dateModified + case src + case name + case alt + } +} diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift b/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift index e7102605fc9..77531e9b80d 100644 --- a/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift +++ b/Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift @@ -72,10 +72,17 @@ extension PersistedProduct: FetchableRecord, PersistableRecord { public static let stockStatusKey = Column(CodingKeys.stockStatusKey) } - public static let images = hasMany(PersistedProductImage.self, - using: ForeignKey([PersistedProductImage.CodingKeys.siteID.stringValue, - PersistedProductImage.CodingKeys.productID.stringValue], - to: primaryKey)) + // Join table association (internal - used by 'images' through association) + private static let productImages = hasMany(PersistedProductImage.self, + using: ForeignKey([PersistedProductImage.CodingKeys.siteID.stringValue, + PersistedProductImage.CodingKeys.productID.stringValue], + to: primaryKey)) + + // Through association to access actual images via join table (use this to fetch images) + public static let images = hasMany(PersistedImage.self, + through: productImages, + using: PersistedProductImage.image) + public static let attributes = hasMany(PersistedProductAttribute.self, using: ForeignKey([PersistedProductAttribute.CodingKeys.siteID.stringValue, PersistedProductAttribute.CodingKeys.productID.stringValue], diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedProductImage.swift b/Modules/Sources/Storage/GRDB/Model/PersistedProductImage.swift index 2938bf66062..66e6e141d0c 100644 --- a/Modules/Sources/Storage/GRDB/Model/PersistedProductImage.swift +++ b/Modules/Sources/Storage/GRDB/Model/PersistedProductImage.swift @@ -2,32 +2,18 @@ import Foundation import GRDB // periphery:ignore - TODO: remove ignore when populating database +/// Join table linking products to images (many-to-many relationship) public struct PersistedProductImage: Codable { public let siteID: Int64 - public let id: Int64 public let productID: Int64 - public let dateCreated: Date - public let dateModified: Date? - public let src: String - public let name: String? - public let alt: String? + public let imageID: Int64 public init(siteID: Int64, - id: Int64, productID: Int64, - dateCreated: Date, - dateModified: Date?, - src: String, - name: String?, - alt: String?) { + imageID: Int64) { self.siteID = siteID - self.id = id self.productID = productID - self.dateCreated = dateCreated - self.dateModified = dateModified - self.src = src - self.name = name - self.alt = alt + self.imageID = imageID } } @@ -35,18 +21,21 @@ public struct PersistedProductImage: Codable { extension PersistedProductImage: FetchableRecord, PersistableRecord { public static var databaseTableName: String { "productImage" } - public static var primaryKey: [String] { [CodingKeys.siteID.stringValue, CodingKeys.id.stringValue] } + public static var primaryKey: [String] { + [CodingKeys.siteID.stringValue, CodingKeys.productID.stringValue, CodingKeys.imageID.stringValue] + } public enum Columns { public static let siteID = Column(CodingKeys.siteID) - public static let id = Column(CodingKeys.id) public static let productID = Column(CodingKeys.productID) - public static let dateCreated = Column(CodingKeys.dateCreated) - public static let dateModified = Column(CodingKeys.dateModified) - public static let src = Column(CodingKeys.src) - public static let name = Column(CodingKeys.name) - public static let alt = Column(CodingKeys.alt) + public static let imageID = Column(CodingKeys.imageID) } + + // Association to the actual image + public static let image = belongsTo(PersistedImage.self, + using: ForeignKey([CodingKeys.siteID.stringValue, + CodingKeys.imageID.stringValue], + to: PersistedImage.primaryKey)) } @@ -54,12 +43,7 @@ extension PersistedProductImage: FetchableRecord, PersistableRecord { extension PersistedProductImage { enum CodingKeys: String, CodingKey { case siteID - case id case productID - case dateCreated - case dateModified - case src - case name - case alt + case imageID } } diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedProductVariation.swift b/Modules/Sources/Storage/GRDB/Model/PersistedProductVariation.swift index b37be1909d9..2241ff89dee 100644 --- a/Modules/Sources/Storage/GRDB/Model/PersistedProductVariation.swift +++ b/Modules/Sources/Storage/GRDB/Model/PersistedProductVariation.swift @@ -65,11 +65,19 @@ extension PersistedProductVariation: FetchableRecord, PersistableRecord { using: ForeignKey([PersistedProductVariationAttribute.CodingKeys.siteID.stringValue, PersistedProductVariationAttribute.CodingKeys.productVariationID.stringValue], to: primaryKey)) - public static let image = hasOne(PersistedProductVariationImage.self, - key: "image", - using: ForeignKey([PersistedProductVariationImage.CodingKeys.siteID.stringValue, - PersistedProductVariationImage.CodingKeys.productVariationID.stringValue], - to: primaryKey)) + + // Join table association (internal - used by 'image' through association) + private static let productVariationImage = hasOne(PersistedProductVariationImage.self, + key: "productVariationImage", + using: ForeignKey([PersistedProductVariationImage.CodingKeys.siteID.stringValue, + PersistedProductVariationImage.CodingKeys.productVariationID.stringValue], + to: primaryKey)) + + // Through association to access actual image via join table (use this to fetch image) + public static let image = hasOne(PersistedImage.self, + through: productVariationImage, + using: PersistedProductVariationImage.image, + key: "image") } diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedProductVariationImage.swift b/Modules/Sources/Storage/GRDB/Model/PersistedProductVariationImage.swift index 3bedeb9cb7e..27b1c37c925 100644 --- a/Modules/Sources/Storage/GRDB/Model/PersistedProductVariationImage.swift +++ b/Modules/Sources/Storage/GRDB/Model/PersistedProductVariationImage.swift @@ -2,32 +2,18 @@ import Foundation import GRDB // periphery:ignore - TODO: remove ignore when populating database +/// Join table linking product variations to images (many-to-many relationship) public struct PersistedProductVariationImage: Codable { public let siteID: Int64 - public let id: Int64 public let productVariationID: Int64 - public let dateCreated: Date - public let dateModified: Date? - public let src: String - public let name: String? - public let alt: String? + public let imageID: Int64 public init(siteID: Int64, - id: Int64, productVariationID: Int64, - dateCreated: Date, - dateModified: Date?, - src: String, - name: String?, - alt: String?) { + imageID: Int64) { self.siteID = siteID - self.id = id self.productVariationID = productVariationID - self.dateCreated = dateCreated - self.dateModified = dateModified - self.src = src - self.name = name - self.alt = alt + self.imageID = imageID } } @@ -35,18 +21,21 @@ public struct PersistedProductVariationImage: Codable { extension PersistedProductVariationImage: FetchableRecord, PersistableRecord { public static var databaseTableName: String { "productVariationImage" } - public static var primaryKey: [String] { [CodingKeys.siteID.stringValue, CodingKeys.id.stringValue] } + public static var primaryKey: [String] { + [CodingKeys.siteID.stringValue, CodingKeys.productVariationID.stringValue, CodingKeys.imageID.stringValue] + } public enum Columns { public static let siteID = Column(CodingKeys.siteID) - public static let id = Column(CodingKeys.id) public static let productVariationID = Column(CodingKeys.productVariationID) - public static let dateCreated = Column(CodingKeys.dateCreated) - public static let dateModified = Column(CodingKeys.dateModified) - public static let src = Column(CodingKeys.src) - public static let name = Column(CodingKeys.name) - public static let alt = Column(CodingKeys.alt) + public static let imageID = Column(CodingKeys.imageID) } + + // Association to the actual image + public static let image = belongsTo(PersistedImage.self, + using: ForeignKey([CodingKeys.siteID.stringValue, + CodingKeys.imageID.stringValue], + to: PersistedImage.primaryKey)) } @@ -54,12 +43,7 @@ extension PersistedProductVariationImage: FetchableRecord, PersistableRecord { extension PersistedProductVariationImage { enum CodingKeys: String, CodingKey { case siteID - case id case productVariationID - case dateCreated - case dateModified - case src - case name - case alt + case imageID } } diff --git a/Modules/Sources/Yosemite/Model/Storage/PersistedImage+Conversions.swift b/Modules/Sources/Yosemite/Model/Storage/PersistedImage+Conversions.swift new file mode 100644 index 00000000000..44e6c307f1f --- /dev/null +++ b/Modules/Sources/Yosemite/Model/Storage/PersistedImage+Conversions.swift @@ -0,0 +1,29 @@ +import Foundation +import Storage + +// MARK: - PersistedImage Conversions +public extension PersistedImage { + /// Create a PersistedImage from a ProductImage + static func make(from productImage: ProductImage, siteID: Int64) -> PersistedImage { + return PersistedImage( + siteID: siteID, + id: productImage.imageID, + dateCreated: productImage.dateCreated, + dateModified: productImage.dateModified, + src: productImage.src, + name: productImage.name, + alt: productImage.alt + ) + } + + func toProductImage() -> ProductImage { + return ProductImage( + imageID: id, + dateCreated: dateCreated, + dateModified: dateModified, + src: src, + name: name, + alt: alt + ) + } +} diff --git a/Modules/Sources/Yosemite/Model/Storage/PersistedProduct+Conversions.swift b/Modules/Sources/Yosemite/Model/Storage/PersistedProduct+Conversions.swift index 1d1a3442e11..3f5fe127ad0 100644 --- a/Modules/Sources/Yosemite/Model/Storage/PersistedProduct+Conversions.swift +++ b/Modules/Sources/Yosemite/Model/Storage/PersistedProduct+Conversions.swift @@ -67,10 +67,17 @@ extension POSProduct { let product = PersistedProduct(from: self) try product.insert(db) - // Save related images + // Save related images and join table entries for image in self.images { - let persistedImage = PersistedProductImage(from: image, siteID: self.siteID, productID: self.productID) - try persistedImage.insert(db) + // Insert or update the image itself + let persistedImage = PersistedImage.make(from: image, siteID: self.siteID) + try persistedImage.save(db) + + // Create join table entry + let joinEntry = PersistedProductImage(siteID: self.siteID, + productID: self.productID, + imageID: image.imageID) + try joinEntry.insert(db) } // Save related attributes @@ -110,31 +117,3 @@ extension PersistedProductAttribute { ) } } - -// MARK: - PersistedProductImage Conversions -// periphery:ignore - TODO: remove ignore when populating database -extension PersistedProductImage { - init(from productImage: ProductImage, siteID: Int64, productID: Int64) { - self.init( - siteID: siteID, - id: productImage.imageID, - productID: productID, - dateCreated: productImage.dateCreated, - dateModified: productImage.dateModified, - src: productImage.src, - name: productImage.name, - alt: productImage.alt - ) - } - - func toProductImage() -> ProductImage { - return ProductImage( - imageID: id, - dateCreated: dateCreated, - dateModified: dateModified, - src: src, - name: name, - alt: alt - ) - } -} diff --git a/Modules/Sources/Yosemite/Model/Storage/PersistedProductVariation+Conversions.swift b/Modules/Sources/Yosemite/Model/Storage/PersistedProductVariation+Conversions.swift index 87beb417fe2..0ff7d24a370 100644 --- a/Modules/Sources/Yosemite/Model/Storage/PersistedProductVariation+Conversions.swift +++ b/Modules/Sources/Yosemite/Model/Storage/PersistedProductVariation+Conversions.swift @@ -63,8 +63,15 @@ extension POSProductVariation { // Save related image if present if let image = self.image { - let persistedImage = PersistedProductVariationImage(from: image, siteID: self.siteID, productVariationID: self.productVariationID) - try persistedImage.insert(db) + // Insert or update the image itself + let persistedImage = PersistedImage.make(from: image, siteID: self.siteID) + try persistedImage.save(db) + + // Create join table entry + let joinEntry = PersistedProductVariationImage(siteID: self.siteID, + productVariationID: self.productVariationID, + imageID: image.imageID) + try joinEntry.insert(db) } // Save related attributes @@ -97,31 +104,3 @@ extension PersistedProductVariationAttribute { ) } } - -// MARK: - PersistedProductVariationImage Conversions -// periphery:ignore - TODO: remove ignore when populating database -extension PersistedProductVariationImage { - public init(from productImage: ProductImage, siteID: Int64, productVariationID: Int64) { - self.init( - siteID: siteID, - id: productImage.imageID, - productVariationID: productVariationID, - dateCreated: productImage.dateCreated, - dateModified: productImage.dateModified, - src: productImage.src, - name: productImage.name, - alt: productImage.alt - ) - } - - public func toProductImage() -> ProductImage { - return ProductImage( - imageID: id, - dateCreated: dateCreated, - dateModified: dateModified, - src: src, - name: name, - alt: alt - ) - } -} diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift index 716feb6d5d4..447d83eb993 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift @@ -38,20 +38,26 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { try product.insert(db, onConflict: .replace) } - for image in catalog.productImagesToPersist { + for variation in catalog.variationsToPersist { + try variation.insert(db, onConflict: .replace) + } + + // Insert actual image data first (shared by products and variations) + for image in catalog.imagesToPersist { try image.insert(db, onConflict: .replace) } - for var attribute in catalog.productAttributesToPersist { - try attribute.insert(db) + // Then insert join table entries + for productImage in catalog.productImagesToPersist { + try productImage.insert(db, onConflict: .replace) } - for variation in catalog.variationsToPersist { - try variation.insert(db, onConflict: .replace) + for variationImage in catalog.variationImagesToPersist { + try variationImage.insert(db, onConflict: .replace) } - for image in catalog.variationImagesToPersist { - try image.insert(db, onConflict: .replace) + for var attribute in catalog.productAttributesToPersist { + try attribute.insert(db) } for var attribute in catalog.variationAttributesToPersist { @@ -82,6 +88,7 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { for product in catalog.productsToPersist { try product.insert(db, onConflict: .replace) + // Delete old join table entries for this product try PersistedProductImage .filter { $0.siteID == siteID && $0.productID == product.id } .deleteAll(db) @@ -91,17 +98,10 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { .deleteAll(db) } - for image in catalog.productImagesToPersist { - try image.insert(db, onConflict: .replace) - } - - for var attribute in catalog.productAttributesToPersist { - try attribute.insert(db, onConflict: .replace) - } - for variation in catalog.variationsToPersist { try variation.insert(db, onConflict: .replace) + // Delete old join table entries for this variation try PersistedProductVariationImage .filter { $0.siteID == siteID && $0.productVariationID == variation.id } .deleteAll(db) @@ -111,10 +111,24 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { .deleteAll(db) } + // Insert/update actual image data (shared by products and variations) + for image in catalog.imagesToPersist { + try image.insert(db, onConflict: .replace) + } + + // Insert new join table entries + for image in catalog.productImagesToPersist { + try image.insert(db, onConflict: .replace) + } + for image in catalog.variationImagesToPersist { try image.insert(db, onConflict: .replace) } + for var attribute in catalog.productAttributesToPersist { + try attribute.insert(db, onConflict: .replace) + } + for var attribute in catalog.variationAttributesToPersist { try attribute.insert(db, onConflict: .replace) } @@ -145,11 +159,37 @@ private extension POSCatalog { products.map { PersistedProduct(from: $0) } } + var imagesToPersist: [PersistedImage] { + let productImages = products.flatMap { product in + product.images.map { PersistedImage.make(from: $0, siteID: product.siteID) } + } + + let variationImages = variations.compactMap { variation -> PersistedImage? in + guard let image = variation.image else { return nil } + return PersistedImage.make(from: image, siteID: variation.siteID) + } + + return deduplicateImages(productImages + variationImages) + } + + func deduplicateImages(_ images: [PersistedImage]) -> [PersistedImage] { + // Deduplicate by imageID since multiple products/variations can share the same image + // (siteID is the same for all images in a catalog) + var imageDict = [Int64: PersistedImage]() + + for image in images { + imageDict[image.id] = image + } + + return Array(imageDict.values) + } + + // Join table entries for product-image relationships var productImagesToPersist: [PersistedProductImage] { products.flatMap { product in - product.images.map { PersistedProductImage(from: $0, - siteID: product.siteID, - productID: product.productID) } + product.images.map { PersistedProductImage(siteID: product.siteID, + productID: product.productID, + imageID: $0.imageID) } } } @@ -165,11 +205,12 @@ private extension POSCatalog { variations.map { PersistedProductVariation(from: $0) } } + // Join table entries for variation-image relationships var variationImagesToPersist: [PersistedProductVariationImage] { variations.compactMap { variation in - variation.image.map { PersistedProductVariationImage(from: $0, - siteID: variation.siteID, - productVariationID: variation.productVariationID) } + variation.image.map { PersistedProductVariationImage(siteID: variation.siteID, + productVariationID: variation.productVariationID, + imageID: $0.imageID) } } } diff --git a/Modules/Tests/YosemiteTests/Storage/PersistedProductTests.swift b/Modules/Tests/YosemiteTests/Storage/PersistedProductTests.swift index 8f22d38f88a..464376cb72a 100644 --- a/Modules/Tests/YosemiteTests/Storage/PersistedProductTests.swift +++ b/Modules/Tests/YosemiteTests/Storage/PersistedProductTests.swift @@ -73,22 +73,20 @@ struct PersistedProductTests { ) let productImages = [ - PersistedProductImage(siteID: siteID, - id: 200, - productID: productID, - dateCreated: Date(timeIntervalSince1970: 10), - dateModified: nil, - src: "https://example.com/p1.png", - name: "p1", - alt: "a1"), - PersistedProductImage(siteID: siteID, - id: 201, - productID: productID, - dateCreated: Date(timeIntervalSince1970: 11), - dateModified: Date(timeIntervalSince1970: 12), - src: "https://example.com/p2.png", - name: nil, - alt: nil) + PersistedImage(siteID: siteID, + id: 200, + dateCreated: Date(timeIntervalSince1970: 10), + dateModified: nil, + src: "https://example.com/p1.png", + name: "p1", + alt: "a1"), + PersistedImage(siteID: siteID, + id: 201, + dateCreated: Date(timeIntervalSince1970: 11), + dateModified: Date(timeIntervalSince1970: 12), + src: "https://example.com/p2.png", + name: nil, + alt: nil) ] let persistedAttributes = [ @@ -166,20 +164,19 @@ struct PersistedProductTests { ) try product.insert(db) - let image1 = PersistedProductImage( + // Insert images into the image table + let image1 = PersistedImage( siteID: 1, id: 200, - productID: 100, dateCreated: Date(timeIntervalSince1970: 1000), dateModified: nil, src: "https://example.com/img1.png", name: "Image 1", alt: "Alt text 1" ) - let image2 = PersistedProductImage( + let image2 = PersistedImage( siteID: 1, id: 201, - productID: 100, dateCreated: Date(timeIntervalSince1970: 2000), dateModified: Date(timeIntervalSince1970: 2500), src: "https://example.com/img2.png", @@ -189,6 +186,12 @@ struct PersistedProductTests { try image1.insert(db) try image2.insert(db) + // Create join table entries + let productImage1 = PersistedProductImage(siteID: 1, productID: 100, imageID: 200) + let productImage2 = PersistedProductImage(siteID: 1, productID: 100, imageID: 201) + try productImage1.insert(db) + try productImage2.insert(db) + var attribute1 = PersistedProductAttribute( siteID: 1, productID: 100, @@ -439,10 +442,9 @@ struct PersistedProductTests { #expect(globalAttr?.variation == false) } - @Test("PersistedProductImage init(from:) and toProductImage round-trip") + @Test("PersistedImage make(from:) and toProductImage round-trip") func product_image_round_trip() throws { // Given - let productID: Int64 = 40 let image = ProductImage(imageID: 400, dateCreated: Date(timeIntervalSince1970: 100), dateModified: Date(timeIntervalSince1970: 200), @@ -452,12 +454,12 @@ struct PersistedProductTests { // When let siteID: Int64 = 4 - let persisted = PersistedProductImage(from: image, siteID: siteID, productID: productID) + let persisted = PersistedImage.make(from: image, siteID: siteID) let back = persisted.toProductImage() // Then #expect(persisted.id == image.imageID) - #expect(persisted.productID == productID) + #expect(persisted.siteID == siteID) #expect(persisted.dateCreated == image.dateCreated) #expect(persisted.dateModified == image.dateModified) #expect(persisted.src == image.src) diff --git a/Modules/Tests/YosemiteTests/Storage/PersistedProductVariationTests.swift b/Modules/Tests/YosemiteTests/Storage/PersistedProductVariationTests.swift index cd3876da153..085eb128e2a 100644 --- a/Modules/Tests/YosemiteTests/Storage/PersistedProductVariationTests.swift +++ b/Modules/Tests/YosemiteTests/Storage/PersistedProductVariationTests.swift @@ -87,10 +87,9 @@ struct PersistedProductVariationTests { name: "Fit", option: "Slim") ] - let varImage = PersistedProductVariationImage( + let varImage = PersistedImage( siteID: siteID, id: 601, - productVariationID: variationID, dateCreated: Date(timeIntervalSince1970: 2000), dateModified: Date(timeIntervalSince1970: 3000), src: "https://example.com/vi.png", @@ -164,10 +163,10 @@ struct PersistedProductVariationTests { ) try variation.insert(db) - let variationImage = PersistedProductVariationImage( + // Insert image into image table + let variationImage = PersistedImage( siteID: 2, id: 600, - productVariationID: 500, dateCreated: Date(timeIntervalSince1970: 3000), dateModified: Date(timeIntervalSince1970: 3500), src: "https://example.com/var-img.png", @@ -176,6 +175,10 @@ struct PersistedProductVariationTests { ) try variationImage.insert(db) + // Create join table entry + let variationImageJoin = PersistedProductVariationImage(siteID: 2, productVariationID: 500, imageID: 600) + try variationImageJoin.insert(db) + var attr1 = PersistedProductVariationAttribute( siteID: 2, productVariationID: 500, @@ -197,7 +200,7 @@ struct PersistedProductVariationTests { // When we fetch it, specifying inclusion of image and attributes struct DetailedVariation: Decodable, FetchableRecord { var variation: PersistedProductVariation - var image: PersistedProductVariationImage + var image: PersistedImage var attributes: [PersistedProductVariationAttribute] enum CodingKeys: CodingKey { @@ -447,10 +450,9 @@ struct PersistedProductVariationTests { #expect(globalAttr?.option == "Navy") } - @Test("PersistedProductVariationImage init(from:) and toProductImage round-trip") + @Test("PersistedImage make(from:) and toProductImage round-trip for variation") func variation_image_round_trip() throws { // Given - let variationID: Int64 = 800 let image = ProductImage(imageID: 801, dateCreated: Date(timeIntervalSince1970: 4000), dateModified: nil, @@ -460,12 +462,12 @@ struct PersistedProductVariationTests { // When let siteID: Int64 = 8 - let persisted = PersistedProductVariationImage(from: image, siteID: siteID, productVariationID: variationID) + let persisted = PersistedImage.make(from: image, siteID: siteID) let back = persisted.toProductImage() // Then #expect(persisted.id == image.imageID) - #expect(persisted.productVariationID == variationID) + #expect(persisted.siteID == siteID) #expect(persisted.dateCreated == image.dateCreated) #expect(persisted.dateModified == image.dateModified) #expect(persisted.src == image.src) diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift index 49b0a3788ec..5846459e11c 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift @@ -120,11 +120,14 @@ struct POSCatalogPersistenceServiceTests { // Then - should not fail and should handle duplicates try await db.read { db in let productCount = try PersistedProduct.fetchCount(db) - let imageCount = try PersistedProductImage.fetchCount(db) + let joinCount = try PersistedProductImage.fetchCount(db) + let imageCount = try PersistedImage.fetchCount(db) #expect(productCount == 2) - // While there's only one, the current implementation doesn't - // have a join table so only one product has a reference to it + // Two products share the same image, so: + // - 2 join table entries (one per product) + // - 1 actual image record (shared) + #expect(joinCount == 2) #expect(imageCount == 1) } } @@ -301,7 +304,7 @@ struct POSCatalogPersistenceServiceTests { } } - @Test func persistIncrementalCatalogData_replaces_images_for_updated_product() async throws { + @Test func persistIncrementalCatalogData_updates_and_adds_images_for_updated_product_but_does_not_delete() async throws { // Given let image1 = ProductImage.fake().copy(imageID: 1, src: "https://example.com/image1.jpg") let image2 = ProductImage.fake().copy(imageID: 2, src: "https://example.com/image2.jpg") @@ -317,14 +320,24 @@ struct POSCatalogPersistenceServiceTests { // Then try await grdbManager.databaseConnection.read { db in - let attributeCount = try PersistedProductImage.fetchCount(db) - #expect(attributeCount == 2) - - let attributes = try PersistedProductImage.fetchAll(db).sorted(by: { $0.id < $1.id }) - #expect(attributes[0].src == "https://example.com/image1-1.jpg") - #expect(attributes[0].productID == 1) - #expect(attributes[1].src == "https://example.com/image3.jpg") - #expect(attributes[1].productID == 1) + // Check join table has correct count + let joinCount = try PersistedProductImage.fetchCount(db) + #expect(joinCount == 2) + + // Check join table entries + let joins = try PersistedProductImage.fetchAll(db).sorted(by: { $0.imageID < $1.imageID }) + #expect(joins.count == 2) + #expect(joins[0].productID == 1) + #expect(joins[0].imageID == 1) + #expect(joins[1].productID == 1) + #expect(joins[1].imageID == 3) + + // Check actual images + let images = try PersistedImage.fetchAll(db).sorted(by: { $0.id < $1.id }) + #expect(images.count == 3) // `image2` remains, but is unlinked. + #expect(images[0].src == "https://example.com/image1-1.jpg") + #expect(images[1].src == "https://example.com/image2.jpg") + #expect(images[2].src == "https://example.com/image3.jpg") } } @@ -449,12 +462,17 @@ struct POSCatalogPersistenceServiceTests { // Then try await grdbManager.databaseConnection.read { db in - let imageCount = try PersistedProductVariationImage.fetchCount(db) - #expect(imageCount == 1) + // Check join table + let joinCount = try PersistedProductVariationImage.fetchCount(db) + #expect(joinCount == 1) + + let join = try PersistedProductVariationImage.fetchOne(db) + #expect(join?.productVariationID == 1) + #expect(join?.imageID == 1) - let image = try PersistedProductVariationImage.fetchOne(db) + // Check actual image + let image = try PersistedImage.fetchOne(db) #expect(image?.src == "https://example.com/variation1-updated.jpg") - #expect(image?.productVariationID == 1) } }