Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ struct V001InitialSchema {
// not derive them from a Swift class.
// https://swiftpackageindex.com/groue/grdb.swift/v7.6.1/documentation/grdb/migrations#Good-Practices-for-Defining-Migrations
try db.create(table: "product") { productTable in
productTable.primaryKey("id", .integer).notNull()
productTable.column("id", .integer).notNull()
productTable.primaryKey(["siteID", "id"]) // SiteID column created by belongsTo relationship
productTable.belongsTo("site", onDelete: .cascade).notNull()

productTable.column("name", .text).notNull()
Expand All @@ -56,7 +57,12 @@ struct V001InitialSchema {
try db.create(table: "productAttribute") { productAttributeTable in
// This table holds local product attributes only. Global attributes belong to a site.
productAttributeTable.autoIncrementedPrimaryKey("id").notNull()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is it worth adding a separate column for the "attribute ID" from the API response, if we ever want to differentiate between local vs. global attributes? Similar question for the variation attribute entity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a separate task on my list (possibly not in linear yet, I can't remember) but I'm planning to make the schema support both types properly before we finalise it. It's just less urgent than this change.

productAttributeTable.belongsTo("product", onDelete: .cascade).notNull()
productAttributeTable.column("siteID", .integer).notNull()
productAttributeTable.column("productID", .integer).notNull()
productAttributeTable.foreignKey(["siteID", "productID"],
references: "product",
columns: ["siteID", "id"],
onDelete: .cascade)

productAttributeTable.column("name", .text).notNull()
productAttributeTable.column("position", .integer).notNull()
Expand All @@ -68,8 +74,14 @@ struct V001InitialSchema {

private static func createProductImageTable(_ db: Database) throws {
try db.create(table: "productImage") { productImageTable in
productImageTable.primaryKey("id", .integer).notNull()
productImageTable.belongsTo("product", onDelete: .cascade).notNull()
productImageTable.column("siteID", .integer).notNull()
productImageTable.column("id", .integer).notNull()
productImageTable.primaryKey(["siteID", "id"])
productImageTable.column("productID", .integer).notNull()
productImageTable.foreignKey(["siteID", "productID"],
references: "product",
columns: ["siteID", "id"],
onDelete: .cascade)

productImageTable.column("dateCreated", .datetime).notNull()
productImageTable.column("dateModified", .datetime)
Expand All @@ -82,9 +94,14 @@ struct V001InitialSchema {

private static func createProductVariationTable(_ db: Database) throws {
try db.create(table: "productVariation") { productVariationTable in
productVariationTable.primaryKey("id", .integer).notNull()
productVariationTable.column("id", .integer).notNull()
productVariationTable.primaryKey(["siteID", "id"]) // SiteID column created by belongsTo relationship
productVariationTable.column("productID", .integer).notNull()
productVariationTable.belongsTo("site", onDelete: .cascade).notNull()
productVariationTable.belongsTo("product", onDelete: .cascade).notNull()
productVariationTable.foreignKey(["siteID", "productID"],
references: "product",
columns: ["siteID", "id"],
onDelete: .cascade)

productVariationTable.column("sku", .text)
productVariationTable.column("globalUniqueID", .text)
Expand All @@ -104,7 +121,12 @@ struct V001InitialSchema {
try db.create(table: "productVariationAttribute") { productVariationAttributeTable in
// This table holds local variation attributes only. Global attributes belong to a site.
productVariationAttributeTable.autoIncrementedPrimaryKey("id").notNull()
productVariationAttributeTable.belongsTo("productVariation", onDelete: .cascade).notNull()
productVariationAttributeTable.column("siteID", .integer).notNull()
productVariationAttributeTable.column("productVariationID", .integer).notNull()
productVariationAttributeTable.foreignKey(["siteID", "productVariationID"],
references: "productVariation",
columns: ["siteID", "id"],
onDelete: .cascade)

productVariationAttributeTable.column("name", .text).notNull()
productVariationAttributeTable.column("option", .text).notNull()
Expand All @@ -113,8 +135,14 @@ struct V001InitialSchema {

private static func createProductVariationImageTable(_ db: Database) throws {
try db.create(table: "productVariationImage") { productVariationImageTable in
productVariationImageTable.primaryKey("id", .integer).notNull()
productVariationImageTable.belongsTo("productVariation", onDelete: .cascade).notNull()
productVariationImageTable.column("siteID", .integer).notNull()
productVariationImageTable.column("id", .integer).notNull()
productVariationImageTable.primaryKey(["siteID", "id"])
productVariationImageTable.column("productVariationID", .integer).notNull()
productVariationImageTable.foreignKey(["siteID", "productVariationID"],
references: "productVariation",
columns: ["siteID", "id"],
onDelete: .cascade)

productVariationImageTable.column("dateCreated", .datetime).notNull()
productVariationImageTable.column("dateModified", .datetime)
Expand Down
40 changes: 24 additions & 16 deletions Modules/Sources/Storage/GRDB/Model/PersistedProduct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,33 @@ public struct PersistedProduct: Codable {
extension PersistedProduct: FetchableRecord, PersistableRecord {
public static var databaseTableName: String { "product" }

public static var primaryKey: [String] { [CodingKeys.siteID.stringValue, CodingKeys.id.stringValue] }

public enum Columns {
static let id = Column(CodingKeys.id)
static let siteID = Column(CodingKeys.siteID)
static let name = Column(CodingKeys.name)
static let productTypeKey = Column(CodingKeys.productTypeKey)
static let fullDescription = Column(CodingKeys.fullDescription)
static let shortDescription = Column(CodingKeys.shortDescription)
static let sku = Column(CodingKeys.sku)
static let globalUniqueID = Column(CodingKeys.globalUniqueID)
static let price = Column(CodingKeys.price)
static let downloadable = Column(CodingKeys.downloadable)
static let parentID = Column(CodingKeys.parentID)
static let manageStock = Column(CodingKeys.manageStock)
static let stockQuantity = Column(CodingKeys.stockQuantity)
static let stockStatusKey = Column(CodingKeys.stockStatusKey)
public static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let name = Column(CodingKeys.name)
public static let productTypeKey = Column(CodingKeys.productTypeKey)
public static let fullDescription = Column(CodingKeys.fullDescription)
public static let shortDescription = Column(CodingKeys.shortDescription)
public static let sku = Column(CodingKeys.sku)
public static let globalUniqueID = Column(CodingKeys.globalUniqueID)
public static let price = Column(CodingKeys.price)
public static let downloadable = Column(CodingKeys.downloadable)
public static let parentID = Column(CodingKeys.parentID)
public static let manageStock = Column(CodingKeys.manageStock)
public static let stockQuantity = Column(CodingKeys.stockQuantity)
public static let stockStatusKey = Column(CodingKeys.stockStatusKey)
}

public static let images = hasMany(PersistedProductImage.self)
public static let attributes = hasMany(PersistedProductAttribute.self)
public static let images = hasMany(PersistedProductImage.self,
using: ForeignKey([PersistedProductImage.CodingKeys.siteID.stringValue,
PersistedProductImage.CodingKeys.productID.stringValue],
to: primaryKey))
public static let attributes = hasMany(PersistedProductAttribute.self,
using: ForeignKey([PersistedProductAttribute.CodingKeys.siteID.stringValue,
PersistedProductAttribute.CodingKeys.productID.stringValue],
to: primaryKey))
}

// periphery:ignore - TODO: remove ignore when populating database
Expand Down
23 changes: 16 additions & 7 deletions Modules/Sources/Storage/GRDB/Model/PersistedProductAttribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import GRDB
// periphery:ignore - TODO: remove ignore when populating database
public struct PersistedProductAttribute: Codable {
public private(set) var id: Int64?
public let siteID: Int64
public let productID: Int64
public let name: String
public let position: Int64
Expand All @@ -12,13 +13,15 @@ public struct PersistedProductAttribute: Codable {
public let options: [String]

public init(id: Int64? = nil,
siteID: Int64,
productID: Int64,
name: String,
position: Int64,
visible: Bool,
variation: Bool,
options: [String]) {
self.id = id
self.siteID = siteID
self.productID = productID
self.name = name
self.position = position
Expand All @@ -32,26 +35,32 @@ public struct PersistedProductAttribute: Codable {
extension PersistedProductAttribute: FetchableRecord, MutablePersistableRecord {
public static var databaseTableName: String { "productAttribute" }

public static var primaryKey: [String] { [CodingKeys.id.stringValue] }

public enum Columns {
static let id = Column(CodingKeys.id)
public static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let productID = Column(CodingKeys.productID)
static let name = Column(CodingKeys.name)
static let position = Column(CodingKeys.position)
static let visible = Column(CodingKeys.visible)
static let variation = Column(CodingKeys.variation)
static let options = Column(CodingKeys.options)
public static let name = Column(CodingKeys.name)
public static let position = Column(CodingKeys.position)
public static let visible = Column(CodingKeys.visible)
public static let variation = Column(CodingKeys.variation)
public static let options = Column(CodingKeys.options)
}

public mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}

public static let product = belongsTo(PersistedProduct.self)
}


// periphery:ignore - TODO: remove ignore when populating database
private extension PersistedProductAttribute {
extension PersistedProductAttribute {
enum CodingKeys: String, CodingKey {
case id
case siteID
case productID
case name
case position
Expand Down
23 changes: 15 additions & 8 deletions Modules/Sources/Storage/GRDB/Model/PersistedProductImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import GRDB

// periphery:ignore - TODO: remove ignore when populating database
public struct PersistedProductImage: Codable {
public let siteID: Int64
public let id: Int64
public let productID: Int64
public let dateCreated: Date
Expand All @@ -11,13 +12,15 @@ public struct PersistedProductImage: Codable {
public let name: String?
public let alt: String?

public init(id: Int64,
public init(siteID: Int64,
id: Int64,
productID: Int64,
dateCreated: Date,
dateModified: Date?,
src: String,
name: String?,
alt: String?) {
self.siteID = siteID
self.id = id
self.productID = productID
self.dateCreated = dateCreated
Expand All @@ -32,21 +35,25 @@ 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 enum Columns {
static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let id = Column(CodingKeys.id)
public static let productID = Column(CodingKeys.productID)
static let dateCreated = Column(CodingKeys.dateCreated)
static let dateModified = Column(CodingKeys.dateModified)
static let src = Column(CodingKeys.src)
static let name = Column(CodingKeys.name)
static let alt = Column(CodingKeys.alt)
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
private extension PersistedProductImage {
extension PersistedProductImage {
enum CodingKeys: String, CodingKey {
case siteID
case id
case productID
case dateCreated
Expand Down
36 changes: 23 additions & 13 deletions Modules/Sources/Storage/GRDB/Model/PersistedProductVariation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,32 @@ public struct PersistedProductVariation: Codable {
extension PersistedProductVariation: FetchableRecord, PersistableRecord {
public static var databaseTableName: String { "productVariation" }

public static var primaryKey: [String] { [CodingKeys.siteID.stringValue, CodingKeys.id.stringValue] }

public enum Columns {
static let id = Column(CodingKeys.id)
static let siteID = Column(CodingKeys.siteID)
static let productID = Column(CodingKeys.productID)
static let sku = Column(CodingKeys.sku)
static let globalUniqueID = Column(CodingKeys.globalUniqueID)
static let price = Column(CodingKeys.price)
static let downloadable = Column(CodingKeys.downloadable)
static let fullDescription = Column(CodingKeys.fullDescription)
static let manageStock = Column(CodingKeys.manageStock)
static let stockQuantity = Column(CodingKeys.stockQuantity)
static let stockStatusKey = Column(CodingKeys.stockStatusKey)
public static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let productID = Column(CodingKeys.productID)
public static let sku = Column(CodingKeys.sku)
public static let globalUniqueID = Column(CodingKeys.globalUniqueID)
public static let price = Column(CodingKeys.price)
public static let downloadable = Column(CodingKeys.downloadable)
public static let fullDescription = Column(CodingKeys.fullDescription)
public static let manageStock = Column(CodingKeys.manageStock)
public static let stockQuantity = Column(CodingKeys.stockQuantity)
public static let stockStatusKey = Column(CodingKeys.stockStatusKey)
}

public static let attributes = hasMany(PersistedProductVariationAttribute.self).forKey("attributes")
public static let image = hasOne(PersistedProductVariationImage.self).forKey("image")
public static let attributes = hasMany(PersistedProductVariationAttribute.self,
key: "attributes",
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))
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import GRDB
// periphery:ignore - TODO: remove ignore when populating database
public struct PersistedProductVariationAttribute: Codable {
public private(set) var id: Int64?
public let siteID: Int64
public let productVariationID: Int64
public let name: String
public let option: String

public init(id: Int64? = nil,
siteID: Int64,
productVariationID: Int64,
name: String,
option: String) {
self.id = id
self.siteID = siteID
self.productVariationID = productVariationID
self.name = name
self.option = option
Expand All @@ -24,23 +27,29 @@ public struct PersistedProductVariationAttribute: Codable {
extension PersistedProductVariationAttribute: FetchableRecord, MutablePersistableRecord {
public static var databaseTableName: String { "productVariationAttribute" }

public static var primaryKey: [String] { [CodingKeys.id.stringValue] }

public enum Columns {
static let id = Column(CodingKeys.id)
public static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let productVariationID = Column(CodingKeys.productVariationID)
static let name = Column(CodingKeys.name)
static let option = Column(CodingKeys.option)
public static let name = Column(CodingKeys.name)
public static let option = Column(CodingKeys.option)
}

public mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}

public static let variation = belongsTo(PersistedProductVariation.self)
}


// periphery:ignore - TODO: remove ignore when populating database
private extension PersistedProductVariationAttribute {
extension PersistedProductVariationAttribute {
Comment on lines -41 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: any reasons for making this extension internal while the same one in other classes stay private?

Copy link
Contributor Author

@joshheald joshheald Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to refer to the column names by for the variation's attributes relationship:

public static let attributes = hasMany(PersistedProductVariationAttribute.self,
                                           using: ForeignKey([PersistedProductVariationAttribute.CodingKeys.siteID.stringValue,
                                                              PersistedProductVariationAttribute.CodingKeys.productVariationID.stringValue],
                                                             to: primaryKey))

However, that relationship isn't actually working (it's one of the causes of the failing tests) so when I've fixed it I'll check whether this can go back to private or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I actually needed to make the others internal too!

enum CodingKeys: String, CodingKey {
case id
case siteID
case productVariationID
case name
case option
Expand Down
Loading