diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dcec5c3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SwiftDataTransferObjects is a Swift Package Manager library containing Data Transfer Objects (DTOs) used by the TelemetryDeck Server. The project started out as a way for various projects to share code, but now it is mainly used to develop Swift struct representations of Apache Druid data structures and query generation. Most other code is mostly unused. The main consumer of this library is a Swift Vapor server application that handles telemetry data processing and analytics. + +## Development Commands + +### Building and Testing +```bash +# Build the project +swift build + +# Run all tests +swift test + +# Run specific test target +swift test --filter DataTransferObjectsTests +swift test --filter QueryTests +swift test --filter QueryResultTests +swift test --filter QueryGenerationTests +swift test --filter SupervisorTests +swift test --filter DataSchemaTests + +# Build in release mode +swift build -c release +``` + +### Package Management +```bash +# Clean build artifacts +swift package clean + +# Update dependencies +swift package update + +# Generate Xcode project (optional) +swift package generate-xcodeproj +``` + +## Architecture Overview + +### Core Data Models +- **DTOv1/DTOv2**: Main data transfer objects with versioning + - `DTOv1`: Legacy models (InsightGroup, LexiconPayloadKey, OrganizationJoinRequest) + - `DTOv2`: Current models (Organization, User, App, Insight, Badge, etc.) +- **Models.swift**: Additional DTOs for API requests, authentication, and UI state + +### Query System +- **CustomQuery**: Main query builder for Apache Druid integration + - Supports multiple query types: timeseries, groupBy, topN, scan, timeBoundary, funnel, experiment + - Handles filters, aggregations, post-aggregations, and time intervals +- **Query Components**: + - `Aggregator`: Define aggregation functions (sum, count, etc.) + - `Filter`: Query filtering logic + - `DimensionSpec`: Dimension specifications for grouping + - `QueryGranularity`: Time granularity (day, week, month) + - `VirtualColumn`: Computed columns + +### Druid Integration +- **Druid/**: Complete Apache Druid configuration DTOs + - `configuration/`: Tuning configs, compaction configs + - `data/input/`: Input formats, sources, and dimension specs + - `indexing/`: Parallel indexing, batch processing + - `ingestion/`: Native batch ingestion specs + - `segment/`: Data schema and transformation specs + - `Supervisor/`: Kafka streaming supervision + +### Chart Configuration +- **ChartConfiguration**: Display settings for analytics charts +- **ChartDefinitionDTO**: Chart metadata and configuration +- **InsightDisplayMode**: Chart types (lineChart, barChart, pieChart, etc.) + +### Query Results +- **QueryResult**: Polymorphic result handling for different query types +- **TimeSeriesQueryResult**: Time-based query results +- **TopNQueryResult**: Top-N dimension results +- **GroupByQueryResult**: Grouped aggregation results +- **ScanQueryResult**: Raw data scanning results + +## Key Dependencies + +- **SwiftDateOperations**: Date manipulation utilities +- **Apple Swift Crypto**: Cryptographic hashing for query stability + +## Development Notes + +### DTO Versioning +The library uses a versioning strategy with `DTOv1` and `DTOv2` namespaces. `DTOv2.Insight` is deprecated in favor of V3InsightsController patterns. + +### Query Hashing +CustomQuery implements stable hashing using SHA256 for caching and query deduplication. The `stableHashValue` property provides consistent query identification. + +### Test Structure +Tests are organized by functionality: +- **DataTransferObjectsTests**: Basic DTO serialization/deserialization +- **QueryTests**: Query building and validation +- **QueryResultTests**: Result parsing and handling +- **QueryGenerationTests**: Advanced query generation (funnels, experiments) +- **SupervisorTests**: Druid supervisor configuration +- **DataSchemaTests**: Data ingestion schema validation + +### Encoding/Decoding +The library uses custom JSON encoding/decoding with: +- `JSONEncoder.telemetryEncoder`: Consistent date formatting +- Custom wrappers (`StringWrapper`, `DoubleWrapper`) for flexible JSON parsing +- `DoublePlusInfinity`: Handles infinity values in query results diff --git a/Sources/DataTransferObjects/ChartDefinitionDTOs/ChartDefinitionDTO.swift b/Sources/DataTransferObjects/ChartDefinitionDTOs/ChartDefinitionDTO.swift deleted file mode 100644 index da489e7..0000000 --- a/Sources/DataTransferObjects/ChartDefinitionDTOs/ChartDefinitionDTO.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ChartDefinitionDTO.swift -// -// -// Created by Daniel Jilg on 10.11.21. -// - -import Foundation - -/// Chart-Representable data that was calculated from an Insight -struct ChartDefinitionDTO: Codable { - /// The ID of the insight that was calculated - public let id: UUID - - public let metadata: MetadataSection - public let data: DataSection - public let axis: AxisSection - - public struct MetadataSection: Codable { - /// The insight that was calculated - public let insight: DTOv2.Insight - - /// When was this result calculated? - public let calculatedAt: Date - - /// How long did this result take to calculate? - public let calculationDuration: TimeInterval - } - - public struct DataSection: Codable { - public let x: String - public let xFormat: String? - public let columns: [Column] - - public struct Column: Codable, Equatable { - public init(label: String, data: [String?]) { - self.label = label - self.data = data - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let array = try container.decode([String?].self) - - label = (array.first ?? "No Label") ?? "No Label" - data = Array(array.dropFirst()) - } - - public let label: String - public let data: [String?] - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - var containingArray = [String?]() - containingArray.append(label) - containingArray.append(contentsOf: data) - try container.encode(containingArray) - } - } - } - - public struct AxisSection: Codable { - public let x: AxisDefinition - - public struct AxisDefinition: Codable { - public let type: String - } - } -} diff --git a/Sources/DataTransferObjects/DTOs/Aggregate.swift b/Sources/DataTransferObjects/DTOs/Aggregate.swift deleted file mode 100644 index 9cfa0c1..0000000 --- a/Sources/DataTransferObjects/DTOs/Aggregate.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Aggregate.swift -// -// -// Created by Daniel Jilg on 15.04.21. -// - -import Foundation - -public extension DTOv1 { - struct Aggregate: Codable { - public init(min: TimeInterval, avg: TimeInterval, max: TimeInterval, median: TimeInterval) { - self.min = min - self.avg = avg - self.max = max - self.median = median - } - - public let min: TimeInterval - public let avg: TimeInterval - public let max: TimeInterval - public let median: TimeInterval - } -} diff --git a/Sources/DataTransferObjects/DTOs/AppAdminEntry.swift b/Sources/DataTransferObjects/DTOs/AppAdminEntry.swift deleted file mode 100644 index f9e005b..0000000 --- a/Sources/DataTransferObjects/DTOs/AppAdminEntry.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -public extension DTOv1 { - struct AppAdminEntry: Codable, Identifiable, Equatable { - public init(id: UUID, appName: String?, organisationName: String?, organisationID: UUID?, signalCount: Int, userCount: Int) { - self.id = id - self.appName = appName - self.organisationName = organisationName - self.organisationID = organisationID - self.signalCount = signalCount - self.userCount = userCount - } - - public let id: UUID - public let appName: String? - public let organisationName: String? - public let organisationID: UUID? - public let signalCount: Int - public let userCount: Int - - func resolvedAppName() -> String { - guard let appName = appName else { return "–" } - - if appName == "w", organisationName == "XAN Software GmbH & Co. KG" { - return "DouWatch" - } - - if appName == "WristW", organisationName == "XAN Software GmbH & Co. KG" { - return "WristWeb" - } - - if appName == "ww", organisationName == "XAN Software GmbH & Co. KG" { - return "WristWeb" - } - - return appName - } - } -} diff --git a/Sources/DataTransferObjects/DTOs/DTOv2.swift b/Sources/DataTransferObjects/DTOs/DTOv2.swift deleted file mode 100644 index b95e5e8..0000000 --- a/Sources/DataTransferObjects/DTOs/DTOv2.swift +++ /dev/null @@ -1,716 +0,0 @@ -// -// DTOv2.swift -// InsightDTOs -// -// Created by Daniel Jilg on 17.08.21. -// - -import Foundation - -public enum DTOv2 { - public struct Organization: Codable, Hashable, Identifiable { - public var id: UUID - public var name: String - public var createdAt: Date? - public var updatedAt: Date? - public var isSuperOrg: Bool - public var stripeCustomerID: String? - public var stripePriceID: String? - public var stripeMaxSignals: Int64? - public var maxSignalsMultiplier: Double? - public var resolvedMaxSignals: Int64 - public var isInRestrictedMode: Bool - public var countryCode: String? - public var referralCode: String - public var referredBy: UUID? - public var appIDs: [App.ID] - public var badgeAwardIDs: [BadgeAward.ID] - public var settings: OrganizationSettings? - - public init( - id: UUID, - name: String, - createdAt: Date?, - updatedAt: Date?, - isSuperOrg: Bool, - stripeCustomerID: String?, - stripePriceID: String?, - stripeMaxSignals: Int64?, - maxSignalsMultiplier: Double?, - resolvedMaxSignals: Int64, - isInRestrictedMode: Bool, - countryCode: String?, - referralCode: String, - referredBy: UUID?, - appIDs: [App.ID], - badgeAwardIDs: [BadgeAward.ID], - settings: OrganizationSettings - ) { - self.id = id - self.name = name - self.createdAt = createdAt - self.updatedAt = updatedAt - self.isSuperOrg = isSuperOrg - self.stripeCustomerID = stripeCustomerID - self.stripePriceID = stripePriceID - self.stripeMaxSignals = stripeMaxSignals - self.maxSignalsMultiplier = maxSignalsMultiplier - self.resolvedMaxSignals = resolvedMaxSignals - self.isInRestrictedMode = isInRestrictedMode - self.countryCode = countryCode - self.referralCode = referralCode - self.referredBy = referredBy - self.appIDs = appIDs - self.badgeAwardIDs = badgeAwardIDs - self.settings = settings - } - } - - public struct OrganizationSettings: Codable, Hashable { - // TBA - - public init() {} - } - - struct User: Identifiable, Codable { - public let id: UUID - public let organization: DTOv1.Organization? - public var firstName: String - public var lastName: String - public var email: String - public let emailIsVerified: Bool - public var receiveMarketingEmails: Bool? - public let isFoundingUser: Bool - public var receiveReports: ReportSendingRate - public var settings: UserSettings? - - public init( - id: UUID, - organization: DTOv1.Organization?, - firstName: String, - lastName: String, - email: String, - emailIsVerified: Bool, - receiveMarketingEmails: Bool?, - isFoundingUser: Bool, - receiveReports: ReportSendingRate, - settings: UserSettings - ) { - self.id = id - self.organization = organization - self.firstName = firstName - self.lastName = lastName - self.email = email - self.emailIsVerified = emailIsVerified - self.receiveMarketingEmails = receiveMarketingEmails - self.isFoundingUser = isFoundingUser - self.receiveReports = receiveReports - self.settings = settings - } - } - - public struct UserSettings: Codable, Hashable { - public enum ChartColorSet: String, Codable, Hashable { - case telemetryDeckRainbow - case highContrast - } - - // if set, use a custom chart color set, e.g. for various forms of color blindness - public var chartColorSet: ChartColorSet - - public init(chartColorSet: ChartColorSet? = nil) { - self.chartColorSet = chartColorSet ?? .telemetryDeckRainbow - } - } - - public struct BadgeAward: Codable, Hashable, Identifiable { - public var id: UUID - public var badgeID: UUID - public var organizationID: UUID - public var awardedAt: Date - - public init( - id: UUID, - badgeID: UUID, - organizationID: UUID, - awardedAt: Date - - ) { - self.id = id - self.badgeID = badgeID - self.organizationID = organizationID - self.awardedAt = awardedAt - } - } - - public struct Badge: Codable, Hashable, Identifiable { - public var id: UUID - public var title: String - public var description: String - public var imageURL: URL? - public var signalCountMultiplier: Double? - - public init(id: UUID, title: String, description: String, imageURL: URL? = nil, signalCountMultiplier: Double? = nil) { - self.id = id - self.title = title - self.description = description - self.imageURL = imageURL - self.signalCountMultiplier = signalCountMultiplier - } - } - - public struct SignalBoost: Codable, Hashable, Identifiable { - public init(id: UUID, receivedAt: Date, organizationID: UUID, source: String, message: String? = nil, signalsReceived: Int64) { - self.id = id - self.receivedAt = receivedAt - self.organizationID = organizationID - self.source = source - self.message = message - self.signalsReceived = signalsReceived - } - - public var id: UUID - public var receivedAt: Date - public var organizationID: UUID - - /// Human-Readable "from" field - public var source: String - - /// An optional message from the booster to the boostee - public var message: String? - - /// How many signals were gifted? - public var signalsReceived: Int64 - } - - public struct App: Codable, Hashable, Identifiable { - public var id: UUID - public var name: String - public var organizationID: Organization.ID - public var insightGroupIDs: [Group.ID] - public var settings: AppSettings - - public init(id: UUID, name: String, organizationID: Organization.ID, insightGroupIDs: [Group.ID], settings: AppSettings) { - self.id = id - self.name = name - self.organizationID = organizationID - self.insightGroupIDs = insightGroupIDs - self.settings = settings - } - } - - public struct AppSettings: Codable, Hashable { - public init( - showExampleData: Bool? = nil, - colorScheme: String? = nil, - displayMode: DTOv2.AppSettings.DisplayMode? = nil, - revenueDisplayMode: DTOv2.AppSettings.RevenueDisplayMode? = nil, - navigationAnalyticsShown: Bool? = nil, - navigationAnalyticsSignalType: String? = nil, - navigationAnalyticsSeparator: String? = nil - ) { - self.showExampleData = showExampleData - self.colorScheme = colorScheme - self.displayMode = displayMode - self.revenueDisplayMode = revenueDisplayMode - self.navigationAnalyticsShown = navigationAnalyticsShown - self.navigationAnalyticsSignalType = navigationAnalyticsSignalType - self.navigationAnalyticsSeparator = navigationAnalyticsSeparator - } - - public enum DisplayMode: String, Codable, Hashable { - case app - case website - } - - public enum RevenueDisplayMode: String, Codable, Hashable { - case telemetryDeck - case revenueCat - } - - /// If true, the app should display demo content instead of - public var showExampleData: Bool? - - /// What colors should charts be in? - /// - /// This should be formatted as - /// - space-separated hex codes like "#ff0000 #12aa33 #baff1e" - /// - a color scheme name like "highcontrast" - /// - or nil for default color scheme - public var colorScheme: String? - - /// How should the overview page for this application be layouted? - public var displayMode: DisplayMode? - - /// How should revenue be displayed? - /// - telemetryDeck: Use the revenue data from TelemetryDeck SDK - /// - revenueCat: Use the revenue data from RevenueCat - /// - nil: Don't display revenue data - public var revenueDisplayMode: RevenueDisplayMode? - - /// If true, show navigation analytics. This is a beta feature and should only be enabled for testing - public var navigationAnalyticsShown: Bool? - - /// If set, use this string as the signal type for navigation analytics. Default is "TelemetryDeck.Navigation.pathChanged" for apps and "pageview" for websites - public var navigationAnalyticsSignalType: String? - - /// If set, use this string as a separator for navigation analytics. Default is "." for apps and "/" for websites - public var navigationAnalyticsSeparator: String? - } - - public struct Group: Codable, Hashable, Identifiable { - public init(id: UUID, title: String, order: Double? = nil, appID: DTOv2.App.ID, insightIDs: [DTOv2.Insight.ID]) { - self.id = id - self.title = title - self.order = order - self.appID = appID - self.insightIDs = insightIDs - } - - public var id: UUID - public var title: String - public var order: Double? - public var appID: App.ID - public var insightIDs: [Insight.ID] - } - - public struct AppWithInsights: Codable, Hashable, Identifiable { - public var id: UUID - public var name: String - public var organizationID: Organization.ID - public var insights: [DTOv2.Insight] - public var settings: AppSettings - - public init(id: UUID, name: String, organizationID: Organization.ID, insights: [DTOv2.Insight], settings: AppSettings) { - self.id = id - self.name = name - self.organizationID = organizationID - self.insights = insights - self.settings = settings - } - } - - /// Defines an insight as saved to the database, no calculation results - @available(*, deprecated, message: "Use V3InsightsController.Insight instead") - public struct Insight: Codable, Hashable, Identifiable { - public var id: UUID - public var groupID: UUID - - /// order in which insights appear in the apps (if not expanded) - public var order: Double? - public var title: String - - /// What kind of insight is this? - public var type: String - - /// If set, display the chart with this accent color, otherwise fall back to default color - public var accentColor: String? - - /// If set, use the custom query in this property instead of constructing a query out of the options below - public var customQuery: CustomQuery? - - /// Which signal types are we interested in? If nil, do not filter by signal type - public var signalType: String? - - /// If true, only include at the newest signal from each user - public var uniqueUser: Bool - - /// Only include signals that match all of these key-values in the payload - public var filters: [String: String] - - /// If set, break down the values in this key - public var breakdownKey: String? - - /// If set, group and count found signals by this time interval. Incompatible with breakdownKey - public var groupBy: QueryGranularity? - - /// How should this insight's data be displayed? - public var displayMode: InsightDisplayMode - - /// If true, the insight will be displayed bigger - public var isExpanded: Bool - - /// The amount of time (in seconds) this query took to calculate last time - public var lastRunTime: TimeInterval? - - /// The date this query was last run - public var lastRunAt: Date? - - public init( - id: UUID, - groupID: UUID, - order: Double?, - title: String, - type: String, - accentColor: String? = nil, - widgetable _: Bool? = false, - customQuery: CustomQuery? = nil, - signalType: String?, - uniqueUser: Bool, - filters: [String: String], - breakdownKey: String?, - groupBy: QueryGranularity?, - displayMode: InsightDisplayMode, - isExpanded: Bool, - lastRunTime: TimeInterval?, - lastRunAt: Date? - ) { - self.id = id - self.groupID = groupID - self.order = order - self.title = title - self.type = type - self.accentColor = accentColor - self.customQuery = customQuery - self.signalType = signalType - self.uniqueUser = uniqueUser - self.filters = filters - self.breakdownKey = breakdownKey - self.groupBy = groupBy - self.displayMode = displayMode - self.isExpanded = isExpanded - self.lastRunTime = lastRunTime - self.lastRunAt = lastRunAt - } - } - - /// Defines the result of an insight calculation - public struct InsightCalculationResult: Codable, Hashable, Identifiable { - /// The ID of the insight that was calculated - public let id: UUID - - /// The insight that was calculated - public let insight: DTOv2.Insight - - /// Current Live Calculated Data - public let data: [DTOv2.InsightCalculationResultRow] - - /// When was this DTO calculated? - public let calculatedAt: Date - - /// How long did this DTO take to calculate? - public let calculationDuration: TimeInterval - - public init(id: UUID, insight: DTOv2.Insight, data: [DTOv2.InsightCalculationResultRow], calculatedAt: Date, calculationDuration: TimeInterval) { - self.id = id - self.insight = insight - self.data = data - self.calculatedAt = calculatedAt - self.calculationDuration = calculationDuration - } - } - - /// Actual row of data inside an InsightCalculationResult - public struct InsightCalculationResultRow: Codable, Hashable { - public var xAxisValue: String - public var yAxisValue: Int64 - - public init(xAxisValue: String, yAxisValue: Int64) { - self.xAxisValue = xAxisValue - self.yAxisValue = yAxisValue - } - } - - public struct PriceStructure: Identifiable, Codable { - public init( - id: String, - order: Int, - title: String, - description: String, - includedSignals: Int64, - nakedPrice: String, - mostPopular: Bool, - currency: String, - billingPeriod: String, - features: [String] - ) { - self.id = id - self.order = order - self.title = title - self.description = description - self.includedSignals = includedSignals - self.mostPopular = mostPopular - self.currency = currency - self.billingPeriod = billingPeriod - self.nakedPrice = nakedPrice - self.features = features - - // currency is derived - switch currency { - case "EUR": - currencySymbol = "€" - case "USD", "CAD": - currencySymbol = "$" - default: - currencySymbol = currency - } - - // price is derived - price = "\(currencySymbol)\(self.nakedPrice)/\(self.billingPeriod)" - } - - public let id: String - public let order: Int - public let title: String - public let description: String - public let includedSignals: Int64 - - /// Price, including period and currency e.g. "$299/month" - public let price: String - - /// Price as a number, e.g. "299" - public let nakedPrice: String - - public let mostPopular: Bool - - /// "EUR" or "USD" or "CAD" - public let currency: String - - /// "month" or "year" - public let billingPeriod: String - - /// "$" or "€" - public let currencySymbol: String - - /// Each of these gets a checkmark in front of it - public let features: [String] - } - - /// A short message that goes out to users and is usually displayed in the app UI - public struct StatusMessage: Identifiable, Codable { - public init(id: String, validFrom: Date, validUntil: Date?, title: String, description: String?, severity: Int?, systemImageName: String?) { - self.id = id - self.validFrom = validFrom - self.validUntil = validUntil - self.title = title - self.description = description - self.severity = severity - self.systemImageName = systemImageName - } - - public let id: String - public let validFrom: Date - public let validUntil: Date? - public let title: String - public let description: String? - - /// 3 = bad, 2 = moderate, 1 = okay, 0 = info - public let severity: Int? - public let systemImageName: String? - } - - struct LexiconSignal: Codable, Hashable, Identifiable { - public init(type: String, signalCount: Int, userCount: Int, sessionCount: Int) { - self.type = type - self.signalCount = signalCount - self.userCount = userCount - self.sessionCount = sessionCount - } - - public var id: String { type } - public var type: String - public var signalCount: Int - public var userCount: Int - public var sessionCount: Int - } - - public struct LexiconPayloadKey: Codable, Hashable, Identifiable { - public init(name: String, count: Int) { - self.name = name - self.count = count - } - - /// Name of the payload key - public let name: String - - /// Occurrences of the payload key within this month and the previous one - public let count: Int - - public var id: String { name } - } -} - -public struct ChartTemplate: Codable { - public init(template: ChartTemplate.Template, breakdownKey: String?, funnelSignalTypes: [String]?) { - self.template = template - self.breakdownKey = breakdownKey - self.funnelSignalTypes = funnelSignalTypes - } - - public enum Template: String, Codable { - case custom - case signalCount - case userCount - case breakdown - case funnel - } - - public let template: Template - public let breakdownKey: String? - public let funnelSignalTypes: [String]? -} - -public enum InsightDisplayMode: String, Codable { - case number // Deprecated, use Raw instead - case raw - case barChart - case lineChart - case pieChart - case funnelChart - case experimentChart - case matrix - case sankey - case lineChartRace -} - -public extension DTOv2.Insight { - static func newTimeSeriesInsight(groupID: UUID) -> DTOv2.Insight { - DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: "New Time Series Insight", - type: "timeseries", - customQuery: nil, - signalType: nil, - uniqueUser: false, - filters: [:], - breakdownKey: nil, - groupBy: .day, - displayMode: .lineChart, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } - - static func newBreakdownInsight(groupID: UUID, title: String? = nil, breakdownKey: String? = nil) -> DTOv2.Insight { - DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: title ?? "New Breakdown Insight", - type: "topN", - customQuery: nil, - signalType: nil, - uniqueUser: false, - filters: [:], - breakdownKey: breakdownKey ?? "systemVersion", - groupBy: .day, - displayMode: .pieChart, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } - - static func newDailyUserCountInsight(groupID: UUID) -> DTOv2.Insight { - DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: "Daily Active Users", - type: "timeseries", - customQuery: nil, - signalType: nil, - uniqueUser: true, - filters: [:], - breakdownKey: nil, - groupBy: .day, - displayMode: .lineChart, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } - - static func newWeeklyUserCountInsight(groupID: UUID) -> DTOv2.Insight { - DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: "Weekly Active Users", - type: "timeseries", - customQuery: nil, - signalType: nil, - uniqueUser: true, - filters: [:], - breakdownKey: nil, - groupBy: .week, - displayMode: .barChart, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } - - static func newMonthlyUserCountInsight(groupID: UUID) -> DTOv2.Insight { - DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: "Active Users this Month", - type: "timeseries", - customQuery: nil, - signalType: nil, - uniqueUser: true, - filters: [:], - breakdownKey: nil, - groupBy: .month, - displayMode: .raw, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } - - static func newSignalInsight(groupID: UUID) -> DTOv2.Insight { - DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: "Signals by Day", - type: "timeseries", - customQuery: nil, - signalType: nil, - uniqueUser: false, - filters: [:], - breakdownKey: nil, - groupBy: .day, - displayMode: .lineChart, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } - - static func newCustomQueryInsight(groupID: UUID) -> DTOv2.Insight { - let customQuery = CustomQuery( - queryType: .groupBy, - dataSource: .init(type: .table, name: "telemetry-signals"), - intervals: [], - granularity: .all, - aggregations: [ - .longSum(.init(type: .longSum, name: "total_usage", fieldName: "count")), - ] - ) - - return DTOv2.Insight( - id: UUID.empty, - groupID: groupID, - order: nil, - title: "Custom Query", - type: "timeseries", - customQuery: customQuery, - signalType: nil, - uniqueUser: false, - filters: [:], - breakdownKey: nil, - groupBy: .day, - displayMode: .lineChart, - isExpanded: false, - lastRunTime: nil, - lastRunAt: nil - ) - } -} diff --git a/Sources/DataTransferObjects/DTOs/InsightCalculationResult.swift b/Sources/DataTransferObjects/DTOs/InsightCalculationResult.swift deleted file mode 100644 index f723840..0000000 --- a/Sources/DataTransferObjects/DTOs/InsightCalculationResult.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// InsightCalculationResult.swift -// -// -// Created by Daniel Jilg on 09.04.21. -// - -import Foundation - -public extension DTOv1 { - /// Defines an insight as saved to the database, no calculation results - struct InsightDTO: Codable, Identifiable { - public var id: UUID - public var group: [String: UUID] - - public var order: Double? - public var title: String - - /// Which signal types are we interested in? If nil, do not filter by signal type - public var signalType: String? - - /// If true, only include at the newest signal from each user - public var uniqueUser: Bool - - /// Only include signals that match all of these key-values in the payload - public var filters: [String: String] - - /// How far to go back to aggregate signals - public var rollingWindowSize: TimeInterval - - /// If set, break down the values in this key - public var breakdownKey: String? - - /// If set, group and count found signals by this time interval. Incompatible with breakdownKey - public var groupBy: QueryGranularity? - - /// How should this insight's data be displayed? - public var displayMode: InsightDisplayMode - - /// If true, the insight will be displayed bigger - public var isExpanded: Bool - - /// The amount of time (in seconds) this query took to calculate last time - public var lastRunTime: TimeInterval? - - /// The query that was last used to run this query - public var lastQuery: String? - - /// The date this query was last run - public var lastRunAt: Date? - } -} - -public extension DTOv1 { - /// Defines the result of an insight calculation - struct InsightCalculationResult: Identifiable, Codable { - public init(id: UUID, order: Double?, title: String, signalType: String?, uniqueUser: Bool, filters: [String: String], - rollingWindowSize _: TimeInterval, breakdownKey: String? = nil, groupBy: QueryGranularity? = nil, - displayMode: InsightDisplayMode, isExpanded: Bool, data: [DTOv1.InsightData], calculatedAt: Date, calculationDuration: TimeInterval) - { - self.id = id - self.order = order - self.title = title - self.signalType = signalType - self.uniqueUser = uniqueUser - self.filters = filters - self.breakdownKey = breakdownKey - self.groupBy = groupBy - self.displayMode = displayMode - self.isExpanded = isExpanded - self.data = data - self.calculatedAt = calculatedAt - self.calculationDuration = calculationDuration - } - - public let id: UUID - - public let order: Double? - public let title: String - - /// Which signal types are we interested in? If nil, do not filter by signal type - public let signalType: String? - - /// If true, only include at the newest signal from each user - public let uniqueUser: Bool - - /// Only include signals that match all of these key-values in the payload - public let filters: [String: String] - - /// If set, break down the values in this key - public var breakdownKey: String? - - /// If set, group and count found signals by this time interval. Incompatible with breakdownKey - public var groupBy: QueryGranularity? - - /// How should this insight's data be displayed? - public var displayMode: InsightDisplayMode - - /// If true, the insight will be displayed bigger - var isExpanded: Bool - - /// Current Live Calculated Data - public let data: [DTOv1.InsightData] - - /// When was this DTO calculated? - public let calculatedAt: Date - - /// How long did this DTO take to calculate? - public let calculationDuration: TimeInterval - } -} diff --git a/Sources/DataTransferObjects/DTOs/InsightData.swift b/Sources/DataTransferObjects/DTOs/InsightData.swift deleted file mode 100644 index f73fc35..0000000 --- a/Sources/DataTransferObjects/DTOs/InsightData.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// InsightData.swift -// -// -// Created by Daniel Jilg on 09.04.21. -// - -import Foundation - -public extension DTOv1 { - /// Actual row of data inside an InsightCalculationResult - struct InsightData: Hashable, Codable { - public var xAxisValue: String - public var yAxisValue: String? - - public init(xAxisValue: String, yAxisValue: String?) { - self.xAxisValue = xAxisValue - self.yAxisValue = yAxisValue - } - } -} diff --git a/Sources/DataTransferObjects/DTOs/KafkaSignalStructure.swift b/Sources/DataTransferObjects/DTOs/KafkaSignalStructure.swift deleted file mode 100644 index ec13b93..0000000 --- a/Sources/DataTransferObjects/DTOs/KafkaSignalStructure.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// KafkaSignalStructure.swift -// -// -// Created by Daniel Jilg on 05.06.21. -// - -import Foundation - -/// Signal with a dictionary-like payload, as received from the Ingester -public struct KafkaSignalStructureWithDict: Codable { - public let receivedAt: Date - public let isTestMode: String - public let appID: UUID - public let clientUser: String - public let sessionID: String - public let type: String - public let payload: [String: String] - - public var platform: String? { payload["platform"] } - public var systemVersion: String? { payload["systemVersion"] } - public var majorSystemVersion: String? { payload["majorSystemVersion"] } - public var majorMinorSystemVersion: String? { payload["majorMinorSystemVersion"] } - public var appVersion: String? { payload["appVersion"] } - public var buildNumber: String? { payload["buildNumber"] } - public var modelName: String? { payload["modelName"] } - public var architecture: String? { payload["architecture"] } - public var operatingSystem: String? { payload["operatingSystem"] } - public var targetEnvironment: String? { payload["targetEnvironment"] } - public var locale: String? { payload["locale"] } - public var telemetryClientVersion: String? { payload["telemetryClientVersion"] } - - public init( - receivedAt: Date, - isTestMode: String, - appID: UUID, - clientUser: String, - sessionID: String, - type: String, - payload: [String: String] - ) { - self.receivedAt = receivedAt - self.isTestMode = isTestMode - self.appID = appID - self.clientUser = clientUser - self.sessionID = sessionID - self.type = type - self.payload = payload - } - - public func toTagStructure() -> KafkaSignalStructureWithTags { - KafkaSignalStructureWithTags( - receivedAt: receivedAt, - isTestMode: isTestMode, - appID: appID, - clientUser: clientUser, - sessionID: sessionID, - type: type, - payload: KafkaSignalStructureWithDict.convertToMultivalueDimension(payload: payload), - - // Pull common payload keys out of the payload dictionary and put them one level up. This can help - // increase performance on the druid level by treating these as fields instead of having to - // string-search through payload for them. - platform: platform, - systemVersion: systemVersion, - majorSystemVersion: majorSystemVersion, - majorMinorSystemVersion: majorMinorSystemVersion, - appVersion: appVersion, - buildNumber: buildNumber, - modelName: modelName, - architecture: architecture, - operatingSystem: operatingSystem, - targetEnvironment: targetEnvironment, - locale: locale, - telemetryClientVersion: telemetryClientVersion - ) - } - - /// Maps the payload dictionary to a String based Array, with key and value concatenated with a colon : character - /// - /// The key should never contain a colon character. Should it contain one anyway, we'll replace it with - /// an underscore _ character. This way, we can ensure the first colon is always the delimiter character, - /// with everything before it the key and everything after it the value. - public static func convertToMultivalueDimension(payload: [String: String]?) -> [String] { - guard let payload = payload else { return [] } - return payload.map { key, value in key.replacingOccurrences(of: ":", with: "_") + ":" + value } - } -} - -/// Signal with a tag-like payload as string array, which is compatible with Druid -/// -/// @see convertToMultivalueDimension -public struct KafkaSignalStructureWithTags: Codable { - public let receivedAt: Date - public let isTestMode: String - public let appID: UUID - public let clientUser: String - public let sessionID: String - public let type: String - public let payload: [String] - - // Denormalized Payload Items - public let platform: String? - public let systemVersion: String? - public let majorSystemVersion: String? - public let majorMinorSystemVersion: String? - public let appVersion: String? - public let buildNumber: String? - public let modelName: String? - public let architecture: String? - public let operatingSystem: String? - public let targetEnvironment: String? - public let locale: String? - public let telemetryClientVersion: String? -} diff --git a/Sources/DataTransferObjects/DTOs/LexiconSignalDTO.swift b/Sources/DataTransferObjects/DTOs/LexiconSignalDTO.swift deleted file mode 100644 index 75467ab..0000000 --- a/Sources/DataTransferObjects/DTOs/LexiconSignalDTO.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// LexiconSignalDTO.swift -// -// -// Created by Daniel Jilg on 12.05.21. -// - -import Foundation - -public extension DTOv1 { - struct LexiconSignalDTO: Codable, Hashable, Identifiable { - public init(type: String, signalCount: Int, userCount: Int, sessionCount: Int) { - self.type = type - self.signalCount = signalCount - self.userCount = userCount - self.sessionCount = sessionCount - } - - public var id: String { type } - public var type: String - public var signalCount: Int - public var userCount: Int - public var sessionCount: Int - } -} diff --git a/Sources/DataTransferObjects/DTOs/OrgAdminEntry.swift b/Sources/DataTransferObjects/DTOs/OrgAdminEntry.swift deleted file mode 100644 index 6374bf7..0000000 --- a/Sources/DataTransferObjects/DTOs/OrgAdminEntry.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// OrgAdminEntry.swift -// Telemetry Admin -// -// Created by Charlotte Böhm on 30.07.21. -// - -import Foundation - -public extension DTOv1 { - struct OrgAdminEntry: Codable, Identifiable { - public var id: UUID - public var organisationName: String? - public var createdAt: Date? - public var updatedAt: Date? - public var appAdminEntries: [DTOv1.AppAdminEntry] - public var signalCount: Int = 0 - } -} diff --git a/Sources/DataTransferObjects/DTOs/OrganisationJoinRequest.swift b/Sources/DataTransferObjects/DTOs/OrganisationJoinRequest.swift deleted file mode 100644 index c415f2c..0000000 --- a/Sources/DataTransferObjects/DTOs/OrganisationJoinRequest.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// OrganisationJoinRequest.swift -// Telemetry Viewer -// -// Created by Daniel Jilg on 14.05.21. -// - -import Foundation - -public extension DTOv1 { - /// Sent to the server to create a user belonging to the organization - struct OrganizationJoinRequestDTO: Codable { - public init(email: String, receiveMarketingEmails: Bool, firstName: String, lastName: String, password: String, organizationID: UUID, registrationToken: String) { - self.email = email - self.firstName = firstName - self.lastName = lastName - self.password = password - self.organizationID = organizationID - self.registrationToken = registrationToken - self.receiveMarketingEmails = receiveMarketingEmails - } - - public var email: String - public var firstName: String - public var lastName: String - public var password: String - public let organizationID: UUID - public var registrationToken: String - public var receiveMarketingEmails: Bool - } -} diff --git a/Sources/DataTransferObjects/DTOs/OrganizationDTO.swift b/Sources/DataTransferObjects/DTOs/OrganizationDTO.swift deleted file mode 100644 index 983e0ec..0000000 --- a/Sources/DataTransferObjects/DTOs/OrganizationDTO.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// OrganizationDTO.swift -// -// -// Created by Daniel Jilg on 09.04.21. -// - -import Foundation - -public extension DTOv1 { - struct Organization: Codable, Hashable, Identifiable { - public init(id: UUID, name: String, isSuperOrg: Bool, createdAt: Date?, updatedAt: Date?) { - self.id = id - self.name = name - self.isSuperOrg = isSuperOrg - self.createdAt = createdAt - self.updatedAt = updatedAt - } - - public var id: UUID - public var name: String - public var isSuperOrg: Bool - public var createdAt: Date? - public var updatedAt: Date? - } -} diff --git a/Sources/DataTransferObjects/DTOs/RegistrationRequestBody.swift b/Sources/DataTransferObjects/DTOs/RegistrationRequestBody.swift deleted file mode 100644 index 26fff52..0000000 --- a/Sources/DataTransferObjects/DTOs/RegistrationRequestBody.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// RegistrationRequestBody.swift -// -// -// Created by Daniel Jilg on 14.05.21. -// - -import Foundation -public extension DTOv1 { - struct RegistrationRequestBody: Codable { - public init() {} - - public var registrationToken: String = "" - public var organisationName: String = "" - public var userFirstName: String = "" - public var userLastName: String = "" - public var userEmail: String = "" - public var userPassword: String = "" - public var userPasswordConfirm: String = "" - public var receiveMarketingEmails: Bool = false - public var countryCode: String? = "" - public var referralCode: String? - public var source: String? - public var tags: [String]? - - public var isValid: ValidationState { - if organisationName.isEmpty || userFirstName.isEmpty || userEmail.isEmpty || userPassword.isEmpty { - return .fieldsMissing - } - - if userPassword != userPasswordConfirm { - return .passwordsNotEqual - } - - if userPassword.count < 8 { - return .passwordTooShort - } - - if userPassword.contains(":") { - return .passwordContainsColon - } - - if !userEmail.contains("@") { - return .noAtInEmail - } - - return .valid - } - } - - enum ValidationState { - case valid - case fieldsMissing - case passwordsNotEqual - case passwordTooShort - case passwordContainsColon - case noAtInEmail - } -} diff --git a/Sources/DataTransferObjects/DTOs/SignalDTO.swift b/Sources/DataTransferObjects/DTOs/SignalDTO.swift deleted file mode 100644 index 4bc00a2..0000000 --- a/Sources/DataTransferObjects/DTOs/SignalDTO.swift +++ /dev/null @@ -1,109 +0,0 @@ -import Foundation - -public extension DTOv1 { - struct IdentifiableSignal: Codable, Hashable, Identifiable { - public var id = UUID() - public var appID: UUID? - public var count: Int - public var receivedAt: Date - public var clientUser: String - public var sessionID: String - public var type: String - public var payload: [String: String]? - public var floatValue: Double? - public var isTestMode: Bool - - public var signal: Signal { - Signal( - appID: appID, - count: count, - receivedAt: receivedAt, - clientUser: clientUser, - sessionID: sessionID, - type: type, - payload: payload, - floatValue: floatValue, - isTestMode: isTestMode - ) - } - } - - struct Signal: Codable, Hashable { - public init( - appID: UUID? = nil, - count: Int? = nil, - receivedAt: Date, - clientUser: String, - sessionID: String? = nil, - type: String, - payload: [String: String]? = nil, - floatValue: Double? = nil, - isTestMode: Bool - ) { - self.appID = appID - self.count = count - self.receivedAt = receivedAt - self.clientUser = clientUser - self.sessionID = sessionID - self.type = type - self.payload = payload - self.floatValue = floatValue - self.isTestMode = isTestMode - } - - public var appID: UUID? - public var count: Int? - public var receivedAt: Date - public var clientUser: String - public var sessionID: String? - public var type: String - public var payload: [String: String]? - public var floatValue: Double? - public var isTestMode: Bool - - public func toIdentifiableSignal() -> IdentifiableSignal { - IdentifiableSignal(id: UUID(), appID: appID, count: count ?? 1, receivedAt: receivedAt, clientUser: clientUser, - sessionID: sessionID ?? "–", type: type, payload: payload, floatValue: floatValue, isTestMode: isTestMode) - } - } - - struct SignalDruidStructure: Codable, Hashable { - public var appID: UUID? - public var count: Int? - public var receivedAt: Date - public var clientUser: String - public var sessionID: String? - public var type: String - public var payload: String - public var floatValue: Double? - public var isTestMode: String - - public func toSignal() -> Signal { - let payloadJSON = payload.replacingOccurrences(of: "\\", with: "").data(using: .utf8) - var actualPayload = [String: String]() - - if let payloadJSON = payloadJSON, - let payloadArray = try? JSONDecoder().decode([String].self, from: payloadJSON) - { - for entry in payloadArray { - let subsequence = entry.split(separator: ":", maxSplits: 1) - if let key = subsequence.first, let value = subsequence.last { - actualPayload[String(key)] = String(value) - } - } - } - - return Signal( - appID: appID, - count: count, - receivedAt: receivedAt, - clientUser: clientUser, - sessionID: sessionID, - type: type, - payload: actualPayload, - floatValue: floatValue, - isTestMode: isTestMode == "true" - ) - } - } -} diff --git a/Sources/DataTransferObjects/DTOs/UserDTO.swift b/Sources/DataTransferObjects/DTOs/UserDTO.swift deleted file mode 100644 index 5fb3788..0000000 --- a/Sources/DataTransferObjects/DTOs/UserDTO.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// UserDTO.swift -// -// -// Created by Daniel Jilg on 09.04.21. -// - -import Foundation - -public extension DTOv1 { - struct UserDTO: Identifiable, Codable { - public let id: UUID - public let organization: DTOv1.Organization? - public var firstName: String - public var lastName: String - public var email: String - public let emailIsVerified: Bool - public var receiveMarketingEmails: Bool? - public let isFoundingUser: Bool - public var receiveReports: ReportSendingRate - - public init( - id: UUID, - organization: DTOv1.Organization?, - firstName: String, - lastName: String, - email: String, - emailIsVerified: Bool, - receiveMarketingEmails: Bool?, - isFoundingUser: Bool, - receiveReports: ReportSendingRate - ) { - self.id = id - self.organization = organization - self.firstName = firstName - self.lastName = lastName - self.email = email - self.emailIsVerified = emailIsVerified - self.receiveMarketingEmails = receiveMarketingEmails - self.isFoundingUser = isFoundingUser - self.receiveReports = receiveReports - } - } -} diff --git a/Sources/DataTransferObjects/Extensions/String+CamelCase.swift b/Sources/DataTransferObjects/Extensions/String+CamelCase.swift deleted file mode 100644 index 044443f..0000000 --- a/Sources/DataTransferObjects/Extensions/String+CamelCase.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// String+CamelCase.swift -// Telemetry Viewer (iOS) -// -// Created by Daniel Jilg on 07.12.20. -// - -import Foundation - -public extension String { - var camelCaseToWords: String { - unicodeScalars.reduce("") { - if CharacterSet.uppercaseLetters.contains($1) { - return $0 + " " + String($1) - } - - return $0 + String($1) - } - } -} diff --git a/Sources/DataTransferObjects/Models.swift b/Sources/DataTransferObjects/Models.swift deleted file mode 100644 index 5baafaa..0000000 --- a/Sources/DataTransferObjects/Models.swift +++ /dev/null @@ -1,321 +0,0 @@ -// -// Models.swift -// Telemetry Viewer -// -// Created by Daniel Jilg on 09.08.20. -// - -import Foundation - -/// Data Transfer Objects -public enum DTOv1 { - public struct InsightGroup: Codable, Identifiable, Hashable { - public var id: UUID - public var title: String - public var order: Double? - public var insights: [InsightDTO] = [] - - public func getDTO() -> Self { - Self(id: id, title: title, order: order) - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - public init(id: UUID, title: String, order: Double? = nil) { - self.id = id - self.title = title - self.order = order - insights = [] - } - } - - public struct LexiconPayloadKey: Codable, Identifiable { - public init(id: UUID, firstSeenAt: Date, isHidden: Bool, payloadKey: String) { - self.id = id - self.firstSeenAt = firstSeenAt - self.isHidden = isHidden - self.payloadKey = payloadKey - } - - public let id: UUID - public let firstSeenAt: Date - - /// If true, don't include this lexicon item in autocomplete lists - public let isHidden: Bool - public let payloadKey: String - } - - /// Represents a standing invitation to join an organization - public struct OrganizationJoinRequest: Codable, Identifiable, Equatable { - public let id: UUID - public let email: String - public let registrationToken: String - public let organization: [String: UUID] - } -} - -@available(*, deprecated, message: "Use DTOv2.App instead") -public struct TelemetryApp: Codable, Hashable, Identifiable { - public init(id: UUID, name: String, organization: [String: String]) { - self.id = id - self.name = name - self.organization = organization - } - - public var id: UUID - public var name: String - public var organization: [String: String] -} - -public struct InsightDefinitionRequestBody: Codable { - public init(order: Double? = nil, title: String, signalType: String? = nil, uniqueUser: Bool, - filters: [String: String], rollingWindowSize: TimeInterval, breakdownKey: String? = nil, - groupBy: QueryGranularity? = nil, displayMode: InsightDisplayMode, groupID: UUID? = nil, id: UUID? = nil, isExpanded: Bool) - { - self.order = order - self.title = title - self.signalType = signalType - self.uniqueUser = uniqueUser - self.filters = filters - self.rollingWindowSize = rollingWindowSize - self.breakdownKey = breakdownKey - self.groupBy = groupBy - self.displayMode = displayMode - self.groupID = groupID - self.id = id - self.isExpanded = isExpanded - } - - public var order: Double? - public var title: String - - /// Which signal types are we interested in? If nil, do not filter by signal type - public var signalType: String? - - /// If true, only include at the newest signal from each user - public var uniqueUser: Bool - - /// Only include signals that match all of these key-values in the payload - public var filters: [String: String] - - /// How far to go back to aggregate signals - public var rollingWindowSize: TimeInterval - - /// If set, break down the values in this key - public var breakdownKey: String? - - /// If set, group and count found signals by this time interval. Incompatible with breakdownKey - public var groupBy: QueryGranularity? - - /// How should this insight's data be displayed? - public var displayMode: InsightDisplayMode - - /// Which group should the insight belong to? (Only use this in update mode) - public var groupID: UUID? - - /// The ID of the insight. Not changeable, only set in update mode - public var id: UUID? - - /// If true, the insight will be displayed bigger - public var isExpanded: Bool - - public static func from(insight: DTOv1.InsightDTO) -> InsightDefinitionRequestBody { - let requestBody = Self( - order: insight.order, - title: insight.title, - signalType: insight.signalType, - uniqueUser: insight.uniqueUser, - filters: insight.filters, - rollingWindowSize: insight.rollingWindowSize, - breakdownKey: insight.breakdownKey, - groupBy: insight.groupBy ?? .day, - displayMode: insight.displayMode, - groupID: insight.group["id"], - id: insight.id, - isExpanded: insight.isExpanded - ) - - return requestBody - } -} - -public enum RegistrationStatus: String, Codable { - case closed - case tokenOnly - case open -} - -public enum TransferError: Error { - case transferFailed - case decodeFailed - case serverError(message: String) - - public var localizedDescription: String { - switch self { - case .transferFailed: - return "There was a communication error with the server. Please check your internet connection and try again later." - case .decodeFailed: - return "The server returned a message that this version of the app could not decode. Please check if there is an update to the app, or contact the developer." - case let .serverError(message: message): - return "The server returned this error message: \(message)" - } - } -} - -public struct ServerErrorDetailMessage: Codable { - public let detail: String -} - -public struct ServerErrorReasonMessage: Codable { - public let reason: String -} - -public struct PasswordChangeRequestBody: Codable { - public init(oldPassword: String, newPassword: String, newPasswordConfirm: String) { - self.oldPassword = oldPassword - self.newPassword = newPassword - self.newPasswordConfirm = newPasswordConfirm - } - - public var oldPassword: String - public var newPassword: String - public var newPasswordConfirm: String -} - -public struct BetaRequestEmailDTO: Codable, Identifiable, Equatable { - public let id: UUID - public let email: String - public let registrationToken: String - public let requestedAt: Date - public let sentAt: Date? - public let isFulfilled: Bool -} - -public struct LoginRequestBody { - public init(userEmail: String = "", userPassword: String = "") { - self.userEmail = userEmail - self.userPassword = userPassword - } - - public var userEmail: String = "" - public var userPassword: String = "" - - public var basicHTMLAuthString: String? { - let loginString = "\(userEmail):\(userPassword)" - guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } - let base64LoginString = loginData.base64EncodedString() - return "Basic \(base64LoginString)" - } - - public var isValid: Bool { - !userEmail.isEmpty && !userPassword.isEmpty - } -} - -public struct RequestPasswordResetRequestBody: Codable { - public init(email: String = "", code: String = "", newPassword: String = "") { - self.email = email - self.code = code - self.newPassword = newPassword - } - - public var email: String = "" - public var code: String = "" - public var newPassword: String = "" - - public var isValidEmailAddress: Bool { - !email.isEmpty - } - - public var isValid: Bool { - !email.isEmpty && !code.isEmpty && !newPassword.isEmpty - } -} - -public struct UserTokenDTO: Codable { - public init(id: UUID? = nil, value: String, user: [String: String]) { - self.id = id - self.value = value - self.user = user - } - - public var id: UUID? - public var value: String - public var user: [String: String] - - public var bearerTokenAuthString: String { - "Bearer \(value)" - } -} - -public struct BetaRequestUpdateBody: Codable { - public init(sentAt: Date?, isFulfilled: Bool) { - self.sentAt = sentAt - self.isFulfilled = isFulfilled - } - - public let sentAt: Date? - public let isFulfilled: Bool -} - -public struct OrganizationAdminListEntry: Codable, Identifiable { - public let id: UUID - public let name: String - public let foundedAt: Date - public let sumSignals: Int - public let isSuperOrg: Bool - public let firstName: String? - public let lastName: String? - public let email: String -} - -public enum AppRootViewSelection: Hashable { - case insightGroup(group: DTOv1.InsightGroup) - case lexicon - case rawSignals - case noSelection -} - -public enum LoadingState: Equatable { - case idle - case loading - case finished(Date) - case error(String, Date) -} - -public struct QueryTaskStatusStruct: Equatable, Codable { - public var status: QueryTaskStatus -} - -public enum QueryTaskStatus: String, Equatable, Codable { - public var id: String { rawValue } - - case running - case successful - case error -} - -public enum RelativeDateDescription: Equatable { - case end(of: CurrentOrPrevious) - case beginning(of: CurrentOrPrevious) - case goBack(days: Int) - case absolute(date: Date) -} - -public enum CurrentOrPrevious: Equatable { - case current(_ value: Calendar.Component) - case previous(_ value: Calendar.Component) -} - -public enum ReportSendingRate: String, Codable { - case daily - case weekly - case monthly - case never -} diff --git a/Tests/DataTransferObjectsTests/ChartDefinitionTests.swift b/Tests/DataTransferObjectsTests/ChartDefinitionTests.swift deleted file mode 100644 index 82649fb..0000000 --- a/Tests/DataTransferObjectsTests/ChartDefinitionTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -@testable import DataTransferObjects -import XCTest - -final class ChartDefinitionTests: XCTestCase { - func testDataSectionDecoding() { - let exampleData = """ - { - "x":"x", - "columns": [ - ["x","2013-01-01", "2013-01-02", "2013-01-03", "2013-01-04", "2013-01-05", "2013-01-06"], - ["data1","30", "200", "100", "400", "150", "250"], - ] - } - """ - .data(using: .utf8)! - - XCTAssertNoThrow(try JSONDecoder.telemetryDecoder.decode(ChartDefinitionDTO.DataSection.self, from: exampleData)) - } - - func testDataSectionEncoding() throws { - let exampleDataSection = ChartDefinitionDTO.DataSection(x: "x", xFormat: nil, columns: []) - - let expectedResult = """ - {"columns":[],"x":"x"} - """ - - XCTAssertEqual(try String(data: JSONEncoder.telemetryEncoder.encode(exampleDataSection), encoding: .utf8)!, expectedResult) - } - - func testColumnEncoding() throws { - let exampleColumn = ChartDefinitionDTO.DataSection.Column(label: "data1", data: ["12", "31", nil, "42"]) - - let expectedResult = """ - ["data1","12","31",null,"42"] - """ - - XCTAssertEqual(try String(data: JSONEncoder.telemetryEncoder.encode(exampleColumn), encoding: .utf8)!, expectedResult) - } - - func testColumnDecoding() throws { - let expectedResult = ChartDefinitionDTO.DataSection.Column(label: "data1", data: ["12", "31", nil, "42"]) - - let testData = """ - ["data1", "12", "31", null, "42"] - """ - .data(using: .utf8)! - - XCTAssertEqual(try JSONDecoder.telemetryDecoder.decode(ChartDefinitionDTO.DataSection.Column.self, from: testData), expectedResult) - } -} diff --git a/Tests/DataTransferObjectsTests/EncodingDecodingTests.swift b/Tests/DataTransferObjectsTests/EncodingDecodingTests.swift deleted file mode 100644 index 90cc864..0000000 --- a/Tests/DataTransferObjectsTests/EncodingDecodingTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -@testable import DataTransferObjects -import XCTest - -final class EncodingDecodingTests: XCTestCase { - func testAppSettingsEncoding() throws { - let input = DTOv2.AppSettings(displayMode: .app) - - let output = try JSONEncoder.telemetryEncoder.encode(input) - - let expectedOutput = """ - { - "displayMode": "app" - } - """ - .filter { !$0.isWhitespace } - - XCTAssertEqual(expectedOutput, String(data: output, encoding: .utf8)!) - } - - func testAppSettingsDecoding() throws { - let input = """ - { - "displayMode": "website" - } - """ - .filter { !$0.isWhitespace } - - let output = try JSONDecoder.telemetryDecoder.decode(DTOv2.AppSettings.self, from: input.data(using: .utf8)!) - - XCTAssertEqual(output.displayMode, .website) - } - - func testAppSettingsDecodingMore() throws { - let input = """ - { - "displayMode": "website", - "showExampleData": false - } - """ - .filter { !$0.isWhitespace } - - let output = try JSONDecoder.telemetryDecoder.decode(DTOv2.AppSettings.self, from: input.data(using: .utf8)!) - - XCTAssertEqual(output.displayMode, .website) - } - - func testAppSettingsDecodingMoreMore() throws { - let input = """ - { - "displayMode": "website", - "showExampleData": false, - "colorScheme": "#f00 #0f0 #00f" - } - """ - .filter { !$0.isWhitespace } - - let output = try JSONDecoder.telemetryDecoder.decode(DTOv2.AppSettings.self, from: input.data(using: .utf8)!) - - XCTAssertEqual(output.displayMode, .website) - } -} diff --git a/Tests/QueryGenerationTests/CompileDownTests.swift b/Tests/QueryGenerationTests/CompileDownTests.swift index 0bef269..ddcb57e 100644 --- a/Tests/QueryGenerationTests/CompileDownTests.swift +++ b/Tests/QueryGenerationTests/CompileDownTests.swift @@ -19,7 +19,7 @@ final class CompileDownTests: XCTestCase { let query = CustomQuery(queryType: .funnel, relativeIntervals: relativeIntervals, granularity: .all, steps: steps) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) // Exact query generation is in FunnelQueryGenerationTests, // here we're just making sure we're jumping into the correct paths. @@ -28,7 +28,7 @@ final class CompileDownTests: XCTestCase { func testBaseFiltersThisOrganization() throws { let query = CustomQuery(queryType: .timeseries, baseFilters: .thisOrganization, relativeIntervals: relativeIntervals, granularity: .all) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) XCTAssertEqual( precompiledQuery.filter, @@ -53,12 +53,12 @@ final class CompileDownTests: XCTestCase { func testBaseFiltersThisApp() throws { // this should fail because the query does not have an appID let queryFailing = CustomQuery(queryType: .timeseries, baseFilters: .thisApp, relativeIntervals: relativeIntervals, granularity: .all) - XCTAssertThrowsError(try queryFailing.precompile(organizationAppIDs: [], isSuperOrg: false)) + XCTAssertThrowsError(try queryFailing.precompile(useNamespace: false, organizationAppIDs: [], isSuperOrg: false)) // This should succeed because an app ID is provided let appID = UUID() let query = CustomQuery(queryType: .timeseries, appID: appID, baseFilters: .thisApp, relativeIntervals: relativeIntervals, granularity: .all) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID, appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID, appID1, appID2], isSuperOrg: false) XCTAssertEqual( precompiledQuery.filter, @@ -71,7 +71,7 @@ final class CompileDownTests: XCTestCase { func testBaseFiltersExampleData() throws { let query = CustomQuery(queryType: .timeseries, baseFilters: .exampleData, relativeIntervals: relativeIntervals, granularity: .all) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) XCTAssertEqual( precompiledQuery.filter, @@ -86,10 +86,10 @@ final class CompileDownTests: XCTestCase { let query = CustomQuery(queryType: .timeseries, baseFilters: .noFilter, relativeIntervals: relativeIntervals, granularity: .all) // this should fail because isSuperOrg is not set to true - XCTAssertThrowsError(try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false)) + XCTAssertThrowsError(try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false)) // this should succeed because isSuperOrg is set to true - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: true) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: true) XCTAssertNil(precompiledQuery.filter) } @@ -97,21 +97,21 @@ final class CompileDownTests: XCTestCase { func testDataSource() throws { // No datasource means data source is telemetry-signals let query1 = CustomQuery(queryType: .timeseries, baseFilters: .thisOrganization, relativeIntervals: relativeIntervals, granularity: .all) - XCTAssertEqual(try query1.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false).dataSource, DataSource("telemetry-signals")) + XCTAssertEqual(try query1.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false).dataSource, DataSource("telemetry-signals")) // Specified datasource but not noFilter + super org will be replaced by telemetry-signals let query2 = CustomQuery(queryType: .timeseries, dataSource: "some-data-source", baseFilters: .thisOrganization, relativeIntervals: relativeIntervals, granularity: .all) - XCTAssertEqual(try query2.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false).dataSource, DataSource("telemetry-signals")) + XCTAssertEqual(try query2.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false).dataSource, DataSource("telemetry-signals")) // Specified datasource will be retained if super org is set let query3 = CustomQuery(queryType: .timeseries, dataSource: "some-data-source", baseFilters: .noFilter, relativeIntervals: relativeIntervals, granularity: .all) - XCTAssertEqual(try query3.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: true).dataSource, DataSource("some-data-source")) + XCTAssertEqual(try query3.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: true).dataSource, DataSource("some-data-source")) } func testThrowsIfNeitherIntervalsNorRelativeIntervalsSet() throws { let query = CustomQuery(queryType: .timeseries, baseFilters: .noFilter, intervals: nil, relativeIntervals: nil, granularity: .all) - XCTAssertThrowsError(try query.precompile(organizationAppIDs: [], isSuperOrg: false)) + XCTAssertThrowsError(try query.precompile(useNamespace: false, organizationAppIDs: [], isSuperOrg: false)) } func testCompilationFailsIfNoPrecompilation() throws { @@ -121,7 +121,7 @@ final class CompileDownTests: XCTestCase { func testIntervalsAreCreated() throws { let query = CustomQuery(queryType: .timeseries, relativeIntervals: relativeIntervals, granularity: .all) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) let compiledQuery = try precompiledQuery.compileToRunnableQuery() XCTAssertNotNil(compiledQuery.intervals) @@ -148,7 +148,7 @@ final class CompileDownTests: XCTestCase { granularity: .all ) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) let compiledQuery = try precompiledQuery.compileToRunnableQuery() guard case .and(let testModeFilter) = compiledQuery.filter else { @@ -207,7 +207,7 @@ final class CompileDownTests: XCTestCase { ), ] ) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) let compiledQuery = try precompiledQuery.compileToRunnableQuery() guard let aggregation = compiledQuery.aggregations?.first else { @@ -231,7 +231,7 @@ final class CompileDownTests: XCTestCase { func testCompilationStatusIsSetCorrectly() throws { let query = CustomQuery(queryType: .timeseries, relativeIntervals: relativeIntervals, granularity: .all) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) let compiledQuery = try precompiledQuery.compileToRunnableQuery() XCTAssertEqual(precompiledQuery.compilationStatus, .precompiled) @@ -260,7 +260,7 @@ final class CompileDownTests: XCTestCase { ] let query = CustomQuery(queryType: .timeseries, restrictions: restrictions, intervals: intervals, granularity: .all) - let precompiledQuery = try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false) let compiledQuery = try precompiledQuery.compileToRunnableQuery() XCTAssertEqual(compiledQuery.restrictions, [ @@ -276,7 +276,7 @@ final class CompileDownTests: XCTestCase { var query = CustomQuery(queryType: .timeseries, intervals: intervals, granularity: .day) query.dataSource = nil - let precompiledQuery = try query.precompile(namespace: "com.telemetrydeck.test", organizationAppIDs: [appID1, appID2], isSuperOrg: false) + let precompiledQuery = try query.precompile(namespace: "com.telemetrydeck.test", useNamespace: true, organizationAppIDs: [appID1, appID2], isSuperOrg: false) let compiledQuery = try precompiledQuery.compileToRunnableQuery() XCTAssertEqual(compiledQuery.dataSource?.name, "com.telemetrydeck.test") } @@ -286,7 +286,7 @@ final class CompileDownTests: XCTestCase { .init(beginningDate: Date(iso8601String: "2023-04-01T00:00:00.000Z")!, endDate: Date(iso8601String: "2023-05-31T00:00:00.000Z")!), ] let query = CustomQuery(queryType: .timeseries, intervals: intervals, granularity: .hour) - XCTAssertNoThrow(try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false)) + XCTAssertNoThrow(try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false)) } func testAllowsDailyGranularityForTopN() throws { @@ -294,7 +294,7 @@ final class CompileDownTests: XCTestCase { .init(beginningDate: Date(iso8601String: "2023-04-01T00:00:00.000Z")!, endDate: Date(iso8601String: "2023-05-31T00:00:00.000Z")!), ] let query = CustomQuery(queryType: .topN, intervals: intervals, granularity: .day) - XCTAssertNoThrow(try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false)) + XCTAssertNoThrow(try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false)) } func testAllowsDailyGranularityForGroupBy() throws { @@ -302,7 +302,7 @@ final class CompileDownTests: XCTestCase { .init(beginningDate: Date(iso8601String: "2023-04-01T00:00:00.000Z")!, endDate: Date(iso8601String: "2023-05-31T00:00:00.000Z")!), ] let query = CustomQuery(queryType: .groupBy, intervals: intervals, granularity: .day) - XCTAssertNoThrow(try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false)) + XCTAssertNoThrow(try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false)) } func testDisallowsHourlyQueriesForTopN() throws { @@ -311,7 +311,7 @@ final class CompileDownTests: XCTestCase { ] let query = CustomQuery(queryType: .topN, intervals: intervals, granularity: .hour) - XCTAssertThrowsError(try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false)) + XCTAssertThrowsError(try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false)) } func testDisallowsHourlyQueriesForGroupBy() throws { @@ -320,6 +320,6 @@ final class CompileDownTests: XCTestCase { ] let query = CustomQuery(queryType: .groupBy, intervals: intervals, granularity: .hour) - XCTAssertThrowsError(try query.precompile(organizationAppIDs: [appID1, appID2], isSuperOrg: false)) + XCTAssertThrowsError(try query.precompile(useNamespace: false, organizationAppIDs: [appID1, appID2], isSuperOrg: false)) } } diff --git a/Tests/QueryGenerationTests/ConvenienceAggregatorTests.swift b/Tests/QueryGenerationTests/ConvenienceAggregatorTests.swift index 5b0f00b..0f7a64e 100644 --- a/Tests/QueryGenerationTests/ConvenienceAggregatorTests.swift +++ b/Tests/QueryGenerationTests/ConvenienceAggregatorTests.swift @@ -4,21 +4,21 @@ import XCTest final class ConvenienceAggregatorTests: XCTestCase { func testUserCountQueryGetsPrecompiled() throws { let query = CustomQuery(queryType: .timeseries, aggregations: [.userCount(.init())]) - let precompiled = try query.precompile(organizationAppIDs: [UUID()], isSuperOrg: false) + let precompiled = try query.precompile(useNamespace: true, organizationAppIDs: [UUID()], isSuperOrg: false) let expectedAggregations: [Aggregator] = [.thetaSketch(.init(name: "Users", fieldName: "clientUser"))] XCTAssertEqual(precompiled.aggregations, expectedAggregations) } func testEventCountQueryGetsPrecompiled() throws { let query = CustomQuery(queryType: .timeseries, aggregations: [.eventCount(.init())]) - let precompiled = try query.precompile(organizationAppIDs: [UUID()], isSuperOrg: false) + let precompiled = try query.precompile(useNamespace: true, organizationAppIDs: [UUID()], isSuperOrg: false) let expectedAggregations: [Aggregator] = [.longSum(.init(type: .longSum, name: "Events", fieldName: "count"))] XCTAssertEqual(precompiled.aggregations, expectedAggregations) } func testHistogramQueryGetsPrecompiled() throws { let query = CustomQuery(queryType: .timeseries, aggregations: [.histogram(.init())]) - let precompiled = try query.precompile(organizationAppIDs: [UUID()], isSuperOrg: false) + let precompiled = try query.precompile(useNamespace: true, organizationAppIDs: [UUID()], isSuperOrg: false) let expectedAggregations: [Aggregator] = [ .quantilesDoublesSketch(.init(name: "_histogramSketch", fieldName: "floatValue", k: 1024, maxStreamLength: nil, shouldFinalize: nil)), .longMin(.init(type: .longMin, name: "_quantilesMinValue", fieldName: "floatValue")), diff --git a/Tests/QueryGenerationTests/ExperimentQueryGenerationTests.swift b/Tests/QueryGenerationTests/ExperimentQueryGenerationTests.swift index a7c2008..ecf2048 100644 --- a/Tests/QueryGenerationTests/ExperimentQueryGenerationTests.swift +++ b/Tests/QueryGenerationTests/ExperimentQueryGenerationTests.swift @@ -135,7 +135,7 @@ final class ExperimentQueryGenerationTests: XCTestCase { sample2: cohort2, successCriterion: successCriterion ) - let generatedTinyQuery = try startingQuery.precompile(organizationAppIDs: organizationAppIDs, isSuperOrg: false) + let generatedTinyQuery = try startingQuery.precompile(useNamespace: false, organizationAppIDs: organizationAppIDs, isSuperOrg: false) XCTAssertEqual(tinyQuery.filter, generatedTinyQuery.filter) XCTAssertEqual(tinyQuery.aggregations, generatedTinyQuery.aggregations)