diff --git a/.github/workflows/closed_issue_message.yml b/.github/workflows/closed_issue_message.yml index d72b7daf8f..a07fc9b14e 100644 --- a/.github/workflows/closed_issue_message.yml +++ b/.github/workflows/closed_issue_message.yml @@ -3,6 +3,10 @@ name: Closed Issue Message on: issues: types: [closed] + +permissions: + issues: write + jobs: auto_comment: runs-on: ubuntu-latest diff --git a/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift b/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift index bfe1717840..8540f066a9 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift @@ -27,7 +27,7 @@ public struct ModelDateFormatting { public static let encodingStrategy: JSONEncoder.DateEncodingStrategy = { let strategy = JSONEncoder.DateEncodingStrategy.custom { date, encoder in var container = encoder.singleValueContainer() - try container.encode(Temporal.DateTime(date).iso8601String) + try container.encode(Temporal.DateTime(date, timeZone: .utc).iso8601String) } return strategy }() diff --git a/Amplify/Categories/DataStore/Model/Internal/Persistable.swift b/Amplify/Categories/DataStore/Model/Internal/Persistable.swift index b309c94d51..b7a53acf5a 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Persistable.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Persistable.swift @@ -20,7 +20,7 @@ import Foundation /// - `Temporal.Time` /// - Warning: Although this has `public` access, it is intended for internal use and should not be used directly /// by host applications. The behavior of this may change without warning. -public protocol Persistable {} +public protocol Persistable: Encodable {} extension Bool: Persistable {} extension Double: Persistable {} diff --git a/Amplify/Categories/DataStore/Model/Temporal/Date.swift b/Amplify/Categories/DataStore/Model/Temporal/Date.swift index ac8cfcdb1d..9b27c313e0 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Date.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Date.swift @@ -18,16 +18,20 @@ extension Temporal { /// /// - Note: `.medium`, `.long`, and `.full` are the same date format. public struct Date: TemporalSpec { + // Inherits documentation from `TemporalSpec` public let foundationDate: Foundation.Date + // Inherits documentation from `TemporalSpec` + public let timeZone: TimeZone? = .utc + // Inherits documentation from `TemporalSpec` public static func now() -> Self { - Temporal.Date(Foundation.Date()) + Temporal.Date(Foundation.Date(), timeZone: .utc) } // Inherits documentation from `TemporalSpec` - public init(_ date: Foundation.Date) { + public init(_ date: Foundation.Date, timeZone: TimeZone?) { self.foundationDate = Temporal .iso8601Calendar .startOfDay(for: date) diff --git a/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift b/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift index 170e7598fd..95c65e5f6e 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift @@ -15,27 +15,33 @@ extension Temporal { /// * `.long` => `yyyy-MM-dd'T'HH:mm:ssZZZZZ` /// * `.full` => `yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ` public struct DateTime: TemporalSpec { + // Inherits documentation from `TemporalSpec` public let foundationDate: Foundation.Date + // Inherits documentation from `TemporalSpec` + public let timeZone: TimeZone? + // Inherits documentation from `TemporalSpec` public static func now() -> Self { - Temporal.DateTime(Foundation.Date()) + Temporal.DateTime(Foundation.Date(), timeZone: .utc) } /// `Temporal.Time` of this `Temporal.DateTime`. public var time: Time { - Time(foundationDate) + Time(foundationDate, timeZone: timeZone) } // Inherits documentation from `TemporalSpec` - public init(_ date: Foundation.Date) { + public init(_ date: Foundation.Date, timeZone: TimeZone? = .utc) { let calendar = Temporal.iso8601Calendar let components = calendar.dateComponents( DateTime.iso8601DateComponents, from: date ) + self.timeZone = timeZone + foundationDate = calendar .date(from: components) ?? date } @@ -57,3 +63,5 @@ extension Temporal { // Allow date unit and time unit operations on `Temporal.DateTime` extension Temporal.DateTime: DateUnitOperable, TimeUnitOperable {} + +extension Temporal.DateTime: Sendable { } diff --git a/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift b/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift index ef43617c29..5aaa135d8d 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift @@ -12,7 +12,7 @@ import Foundation @usableFromInline internal struct SpecBasedDateConverting { @usableFromInline - internal typealias DateConverter = (_ string: String, _ format: TemporalFormat?) throws -> Date + internal typealias DateConverter = (_ string: String, _ format: TemporalFormat?) throws -> (Date, TimeZone) @usableFromInline internal let convert: DateConverter @@ -28,19 +28,21 @@ internal struct SpecBasedDateConverting { internal static func `default`( iso8601String: String, format: TemporalFormat? = nil - ) throws -> Date { + ) throws -> (Date, TimeZone) { let date: Foundation.Date + let tz: TimeZone = TimeZone(iso8601DateString: iso8601String) ?? .utc if let format = format { date = try Temporal.date( from: iso8601String, with: [format(for: Spec.self)] ) + } else { date = try Temporal.date( from: iso8601String, with: TemporalFormat.sortedFormats(for: Spec.self) ) } - return date + return (date, tz) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift b/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift index 25d4674e53..bc9e9e47e0 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift @@ -15,11 +15,13 @@ import Foundation extension TemporalSpec where Self: Comparable { public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.iso8601String == rhs.iso8601String + return lhs.iso8601FormattedString(format: .full, timeZone: .utc) + == rhs.iso8601FormattedString(format: .full, timeZone: .utc) } public static func < (lhs: Self, rhs: Self) -> Bool { - return lhs.iso8601String < rhs.iso8601String + return lhs.iso8601FormattedString(format: .full, timeZone: .utc) + < rhs.iso8601FormattedString(format: .full, timeZone: .utc) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift b/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift index a1be6cdb2e..e92f4f9435 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift @@ -27,6 +27,10 @@ public protocol TemporalSpec { /// by a Foundation `Date` instance. var foundationDate: Foundation.Date { get } + /// The timezone field is an optional field used to specify the timezone associated + /// with a particular date. + var timeZone: TimeZone? { get } + /// The ISO-8601 formatted string in the UTC `TimeZone`. /// - SeeAlso: `iso8601FormattedString(TemporalFormat, TimeZone) -> String` var iso8601String: String { get } @@ -57,7 +61,7 @@ public protocol TemporalSpec { /// Constructs a `TemporalSpec` from a `Date` object. /// - Parameter date: The `Date` instance that will be used as the reference of the /// `TemporalSpec` instance. - init(_ date: Foundation.Date) + init(_ date: Foundation.Date, timeZone: TimeZone?) /// A string representation of the underlying date formatted using ISO8601 rules. /// @@ -90,25 +94,25 @@ extension TemporalSpec { /// The ISO8601 representation of the scalar using `.full` as the format and `.utc` as `TimeZone`. /// - SeeAlso: `iso8601FormattedString(format:timeZone:)` public var iso8601String: String { - iso8601FormattedString(format: .full) + iso8601FormattedString(format: .full, timeZone: timeZone ?? .utc) } @inlinable public init(iso8601String: String, format: TemporalFormat) throws { - let date = try SpecBasedDateConverting() + let (date, tz) = try SpecBasedDateConverting() .convert(iso8601String, format) - self.init(date) + self.init(date, timeZone: tz) } @inlinable public init( iso8601String: String ) throws { - let date = try SpecBasedDateConverting() + let (date, tz) = try SpecBasedDateConverting() .convert(iso8601String, nil) - self.init(date) + self.init(date, timeZone: tz) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift b/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift index fe7ae56e4f..a413ab566d 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift @@ -33,6 +33,6 @@ extension TemporalSpec { """ ) } - return Self.init(date) + return Self.init(date, timeZone: timeZone) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/Time.swift b/Amplify/Categories/DataStore/Model/Temporal/Time.swift index 9d621ddb6e..d4185e874d 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Time.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Time.swift @@ -18,13 +18,16 @@ extension Temporal { // Inherits documentation from `TemporalSpec` public let foundationDate: Foundation.Date + // Inherits documentation from `TemporalSpec` + public let timeZone: TimeZone? = .utc + // Inherits documentation from `TemporalSpec` public static func now() -> Self { - Temporal.Time(Foundation.Date()) + Temporal.Time(Foundation.Date(), timeZone: .utc) } // Inherits documentation from `TemporalSpec` - public init(_ date: Foundation.Date) { + public init(_ date: Foundation.Date, timeZone: TimeZone?) { // Sets the date to a fixed instant so time-only operations are safe let calendar = Temporal.iso8601Calendar var components = calendar.dateComponents( @@ -45,7 +48,6 @@ extension Temporal { components.year = 2_000 components.month = 1 components.day = 1 - self.foundationDate = calendar .date(from: components) ?? date } diff --git a/Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift b/Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift new file mode 100644 index 0000000000..9907557f32 --- /dev/null +++ b/Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift @@ -0,0 +1,150 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Foundation + +extension TimeZone { + + @usableFromInline + internal init?(iso8601DateString: String) { + switch ISO8601TimeZonePart.from(iso8601DateString: iso8601DateString) { + case .some(.utc): + self.init(abbreviation: "UTC") + case let .some(.hh(hours: hours)): + self.init(secondsFromGMT: hours * 60 * 60) + case let .some(.hhmm(hours: hours, minutes: minutes)), + let .some(.hh_mm(hours: hours, minuts: minutes)): + self.init(secondsFromGMT: hours * 60 * 60 + + (hours > 0 ? 1 : -1) * minutes * 60) + case let .some(.hh_mm_ss(hours: hours, minutes: minutes, seconds: seconds)): + self.init(secondsFromGMT: hours * 60 * 60 + + (hours > 0 ? 1 : -1) * minutes * 60 + + (hours > 0 ? 1 : -1) * seconds) + case .none: + return nil + } + } +} + + +/// ISO8601 Time Zone formats +/// - Note: +/// `±hh:mm:ss` is not a standard of ISO8601 date formate. It's supported by `AWSDateTime` exclusively. +/// +/// references: +/// https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators +/// https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html#graph-ql-aws-appsync-scalars +fileprivate enum ISO8601TimeZoneFormat { + case utc, hh, hhmm, hh_mm, hh_mm_ss + + var format: String { + switch self { + case .utc: + return "Z" + case .hh: + return "±hh" + case .hhmm: + return "±hhmm" + case .hh_mm: + return "±hh:mm" + case .hh_mm_ss: + return "±hh:mm:ss" + } + } + + var regex: NSRegularExpression? { + switch self { + case .utc: + return try? NSRegularExpression(pattern: "^Z$") + case .hh: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}$") + case .hhmm: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}\\d{2}$") + case .hh_mm: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}:\\d{2}$") + case .hh_mm_ss: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}:\\d{2}:\\d{2}$") + } + } + + var parts: [NSRange] { + switch self { + case .utc: + return [] + case .hh: + return [NSRange(location: 0, length: 3)] + case .hhmm: + return [ + NSRange(location: 0, length: 3), + NSRange(location: 3, length: 2) + ] + case .hh_mm: + return [ + NSRange(location: 0, length: 3), + NSRange(location: 4, length: 2) + ] + case .hh_mm_ss: + return [ + NSRange(location: 0, length: 3), + NSRange(location: 4, length: 2), + NSRange(location: 7, length: 2) + ] + } + } +} + +fileprivate enum ISO8601TimeZonePart { + case utc + case hh(hours: Int) + case hhmm(hours: Int, minutes: Int) + case hh_mm(hours: Int, minuts: Int) + case hh_mm_ss(hours: Int, minutes: Int, seconds: Int) + + + static func from(iso8601DateString: String) -> ISO8601TimeZonePart? { + return tryExtract(from: iso8601DateString, with: .utc) + ?? tryExtract(from: iso8601DateString, with: .hh) + ?? tryExtract(from: iso8601DateString, with: .hhmm) + ?? tryExtract(from: iso8601DateString, with: .hh_mm) + ?? tryExtract(from: iso8601DateString, with: .hh_mm_ss) + ?? nil + } +} + +fileprivate func tryExtract( + from dateString: String, + with format: ISO8601TimeZoneFormat +) -> ISO8601TimeZonePart? { + guard dateString.count > format.format.count else { + return nil + } + + let tz = String(dateString.dropFirst(dateString.count - format.format.count)) + + guard format.regex.flatMap({ + $0.firstMatch(in: tz, range: NSRange(location: 0, length: tz.count)) + }) != nil else { + return nil + } + + let parts = format.parts.compactMap { range in + Range(range, in: tz).flatMap { Int(tz[$0]) } + } + + guard parts.count == format.parts.count else { + return nil + } + + switch format { + case .utc: return .utc + case .hh: return .hh(hours: parts[0]) + case .hhmm: return .hhmm(hours: parts[0], minutes: parts[1]) + case .hh_mm: return .hh_mm(hours: parts[0], minuts: parts[1]) + case .hh_mm_ss: return .hh_mm_ss(hours: parts[0], minutes: parts[1], seconds: parts[2]) + } +} diff --git a/Amplify/Categories/DataStore/Query/QueryOperator.swift b/Amplify/Categories/DataStore/Query/QueryOperator.swift index 12923da37d..18578eb552 100644 --- a/Amplify/Categories/DataStore/Query/QueryOperator.swift +++ b/Amplify/Categories/DataStore/Query/QueryOperator.swift @@ -7,7 +7,7 @@ import Foundation -public enum QueryOperator { +public enum QueryOperator: Encodable { case notEqual(_ value: Persistable?) case equals(_ value: Persistable?) case lessOrEqual(_ value: Persistable) @@ -18,7 +18,7 @@ public enum QueryOperator { case notContains(_ value: String) case between(start: Persistable, end: Persistable) case beginsWith(_ value: String) - + public func evaluate(target: Any) -> Bool { switch self { case .notEqual(let predicateValue): @@ -51,4 +51,60 @@ public enum QueryOperator { } return false } + + private enum CodingKeys: String, CodingKey { + case type + case value + case start + case end + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .notEqual(let value): + try container.encode("notEqual", forKey: .type) + if let value = value { + try container.encode(value, forKey: .value) + } + case .equals(let value): + try container.encode("equals", forKey: .type) + if let value = value { + try container.encode(value, forKey: .value) + } + case .lessOrEqual(let value): + try container.encode("lessOrEqual", forKey: .type) + try container.encode(value, forKey: .value) + + case .lessThan(let value): + try container.encode("lessThan", forKey: .type) + try container.encode(value, forKey: .value) + + case .greaterOrEqual(let value): + try container.encode("greaterOrEqual", forKey: .type) + try container.encode(value, forKey: .value) + + case .greaterThan(let value): + try container.encode("greaterThan", forKey: .type) + try container.encode(value, forKey: .value) + + case .contains(let value): + try container.encode("contains", forKey: .type) + try container.encode(value, forKey: .value) + + case .notContains(let value): + try container.encode("notContains", forKey: .type) + try container.encode(value, forKey: .value) + + case .between(let start, let end): + try container.encode("between", forKey: .type) + try container.encode(start, forKey: .start) + try container.encode(end, forKey: .end) + + case .beginsWith(let value): + try container.encode("beginsWith", forKey: .type) + try container.encode(value, forKey: .value) + } + } } diff --git a/Amplify/Categories/DataStore/Query/QueryPredicate.swift b/Amplify/Categories/DataStore/Query/QueryPredicate.swift index b7aab99938..5d242502a1 100644 --- a/Amplify/Categories/DataStore/Query/QueryPredicate.swift +++ b/Amplify/Categories/DataStore/Query/QueryPredicate.swift @@ -8,9 +8,9 @@ import Foundation /// Protocol that indicates concrete types conforming to it can be used a predicate member. -public protocol QueryPredicate: Evaluable {} +public protocol QueryPredicate: Evaluable, Encodable {} -public enum QueryPredicateGroupType: String { +public enum QueryPredicateGroupType: String, Encodable { case and case or case not @@ -26,14 +26,14 @@ public func not(_ predicate: Predicate) -> QueryPredi /// The case `.all` is a predicate used as an argument to select all of a single modeltype. We /// chose `.all` instead of `nil` because we didn't want to use the implicit nature of `nil` to /// specify an action applies to an entire data set. -public enum QueryPredicateConstant: QueryPredicate { +public enum QueryPredicateConstant: QueryPredicate, Encodable { case all public func evaluate(target: Model) -> Bool { return true } } -public class QueryPredicateGroup: QueryPredicate { +public class QueryPredicateGroup: QueryPredicate, Encodable { public internal(set) var type: QueryPredicateGroupType public internal(set) var predicates: [QueryPredicate] @@ -92,9 +92,37 @@ public class QueryPredicateGroup: QueryPredicate { return !predicate.evaluate(target: target) } } + + // MARK: - Encodable conformance + + private enum CodingKeys: String, CodingKey { + case type + case predicates + } + + struct AnyQueryPredicate: Encodable { + private let _encode: (Encoder) throws -> Void + + init(_ base: QueryPredicate) { + _encode = base.encode + } + + func encode(to encoder: Encoder) throws { + try _encode(encoder) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type.rawValue, forKey: .type) + + let anyPredicates = predicates.map(AnyQueryPredicate.init) + try container.encode(anyPredicates, forKey: .predicates) + } + } -public class QueryPredicateOperation: QueryPredicate { +public class QueryPredicateOperation: QueryPredicate, Encodable { public let field: String public let `operator`: QueryOperator diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthChangePasswordTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthChangePasswordTask.swift index ad3987d7f4..6e16c26142 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthChangePasswordTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthChangePasswordTask.swift @@ -58,10 +58,4 @@ class AWSAuthChangePasswordTask: AuthChangePasswordTask, DefaultLogger { _ = try await userPoolService.changePassword(input: input) } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthClearFederationToIdentityPoolTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthClearFederationToIdentityPoolTask.swift index 9aef916b05..06dfe200c9 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthClearFederationToIdentityPoolTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthClearFederationToIdentityPoolTask.swift @@ -42,10 +42,4 @@ public class AWSAuthClearFederationToIdentityPoolTask: AuthClearFederationToIden return } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmResetPasswordTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmResetPasswordTask.swift index bd9c8e0bc4..4bf0c1bc12 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmResetPasswordTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmResetPasswordTask.swift @@ -89,10 +89,4 @@ class AWSAuthConfirmResetPasswordTask: AuthConfirmResetPasswordTask, DefaultLogg _ = try await userPoolService.confirmForgotPassword(input: input) } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift index 0ff9daa9f9..cba88b1466 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignInTask.swift @@ -148,10 +148,4 @@ class AWSAuthConfirmSignInTask: AuthConfirmSignInTask, DefaultLogger { friendlyDeviceName: pluginOptions?.friendlyDeviceName) } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignUpTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignUpTask.swift index 962ae5c95a..819e4e44c9 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignUpTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthConfirmSignUpTask.swift @@ -53,10 +53,4 @@ class AWSAuthConfirmSignUpTask: AuthConfirmSignUpTask, DefaultLogger { } } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthDeleteUserTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthDeleteUserTask.swift index 34e701e7b9..a684ed36d7 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthDeleteUserTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthDeleteUserTask.swift @@ -86,10 +86,4 @@ class AWSAuthDeleteUserTask: AuthDeleteUserTask, DefaultLogger { await taskHelper.didStateMachineConfigured() } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFederateToIdentityPoolTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFederateToIdentityPoolTask.swift index 45da8e257b..9f3151a63f 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFederateToIdentityPoolTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFederateToIdentityPoolTask.swift @@ -113,11 +113,4 @@ public class AWSAuthFederateToIdentityPoolTask: AuthFederateToIdentityPoolTask, } } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift index 5f3e4f5d89..2995593ff6 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthFetchSessionTask.swift @@ -33,11 +33,4 @@ class AWSAuthFetchSessionTask: AuthFetchSessionTask, DefaultLogger { forceRefresh: doesNeedForceRefresh) } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResendSignUpCodeTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResendSignUpCodeTask.swift index 44b1836d24..9df2af8c99 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResendSignUpCodeTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResendSignUpCodeTask.swift @@ -94,11 +94,4 @@ class AWSAuthResendSignUpCodeTask: AuthResendSignUpCodeTask, DefaultLogger { return deliveryDetails } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResetPasswordTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResetPasswordTask.swift index a8bb33034e..9bf6712a49 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResetPasswordTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthResetPasswordTask.swift @@ -94,12 +94,4 @@ class AWSAuthResetPasswordTask: AuthResetPasswordTask, DefaultLogger { let authResetPasswordResult = AuthResetPasswordResult(isPasswordReset: false, nextStep: nextStep) return authResetPasswordResult } - - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInTask.swift index fb82916fd9..3cb18018bf 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignInTask.swift @@ -170,12 +170,4 @@ class AWSAuthSignInTask: AuthSignInTask, DefaultLogger { return clientMetadata } - - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignOutTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignOutTask.swift index aead811e1f..52c6b94f68 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignOutTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignOutTask.swift @@ -77,12 +77,4 @@ class AWSAuthSignOutTask: AuthSignOutTask, DefaultLogger { let event = AuthenticationEvent(eventType: .signOutRequested(signOutData)) await authStateMachine.send(event) } - - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignUpTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignUpTask.swift index 485663d3d8..a52c369507 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignUpTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthSignUpTask.swift @@ -62,12 +62,4 @@ class AWSAuthSignUpTask: AuthSignUpTask, DefaultLogger { ) } } - - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthWebUISignInTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthWebUISignInTask.swift index be7690da99..7244ea854c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthWebUISignInTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/AWSAuthWebUISignInTask.swift @@ -47,12 +47,5 @@ class AWSAuthWebUISignInTask: AuthWebUISignInTask, DefaultLogger { } } - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } } #endif diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthFetchDevicesTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthFetchDevicesTask.swift index 6611c0d81e..5ba804b989 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthFetchDevicesTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthFetchDevicesTask.swift @@ -11,7 +11,7 @@ import AWSPluginsCore import ClientRuntime import AWSCognitoIdentityProvider -class AWSAuthFetchDevicesTask: AuthFetchDevicesTask { +class AWSAuthFetchDevicesTask: AuthFetchDevicesTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthFetchDevicesRequest diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthForgetDeviceTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthForgetDeviceTask.swift index a96a984a11..239b4f9eac 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthForgetDeviceTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthForgetDeviceTask.swift @@ -11,7 +11,7 @@ import AWSPluginsCore import ClientRuntime import AWSCognitoIdentityProvider -class AWSAuthForgetDeviceTask: AuthForgetDeviceTask { +class AWSAuthForgetDeviceTask: AuthForgetDeviceTask, DefaultLogger { private let request: AuthForgetDeviceRequest private let authStateMachine: AuthStateMachine diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthRememberDeviceTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthRememberDeviceTask.swift index d0345e8aab..71eb26f3f7 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthRememberDeviceTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/DeviceTasks/AWSAuthRememberDeviceTask.swift @@ -11,7 +11,7 @@ import AWSPluginsCore import ClientRuntime import AWSCognitoIdentityProvider -class AWSAuthRememberDeviceTask: AuthRememberDeviceTask { +class AWSAuthRememberDeviceTask: AuthRememberDeviceTask, DefaultLogger { private let request: AuthRememberDeviceRequest private let authStateMachine: AuthStateMachine diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/Protocols/AmplifyAuthTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/Protocols/AmplifyAuthTask.swift index 5d073c1630..ecd2a9a15a 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/Protocols/AmplifyAuthTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/Protocols/AmplifyAuthTask.swift @@ -25,14 +25,17 @@ protocol AmplifyAuthTask { } -extension AmplifyAuthTask { +extension AmplifyAuthTask where Self: DefaultLogger { var value: Success { get async throws { do { + log.info("Starting execution for \(eventName)") let valueReturned = try await execute() + log.info("Successfully completed execution for \(eventName) with result:\n\(prettify(valueReturned))") dispatch(result: .success(valueReturned)) return valueReturned } catch let error as Failure { + log.error("Failed execution for \(eventName) with error:\n\(prettify(error))") dispatch(result: .failure(error)) throw error } @@ -44,4 +47,10 @@ extension AmplifyAuthTask { let payload = HubPayload(eventName: eventName, context: nil, data: result) Amplify.Hub.dispatch(to: channel, payload: payload) } + + private func prettify(_ value: T) -> String { + var result = "" + dump(value, to: &result) + return result + } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthAttributeResendConfirmationCodeTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthAttributeResendConfirmationCodeTask.swift index 66796374ef..70d30b966b 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthAttributeResendConfirmationCodeTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthAttributeResendConfirmationCodeTask.swift @@ -10,7 +10,7 @@ import Amplify import AWSPluginsCore import AWSCognitoIdentityProvider -class AWSAuthAttributeResendConfirmationCodeTask: AuthAttributeResendConfirmationCodeTask { +class AWSAuthAttributeResendConfirmationCodeTask: AuthAttributeResendConfirmationCodeTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthAttributeResendConfirmationCodeRequest diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthConfirmUserAttributeTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthConfirmUserAttributeTask.swift index 8ed94106df..b9b6574201 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthConfirmUserAttributeTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthConfirmUserAttributeTask.swift @@ -11,7 +11,7 @@ import AWSPluginsCore import ClientRuntime import AWSCognitoIdentityProvider -class AWSAuthConfirmUserAttributeTask: AuthConfirmUserAttributeTask { +class AWSAuthConfirmUserAttributeTask: AuthConfirmUserAttributeTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthConfirmUserAttributeRequest diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthFetchUserAttributeTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthFetchUserAttributeTask.swift index cf15cc9395..c9e7a07727 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthFetchUserAttributeTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthFetchUserAttributeTask.swift @@ -11,7 +11,7 @@ import AWSPluginsCore import ClientRuntime import AWSCognitoIdentityProvider -class AWSAuthFetchUserAttributeTask: AuthFetchUserAttributeTask { +class AWSAuthFetchUserAttributeTask: AuthFetchUserAttributeTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthFetchUserAttributesRequest diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthSendUserAttributeVerificationCodeTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthSendUserAttributeVerificationCodeTask.swift index 5dc3962038..6acd247d98 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthSendUserAttributeVerificationCodeTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthSendUserAttributeVerificationCodeTask.swift @@ -10,7 +10,7 @@ import Amplify import AWSPluginsCore import AWSCognitoIdentityProvider -class AWSAuthSendUserAttributeVerificationCodeTask: AuthSendUserAttributeVerificationCodeTask { +class AWSAuthSendUserAttributeVerificationCodeTask: AuthSendUserAttributeVerificationCodeTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthSendUserAttributeVerificationCodeRequest diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributeTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributeTask.swift index 9b25a406bf..b6f318ddfd 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributeTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributeTask.swift @@ -10,7 +10,7 @@ import Amplify import AWSPluginsCore import AWSCognitoIdentityProvider -class AWSAuthUpdateUserAttributeTask: AuthUpdateUserAttributeTask { +class AWSAuthUpdateUserAttributeTask: AuthUpdateUserAttributeTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthUpdateUserAttributeRequest diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributesTask.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributesTask.swift index 0b0b8ed3dd..187791614a 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributesTask.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Task/UserTasks/AWSAuthUpdateUserAttributesTask.swift @@ -10,7 +10,7 @@ import Amplify import AWSPluginsCore import AWSCognitoIdentityProvider -class AWSAuthUpdateUserAttributesTask: AuthUpdateUserAttributesTask { +class AWSAuthUpdateUserAttributesTask: AuthUpdateUserAttributesTask, DefaultLogger { typealias CognitoUserPoolFactory = () throws -> CognitoUserPoolBehavior private let request: AuthUpdateUserAttributesRequest diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata+Schema.swift b/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata+Schema.swift index 1fd0898491..99ec90c255 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata+Schema.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata+Schema.swift @@ -15,6 +15,7 @@ extension ModelSyncMetadata { public enum CodingKeys: String, ModelKey { case id case lastSync + case syncPredicate } public static let keys = CodingKeys.self @@ -27,7 +28,8 @@ extension ModelSyncMetadata { definition.fields( .id(), - .field(keys.lastSync, is: .optional, ofType: .int) + .field(keys.lastSync, is: .optional, ofType: .int), + .field(keys.syncPredicate, is: .optional, ofType: .string) ) } } diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata.swift b/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata.swift index 074a86f288..bcecd4e60e 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Sync/ModelSync/ModelSyncMetadata.swift @@ -13,10 +13,15 @@ public struct ModelSyncMetadata: Model { /// The timestamp (in Unix seconds) at which the last sync was started, as reported by the service public var lastSync: Int64? + + /// The sync predicate for this model, extracted out from the sync expression. + public var syncPredicate: String? public init(id: String, - lastSync: Int64?) { + lastSync: Int64? = nil, + syncPredicate: String? = nil) { self.id = id self.lastSync = lastSync + self.syncPredicate = syncPredicate } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Migration/ModelSyncMetadataMigration.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Migration/ModelSyncMetadataMigration.swift new file mode 100644 index 0000000000..08dffbe272 --- /dev/null +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Migration/ModelSyncMetadataMigration.swift @@ -0,0 +1,108 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +import Foundation +import SQLite +import AWSPluginsCore + +class ModelSyncMetadataMigration: ModelMigration { + + weak var storageAdapter: SQLiteStorageEngineAdapter? + + func apply() throws { + try performModelMetadataSyncPredicateUpgrade() + } + + init(storageAdapter: SQLiteStorageEngineAdapter? = nil) { + self.storageAdapter = storageAdapter + } + + /// Add the new syncPredicate column for the ModelSyncMetadata system table. + /// + /// ModelSyncMetadata's syncPredicate column was added in Amplify version 2.22.0 to + /// support a bug fix related to persisting the sync predicate of the sync expression. + /// Apps before upgrading to this version of the plugin will have created the table already. + /// Upgraded apps will not re-create the table with the CreateTableStatement, neither will throw an error + /// (CreateTableStatement is run with 'create table if not exists' doing a no-op). This function + /// checks if the column exists on the table, and if it doesn't, alter the table to add the new column. + /// + /// For more details, see https://github.com/aws-amplify/amplify-swift/pull/2757. + /// - Returns: `true` if upgrade occured, `false` otherwise. + @discardableResult + func performModelMetadataSyncPredicateUpgrade() throws -> Bool { + do { + guard let field = ModelSyncMetadata.schema.field( + withName: ModelSyncMetadata.keys.syncPredicate.stringValue) else { + log.error("Could not find corresponding ModelField from ModelSyncMetadata for syncPredicate") + return false + } + let exists = try columnExists(modelSchema: ModelSyncMetadata.schema, + field: field) + guard !exists else { + log.debug("Detected ModelSyncMetadata table has syncPredicate column. No migration needed") + return false + } + + log.debug("Detected ModelSyncMetadata table exists without syncPredicate column.") + guard let storageAdapter = storageAdapter else { + log.debug("Missing SQLiteStorageEngineAdapter for model migration") + throw DataStoreError.nilStorageAdapter() + } + guard let connection = storageAdapter.connection else { + throw DataStoreError.nilSQLiteConnection() + } + let addColumnStatement = AlterTableAddColumnStatement( + modelSchema: ModelSyncMetadata.schema, + field: field).stringValue + try connection.execute(addColumnStatement) + log.debug("ModelSyncMetadata table altered to add syncPredicate column.") + return true + } catch { + throw DataStoreError.invalidOperation(causedBy: error) + } + } + + func columnExists(modelSchema: ModelSchema, field: ModelField) throws -> Bool { + guard let storageAdapter = storageAdapter else { + log.debug("Missing SQLiteStorageEngineAdapter for model migration") + throw DataStoreError.nilStorageAdapter() + } + guard let connection = storageAdapter.connection else { + throw DataStoreError.nilSQLiteConnection() + } + + let tableInfoStatement = TableInfoStatement(modelSchema: modelSchema) + do { + let existingColumns = try connection.prepare(tableInfoStatement.stringValue).run() + let columnToFind = field.name + var columnExists = false + for column in existingColumns { + // The second element is the column name + if column.count >= 2, + let columnName = column[1], + let columNameString = columnName as? String, + columnToFind == columNameString { + columnExists = true + break + } + } + return columnExists + } catch { + throw DataStoreError.invalidOperation(causedBy: error) + } + } +} + +extension ModelSyncMetadataMigration: DefaultLogger { + public static var log: Logger { + Amplify.Logging.logger(forCategory: CategoryType.dataStore.displayName, forNamespace: String(describing: self)) + } + public var log: Logger { + Self.log + } +} diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+AlterTable.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+AlterTable.swift index b547f9a34e..b60016d856 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+AlterTable.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+AlterTable.swift @@ -22,3 +22,13 @@ struct AlterTableStatement: SQLStatement { self.modelSchema = toModelSchema } } + +struct AlterTableAddColumnStatement: SQLStatement { + var modelSchema: ModelSchema + var field: ModelField + + var stringValue: String { + "ALTER TABLE \"\(modelSchema.name)\" ADD COLUMN \"\(field.sqlName)\" \"\(field.sqlType)\";" + } +} + diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+TableInfoStatement.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+TableInfoStatement.swift new file mode 100644 index 0000000000..b88d7ca7d0 --- /dev/null +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/SQLStatement+TableInfoStatement.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Amplify +import Foundation +import SQLite + +struct TableInfoStatement: SQLStatement { + let modelSchema: ModelSchema + + var stringValue: String { + return "PRAGMA table_info(\"\(modelSchema.name)\");" + } +} diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/StorageEngineAdapter+SQLite.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/StorageEngineAdapter+SQLite.swift index f6d5426b9f..1f65ffc7e7 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/StorageEngineAdapter+SQLite.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/StorageEngineAdapter+SQLite.swift @@ -117,8 +117,12 @@ final class SQLiteStorageEngineAdapter: StorageEngineAdapter { let delegate = SQLiteMutationSyncMetadataMigrationDelegate( storageAdapter: self, modelSchemas: modelSchemas) - let modelMigration = MutationSyncMetadataMigration(delegate: delegate) - let modelMigrations = ModelMigrations(modelMigrations: [modelMigration]) + let mutationSyncMetadataMigration = MutationSyncMetadataMigration(delegate: delegate) + + let modelSyncMetadataMigration = ModelSyncMetadataMigration(storageAdapter: self) + + let modelMigrations = ModelMigrations(modelMigrations: [mutationSyncMetadataMigration, + modelSyncMetadataMigration]) do { try modelMigrations.apply() } catch { diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift index 040dfb7f4a..711ad7ab57 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift @@ -74,6 +74,12 @@ extension StorageEngine { } } + /// Expresses whether the `StorageEngine` syncs from a remote source + /// based on whether the `AWSAPIPlugin` is present. + var syncsFromRemote: Bool { + tryGetAPIPlugin() != nil + } + private func tryGetAPIPlugin() -> APICategoryPlugin? { do { return try Amplify.API.getPlugin(for: validAPIPluginKey) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngineBehavior.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngineBehavior.swift index d17a12f38c..43c8878703 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngineBehavior.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngineBehavior.swift @@ -31,4 +31,7 @@ protocol StorageEngineBehavior: AnyObject, ModelStorageBehavior { func startSync() -> Result func stopSync(completion: @escaping DataStoreCallback) func clear(completion: @escaping DataStoreCallback) + + /// expresses whether the conforming type is syncing from a remote source. + var syncsFromRemote: Bool { get } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Subscribe/DataStoreObserveQueryOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Subscribe/DataStoreObserveQueryOperation.swift index dada158b38..775b576da5 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Subscribe/DataStoreObserveQueryOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Subscribe/DataStoreObserveQueryOperation.swift @@ -222,12 +222,26 @@ class ObserveQueryTaskRunner: InternalTaskRunner, InternalTaskAsyncThr func subscribeToItemChanges() { serialQueue.async { [weak self] in guard let self = self else { return } + self.batchItemsChangedSink = self.dataStorePublisher.publisher .filter { _ in !self.dispatchedModelSyncedEvent.get() } .filter(self.filterByModelName(mutationEvent:)) .filter(self.filterByPredicateMatch(mutationEvent:)) .handleEvents(receiveOutput: self.onItemChangeDuringSync(mutationEvent:) ) - .collect(.byTimeOrCount(self.serialQueue, self.itemsChangedPeriodicPublishTimeInSeconds, self.itemsChangedMaxSize)) + .collect( + .byTimeOrCount( + // on queue + self.serialQueue, + // collect over this timeframe + self.itemsChangedPeriodicPublishTimeInSeconds, + // If the `storageEngine` does sync from remote, the initial batch should + // collect snapshots based on time / snapshots received. + // If it doesn't, it should publish each snapshot without waiting. + self.storageEngine.syncsFromRemote + ? self.itemsChangedMaxSize + : 1 + ) + ) .sink(receiveCompletion: self.onReceiveCompletion(completed:), receiveValue: self.onItemsChangeDuringSync(mutationEvents:)) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift index a81f840cd0..1fc206425e 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift @@ -29,7 +29,32 @@ final class InitialSyncOperation: AsynchronousOperation { private var syncPageSize: UInt { return dataStoreConfiguration.syncPageSize } - + + private var syncPredicate: QueryPredicate? { + return dataStoreConfiguration.syncExpressions.first { + $0.modelSchema.name == self.modelSchema.name + }?.modelPredicate() + } + + private var syncPredicateString: String? { + guard let syncPredicate = syncPredicate, + let data = try? syncPredicateEncoder.encode(syncPredicate) else { + return nil + } + return String(data: data, encoding: .utf8) + } + + private lazy var _syncPredicateEncoder: JSONEncoder = { + var encoder = JSONEncoder() + encoder.dateEncodingStrategy = ModelDateFormatting.encodingStrategy + encoder.outputFormatting = [.sortedKeys] + return encoder + }() + + var syncPredicateEncoder: JSONEncoder { + _syncPredicateEncoder + } + private let initialSyncOperationTopic: PassthroughSubject var publisher: AnyPublisher { return initialSyncOperationTopic.eraseToAnyPublisher() @@ -59,36 +84,13 @@ final class InitialSyncOperation: AsynchronousOperation { } log.info("Beginning sync for \(modelSchema.name)") - let lastSyncTime = getLastSyncTime() - let syncType: SyncType = lastSyncTime == nil ? .fullSync : .deltaSync - initialSyncOperationTopic.send(.started(modelName: modelSchema.name, syncType: syncType)) + let lastSyncMetadata = getLastSyncMetadata() + let lastSyncTime = getLastSyncTime(lastSyncMetadata) Task { await query(lastSyncTime: lastSyncTime) } } - private func getLastSyncTime() -> Int64? { - guard !isCancelled else { - finish(result: .successfulVoid) - return nil - } - - let lastSyncMetadata = getLastSyncMetadata() - guard let lastSync = lastSyncMetadata?.lastSync else { - return nil - } - - let lastSyncDate = Date(timeIntervalSince1970: TimeInterval.milliseconds(Double(lastSync))) - let secondsSinceLastSync = (lastSyncDate.timeIntervalSinceNow * -1) - if secondsSinceLastSync < 0 { - log.info("lastSyncTime was in the future, assuming base query") - return nil - } - - let shouldDoDeltaQuery = secondsSinceLastSync < dataStoreConfiguration.syncInterval - return shouldDoDeltaQuery ? lastSync : nil - } - private func getLastSyncMetadata() -> ModelSyncMetadata? { guard !isCancelled else { finish(result: .successfulVoid) @@ -108,6 +110,51 @@ final class InitialSyncOperation: AsynchronousOperation { return nil } } + + /// Retrieve the lastSync time for the request before performing the query operation. + /// + /// - Parameter lastSyncMetadata: Retrieved persisted sync metadata for this model + /// - Returns: A `lastSync` time for the query request. + func getLastSyncTime(_ lastSyncMetadata: ModelSyncMetadata?) -> Int64? { + let syncType: SyncType + let lastSyncTime: Int64? + if syncPredicateChanged(self.syncPredicateString, lastSyncMetadata?.syncPredicate) { + log.info("SyncPredicate for \(modelSchema.name) changed, performing full sync.") + lastSyncTime = nil + syncType = .fullSync + } else { + lastSyncTime = getLastSyncTime(lastSync: lastSyncMetadata?.lastSync) + syncType = lastSyncTime == nil ? .fullSync : .deltaSync + } + initialSyncOperationTopic.send(.started(modelName: modelSchema.name, syncType: syncType)) + return lastSyncTime + } + + private func syncPredicateChanged(_ lastSyncPredicate: String?, _ currentSyncPredicate: String?) -> Bool { + switch (lastSyncPredicate, currentSyncPredicate) { + case (.some, .some): + return lastSyncPredicate != currentSyncPredicate + case (.some, .none), (.none, .some): + return true + case (.none, .none): + return false + } + } + + private func getLastSyncTime(lastSync: Int64?) -> Int64? { + guard let lastSync = lastSync else { + return nil + } + let lastSyncDate = Date(timeIntervalSince1970: TimeInterval.milliseconds(Double(lastSync))) + let secondsSinceLastSync = (lastSyncDate.timeIntervalSinceNow * -1) + if secondsSinceLastSync < 0 { + log.info("lastSyncTime was in the future, assuming base query") + return nil + } + + let shouldDoDeltaQuery = secondsSinceLastSync < dataStoreConfiguration.syncInterval + return shouldDoDeltaQuery ? lastSync : nil + } private func query(lastSyncTime: Int64?, nextToken: String? = nil) async { guard !isCancelled else { @@ -121,11 +168,6 @@ final class InitialSyncOperation: AsynchronousOperation { } let minSyncPageSize = Int(min(syncMaxRecords - recordsReceived, syncPageSize)) let limit = minSyncPageSize < 0 ? Int(syncPageSize) : minSyncPageSize - let syncExpression = dataStoreConfiguration.syncExpressions.first { - $0.modelSchema.name == modelSchema.name - } - let queryPredicate = syncExpression?.modelPredicate() - let completionListener: GraphQLOperation.ResultListener = { result in switch result { case .failure(let apiError): @@ -146,7 +188,7 @@ final class InitialSyncOperation: AsynchronousOperation { RetryableGraphQLOperation(requestFactory: { GraphQLRequest.syncQuery(modelSchema: self.modelSchema, - where: queryPredicate, + where: self.syncPredicate, limit: limit, nextToken: nextToken, lastSync: lastSyncTime, @@ -208,8 +250,10 @@ final class InitialSyncOperation: AsynchronousOperation { finish(result: .failure(DataStoreError.nilStorageAdapter())) return } - - let syncMetadata = ModelSyncMetadata(id: modelSchema.name, lastSync: lastSyncTime) + + let syncMetadata = ModelSyncMetadata(id: modelSchema.name, + lastSync: lastSyncTime, + syncPredicate: syncPredicateString) storageAdapter.save(syncMetadata, condition: nil, eagerLoad: true) { result in switch result { case .failure(let dataStoreError): diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/QueryPredicateTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/QueryPredicateTests.swift index ec0c082798..77bd4cea56 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/QueryPredicateTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/QueryPredicateTests.swift @@ -13,6 +13,17 @@ import XCTest class QueryPredicateTests: XCTestCase { + private lazy var _encoder: JSONEncoder = { + var encoder = JSONEncoder() + encoder.dateEncodingStrategy = ModelDateFormatting.encodingStrategy + encoder.outputFormatting = [.sortedKeys] + return encoder + }() + + var encoder: JSONEncoder { + _encoder + } + /// it should create a simple `QueryPredicateOperation` func testSingleQueryPredicateOperation() { let post = Post.keys @@ -36,6 +47,10 @@ class QueryPredicateTests: XCTestCase { ) XCTAssertEqual(predicate, expected) + + let predicateString = String(data: try! encoder.encode(predicate), encoding: .utf8)! + let expectedString = String(data: try! encoder.encode(expected), encoding: .utf8)! + XCTAssert(predicateString == expectedString) } /// it should create a valid `QueryPredicateOperation` with nested predicates @@ -68,6 +83,10 @@ class QueryPredicateTests: XCTestCase { ] ) XCTAssert(predicate == expected) + + let predicateString = String(data: try! encoder.encode(predicate), encoding: .utf8)! + let expectedString = String(data: try! encoder.encode(expected), encoding: .utf8)! + XCTAssert(predicateString == expectedString) } /// it should verify that predicates created using functions match their operators @@ -144,6 +163,9 @@ class QueryPredicateTests: XCTestCase { && !(post.updatedAt == nil) XCTAssertEqual(funcationPredicate, operatorPredicate) + + let funcationPredicateString = String(data: try! encoder.encode(funcationPredicate), encoding: .utf8)! + let operatorPredicateString = String(data: try! encoder.encode(operatorPredicate), encoding: .utf8)! + XCTAssert(funcationPredicateString == operatorPredicateString) } - } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Migration/ModelSyncMutationMigrationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Migration/ModelSyncMutationMigrationTests.swift new file mode 100644 index 0000000000..95e42c224a --- /dev/null +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Migration/ModelSyncMutationMigrationTests.swift @@ -0,0 +1,101 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SQLite + +@testable import Amplify +@testable import AWSPluginsCore +@testable import AmplifyTestCommon +@testable import AWSDataStorePlugin + +final class ModelSyncMetadataMigrationTests: XCTestCase { + + var connection: Connection! + var storageEngine: StorageEngine! + var storageAdapter: SQLiteStorageEngineAdapter! + var dataStorePlugin: AWSDataStorePlugin! + + override func setUp() async throws { + await Amplify.reset() + + do { + connection = try Connection(.inMemory) + storageAdapter = try SQLiteStorageEngineAdapter(connection: connection) + } catch { + XCTFail(String(describing: error)) + return + } + } + + /// Use the latest schema and create the table with it. + /// The column should be found and no upgrade should occur. + func testPerformModelMetadataSyncPredicateUpgrade_ColumnExists() throws { + try storageAdapter.setUp(modelSchemas: StorageEngine.systemModelSchemas) + guard let field = ModelSyncMetadata.schema.field( + withName: ModelSyncMetadata.keys.syncPredicate.stringValue) else { + XCTFail("Could not find corresponding ModelField from ModelSyncMetadata for syncPredicate") + return + } + let modelSyncMetadataMigration = ModelSyncMetadataMigration(storageAdapter: storageAdapter) + + let exists = try modelSyncMetadataMigration.columnExists(modelSchema: ModelSyncMetadata.schema, field: field) + XCTAssertTrue(exists) + + do { + let result = try modelSyncMetadataMigration.performModelMetadataSyncPredicateUpgrade() + XCTAssertFalse(result) + } catch { + XCTFail("Failed to perform upgrade \(error)") + } + } + + /// Create a local copy of the previous ModelSyncMetadata, without sync predicate + /// Create the table with the previous schema, and perform the upgrade. + /// The upgrade should have occurred successfully. + func testPerformModelMetadataSyncPredicateUpgrade_ColumnDoesNotExist() throws { + struct ModelSyncMetadata: Model { + public let id: String + public var lastSync: Int? + + public init(id: String, + lastSync: Int?) { + self.id = id + self.lastSync = lastSync + } + public enum CodingKeys: String, ModelKey { + case id + case lastSync + } + public static let keys = CodingKeys.self + public static let schema = defineSchema { definition in + definition.attributes(.isSystem) + definition.fields( + .id(), + .field(keys.lastSync, is: .optional, ofType: .int) + ) + } + } + + try storageAdapter.setUp(modelSchemas: [ModelSyncMetadata.schema]) + guard let field = AWSPluginsCore.ModelSyncMetadata.schema.field( + withName: AWSPluginsCore.ModelSyncMetadata.keys.syncPredicate.stringValue) else { + XCTFail("Could not find corresponding ModelField from ModelSyncMetadata for syncPredicate") + return + } + let modelSyncMetadataMigration = ModelSyncMetadataMigration(storageAdapter: storageAdapter) + let exists = try modelSyncMetadataMigration.columnExists(modelSchema: AWSPluginsCore.ModelSyncMetadata.schema, field: field) + XCTAssertFalse(exists) + + do { + let result = try modelSyncMetadataMigration.performModelMetadataSyncPredicateUpgrade() + XCTAssertTrue(result) + } catch { + XCTFail("Failed to perform upgrade \(error)") + } + } +} diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift index d5fd935321..d1a8a464aa 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift @@ -22,6 +22,225 @@ class InitialSyncOperationTests: XCTestCase { ModelRegistry.register(modelType: MockSynced.self) } + // MARK: - GetLastSyncTime + + func testFullSyncWhenLastSyncPredicateNilAndCurrentSyncPredicateNonNil() { + let lastSyncTime: Int64 = 123456 + let lastSyncPredicate: String? = nil + let currentSyncPredicate: DataStoreConfiguration + #if os(watchOS) + currentSyncPredicate = DataStoreConfiguration.custom( + syncExpressions: [ + .syncExpression( + MockSynced.schema, + where: { MockSynced.keys.id.eq("123") } + ) + ], + disableSubscriptions: { false } + ) + #else + currentSyncPredicate = DataStoreConfiguration.custom( + syncExpressions: [ + .syncExpression( + MockSynced.schema, + where: { MockSynced.keys.id.eq("123") } + ) + ] + ) + #endif + + let expectedSyncType = SyncType.fullSync + let expectedLastSync: Int64? = nil + + let syncStartedReceived = expectation(description: "Sync started received, sync operation started") + let operation = InitialSyncOperation( + modelSchema: MockSynced.schema, + api: nil, + reconciliationQueue: nil, + storageAdapter: nil, + dataStoreConfiguration: currentSyncPredicate, + authModeStrategy: AWSDefaultAuthModeStrategy()) + let sink = operation + .publisher + .sink(receiveCompletion: { _ in + }, receiveValue: { value in + switch value { + case .started(modelName: let modelName, syncType: let syncType): + XCTAssertEqual(modelName, "MockSynced") + XCTAssertEqual(syncType, expectedSyncType) + syncStartedReceived.fulfill() + default: + break + } + }) + + let lastSyncMetadataLastSyncNil = ModelSyncMetadata(id: MockSynced.schema.name, + lastSync: lastSyncTime, + syncPredicate: lastSyncPredicate) + XCTAssertEqual(operation.getLastSyncTime(lastSyncMetadataLastSyncNil), expectedLastSync) + + waitForExpectations(timeout: 1) + sink.cancel() + } + + func testFullSyncWhenLastSyncPredicateNonNilAndCurrentSyncPredicateNil() { + let lastSyncTime: Int64 = 123456 + let lastSyncPredicate: String? = "non nil" + let expectedSyncType = SyncType.fullSync + let expectedLastSync: Int64? = nil + + let syncStartedReceived = expectation(description: "Sync started received, sync operation started") + let operation = InitialSyncOperation( + modelSchema: MockSynced.schema, + api: nil, + reconciliationQueue: nil, + storageAdapter: nil, + dataStoreConfiguration: .testDefault(), + authModeStrategy: AWSDefaultAuthModeStrategy()) + let sink = operation + .publisher + .sink(receiveCompletion: { _ in + }, receiveValue: { value in + switch value { + case .started(modelName: let modelName, syncType: let syncType): + XCTAssertEqual(modelName, "MockSynced") + XCTAssertEqual(syncType, expectedSyncType) + syncStartedReceived.fulfill() + default: + break + } + }) + + let lastSyncMetadataLastSyncNil = ModelSyncMetadata(id: MockSynced.schema.name, + lastSync: lastSyncTime, + syncPredicate: lastSyncPredicate) + XCTAssertEqual(operation.getLastSyncTime(lastSyncMetadataLastSyncNil), expectedLastSync) + + waitForExpectations(timeout: 1) + sink.cancel() + } + + func testFullSyncWhenLastSyncPredicateDifferentFromCurrentSyncPredicate() { + let lastSyncTime: Int64 = 123456 + let lastSyncPredicate: String? = "non nil different from current predicate" + let currentSyncPredicate: DataStoreConfiguration + #if os(watchOS) + currentSyncPredicate = DataStoreConfiguration.custom( + syncExpressions: [ + .syncExpression( + MockSynced.schema, + where: { MockSynced.keys.id.eq("123") } + ) + ], + disableSubscriptions: { false } + ) + #else + currentSyncPredicate = DataStoreConfiguration.custom( + syncExpressions: [ + .syncExpression( + MockSynced.schema, + where: { MockSynced.keys.id.eq("123") } + ) + ] + ) + #endif + + let expectedSyncType = SyncType.fullSync + let expectedLastSync: Int64? = nil + + let syncStartedReceived = expectation(description: "Sync started received, sync operation started") + let operation = InitialSyncOperation( + modelSchema: MockSynced.schema, + api: nil, + reconciliationQueue: nil, + storageAdapter: nil, + dataStoreConfiguration: currentSyncPredicate, + authModeStrategy: AWSDefaultAuthModeStrategy()) + let sink = operation + .publisher + .sink(receiveCompletion: { _ in + }, receiveValue: { value in + switch value { + case .started(modelName: let modelName, syncType: let syncType): + XCTAssertEqual(modelName, "MockSynced") + XCTAssertEqual(syncType, expectedSyncType) + syncStartedReceived.fulfill() + default: + break + } + }) + + let lastSyncMetadataLastSyncNil = ModelSyncMetadata(id: MockSynced.schema.name, + lastSync: lastSyncTime, + syncPredicate: lastSyncPredicate) + XCTAssertEqual(operation.getLastSyncTime(lastSyncMetadataLastSyncNil), expectedLastSync) + + waitForExpectations(timeout: 1) + sink.cancel() + } + + func testDeltaSyncWhenLastSyncPredicateSameAsCurrentSyncPredicate() { + let startDateSeconds = (Int64(Date().timeIntervalSince1970) - 100) + let lastSyncTime: Int64 = startDateSeconds * 1_000 + let lastSyncPredicate: String? = "{\"field\":\"id\",\"operator\":{\"type\":\"equals\",\"value\":\"123\"}}" + let currentSyncPredicate: DataStoreConfiguration + #if os(watchOS) + currentSyncPredicate = DataStoreConfiguration.custom( + syncExpressions: [ + .syncExpression( + MockSynced.schema, + where: { MockSynced.keys.id.eq("123") } + ) + ], + disableSubscriptions: { false } + ) + #else + currentSyncPredicate = DataStoreConfiguration.custom( + syncExpressions: [ + .syncExpression( + MockSynced.schema, + where: { MockSynced.keys.id.eq("123") } + ) + ] + ) + #endif + + let expectedSyncType = SyncType.deltaSync + let expectedLastSync: Int64? = lastSyncTime + + let syncStartedReceived = expectation(description: "Sync started received, sync operation started") + let operation = InitialSyncOperation( + modelSchema: MockSynced.schema, + api: nil, + reconciliationQueue: nil, + storageAdapter: nil, + dataStoreConfiguration: currentSyncPredicate, + authModeStrategy: AWSDefaultAuthModeStrategy()) + let sink = operation + .publisher + .sink(receiveCompletion: { _ in + }, receiveValue: { value in + switch value { + case .started(modelName: let modelName, syncType: let syncType): + XCTAssertEqual(modelName, "MockSynced") + XCTAssertEqual(syncType, expectedSyncType) + syncStartedReceived.fulfill() + default: + break + } + }) + + let lastSyncMetadataLastSyncNil = ModelSyncMetadata(id: MockSynced.schema.name, + lastSync: lastSyncTime, + syncPredicate: lastSyncPredicate) + XCTAssertEqual(operation.getLastSyncTime(lastSyncMetadataLastSyncNil), expectedLastSync) + + waitForExpectations(timeout: 1) + sink.cancel() + } + + // MARK: - `main()` tests + /// - Given: An InitialSyncOperation /// - When: /// - I invoke main() diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/Support/MockSQLiteStorageEngineAdapter.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/Support/MockSQLiteStorageEngineAdapter.swift index 8dfed11a58..5b5fb2d46a 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/Support/MockSQLiteStorageEngineAdapter.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/Support/MockSQLiteStorageEngineAdapter.swift @@ -301,6 +301,8 @@ class MockStorageEngineBehavior: StorageEngineBehavior { } + var syncsFromRemote: Bool { true } + var mockSyncEnginePublisher: PassthroughSubject! var mockSyncEngineSubscription: AnyCancellable! { willSet { diff --git a/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift b/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift index 663e6b885d..914cd6ee24 100644 --- a/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift +++ b/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift @@ -42,6 +42,50 @@ class TemporalTests: XCTestCase { } } + /// - Given: a `DateTime` string in ISO8601 format + /// - When: + /// - the input has time zone info + /// - Then: + /// - DateTime should be parsed correctly with time zone info + /// - Date should be parsed with utc time zone + /// - Time should be parsed with utc time zone + func testConvertToIso8601String() { + do { + let datetime = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03-08:00") + XCTAssertEqual(datetime.iso8601String, "2023-11-30T11:04:03.000-08:00") + let datetime0 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03+08:00") + XCTAssertEqual(datetime0.iso8601String, "2023-11-30T11:04:03.000+08:00") + let datetime1 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03.322-0800") + XCTAssertEqual(datetime1.iso8601String, "2023-11-30T11:04:03.322-08:00") + let datetime2 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128-0830") + XCTAssertEqual(datetime2.iso8601String, "2023-11-30T14:09:27.128-08:30") + let datetime3 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128-0339") + XCTAssertEqual(datetime3.iso8601String, "2023-11-30T14:09:27.128-03:39") + let datetime4 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128-0000") + XCTAssertEqual(datetime4.iso8601String, "2023-11-30T14:09:27.128Z") + let datetime5 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03+08:00:21") + XCTAssertEqual(datetime5.iso8601String, "2023-11-30T11:03:42.000+08:00") + let datetime6 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03-08:00:21") + XCTAssertEqual(datetime6.iso8601String, "2023-11-30T11:04:24.000-08:00") + let datetime7 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128Z") + XCTAssertEqual(datetime7.iso8601String, "2023-11-30T14:09:27.128Z") + if #available(iOS 15.0, tvOS 15.0, *) { + let now = Date.now + let dateFormatter = DateFormatter() + dateFormatter.timeZone = .init(abbreviation: "HKT") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + let datetime7 = Temporal.DateTime(now, timeZone: .init(abbreviation: "HKT")) + XCTAssertEqual(datetime7.iso8601String, "\(dateFormatter.string(from: now))+08:00") + } + let date = try Temporal.Date(iso8601String: "2023-11-30-08:00") + XCTAssertEqual(date.iso8601String, "2023-11-30Z") + let time = try Temporal.Time(iso8601String: "11:00:00.000-08:00") + XCTAssertEqual(time.iso8601String, "19:00:00.000Z") + } catch { + XCTFail(error.localizedDescription) + } + } + /// - Given: a `DateTime` string /// - When: /// - the input format is `yyyy-MM-dd'T'HH:mm:ss'Z'` @@ -144,7 +188,7 @@ class TemporalTests: XCTestCase { func testFullDateTimeParsingOnPST() { do { let datetime = try Temporal.DateTime(iso8601String: "2020-01-20T08:00:00.180-08:00") - XCTAssertEqual(datetime.iso8601String, "2020-01-20T16:00:00.180Z") + XCTAssertEqual(datetime.iso8601String, "2020-01-20T08:00:00.180-08:00") XCTAssertEqual(datetime.iso8601FormattedString(format: .short, timeZone: pst), "2020-01-20T08:00") XCTAssertEqual(datetime.iso8601FormattedString(format: .short, timeZone: .utc), "2020-01-20T16:00") XCTAssertEqual(datetime.iso8601FormattedString(format: .medium, timeZone: pst), "2020-01-20T08:00:00") diff --git a/AmplifyTests/CoreTests/Model+CodableTests.swift b/AmplifyTests/CoreTests/Model+CodableTests.swift index 2333efa45c..c03c44edc7 100644 --- a/AmplifyTests/CoreTests/Model+CodableTests.swift +++ b/AmplifyTests/CoreTests/Model+CodableTests.swift @@ -24,7 +24,7 @@ class ModelCodableTests: XCTestCase { } func testToJSON() throws { - let createdAt = Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123)) + let createdAt = Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123), timeZone: .utc) let post = Post(id: "post-1", title: "title", content: "content", @@ -39,7 +39,7 @@ class ModelCodableTests: XCTestCase { XCTAssertEqual(post?.id, "post-1") XCTAssertEqual(post?.title, "title") XCTAssertEqual(post?.content, "content") - XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123))) + XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123), timeZone: .utc)) } func testDecodeWithoutFractionalSeconds() throws { @@ -47,6 +47,6 @@ class ModelCodableTests: XCTestCase { XCTAssertEqual(post?.id, "post-1") XCTAssertEqual(post?.title, "title") XCTAssertEqual(post?.content, "content") - XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000))) + XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000), timeZone: .utc)) } }