Sources/XCMetricsBackendLib/Builds/Controllers/BuildController.swift (169 lines of code) (raw):

// Copyright (c) 2020 Spotify AB. // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import Fluent import FluentSQL import Vapor /// Controller with endpoints that return Builds related data public struct BuildController: RouteCollection { /// Returns the routes supported by this Controller. /// All the routes are in the `v1/build` path /// - Parameter routes: RoutesBuilder to which the routes will be added /// - Throws: An `Error` if something goes wrong public func boot(routes: RoutesBuilder) throws { routes.get("v1", "build", "error", ":id", use: buildErrors) routes.get("v1", "build", "host", ":id", use: buildHost) routes.get("v1", "build", "warning", ":id", use: buildWarnings) routes.get("v1", "build", "metadata", ":id", use: metadata) routes.post("v1", "build", "filter", use: list) routes.get("v1", "build", "project", use: projects) routes.get("v1", "build", ":id", use: build) routes.get("v1", "build", use: index) routes.get("v1", "build", "step", ":day", ":id", use: targetSteps) routes.post("v1", "build", "metadata", "filter", use: metadataFilter) } /// Endpoint that returns the paginated list of `Build` /// sorted by date from the most recent to the least recent. /// - Method: `GET` /// - Route: `/v1/build?page=1&per=10` /// - Request parameters /// - `page`. Optional. Page number to fetch. Default is `1` /// - `per`. Optional. Number of items to fetch per page. Default is `10` /// - Response: /// /// ``` /// { /// "metadata": { /// "per": 10, /// "total": 100, /// "page": 2 /// }, /// "items": [ /// { /// "userid": "tim", /// "warningCount": 3, /// "duration": 1.10, /// "isCi": false, /// "startTimestamp": "2020-11-02T16:36:22Z", /// "startTimestampMicroseconds": 1604334982.5824749, /// "category": "noop", /// "endTimestampMicroseconds": 1604334993.6019359, /// "tag": "", /// "compilationEndTimestamp": "2020-11-02T16:36:22Z", /// "compilationDuration": 0, /// "projectName": "MyProject", /// "compilationEndTimestampMicroseconds": 1604334982.5824749, /// "errorCount": 0, /// "buildStatus": "succeeded", /// "day": "2020-11-02T00:00:00Z", /// "id": "MyMac_D682E30D-AF89-4712-A78E-85DC0AAB83C8_1", /// "schema": "App", /// "compiledCount": 0, /// "endTimestamp": "2020-11-02T16:36:33Z", /// "userid256": "c28b6fd9a49bd8c74767501a114784d327336f3ff861873341b5b64900125463", /// "machineName": "MyMac", /// "wasSuspended": false /// }, /// ... /// ] /// } /// ``` /// public func index(req: Request) -> EventLoopFuture<Page<Build>> { return Build.query(on: req.db) .sort(\.$startTimestampMicroseconds, .descending) .paginate(for: req) } /// Endpoint that returns the paginated list of `Build` /// filtered by different criteria, like creation date, build status and project name /// - Method: `POST` /// - Route: `/v1/build/filter` /// - Request body /// /// ``` /// { /// "from": "2020-10-23T04:00:00Z", /// "to": "2021-10-24T17:00:00Z", /// "page": 1, /// "per": 5, /// "projectName": "MyProject", /// "status": "failed" /// } /// ``` /// /// - Body Parameters /// - `from`. Lower limit creation date of the `Build` /// - `to`. Upper limit creation date of the `Build` /// - `projectName`. Optional. Name of the project that was built /// - `status`. Optional. Status of the `Build`. Possible values: `succeeded`, `failed` or `stopped` /// - `page`. Optional. Page number to fetch. Default is `1` /// - `per`. Optional. Number of items to fetch per page. Default is `10` /// /// - Response: /// /// ``` /// { /// "metadata": { /// "per": 5, /// "total": 6, /// "page": 1 /// }, /// "items": [ /// { /// "userid": "tim", /// "warningCount": 3, /// "duration": 1.4963999999999999e-05, /// "startTimestamp": "2020-11-02T16:38:40Z", /// "isCi": false, /// "startTimestampMicroseconds": 1604335120.279242, /// "category": "incremental", /// "endTimestampMicroseconds": 1604335135.242979, /// "day": "2020-11-02T00:00:00Z", /// "compilationEndTimestamp": "2020-11-02T16:38:55Z", /// "compilationDuration": 1.4849e-05, /// "projectName": "MyProject", /// "compilationEndTimestampMicroseconds": 1604335135.128335, /// "buildStatus": "failed", /// "id": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1", /// "tag": "", /// "errorCount": 1, /// "schema": "MyProject", /// "compiledCount": 86, /// "endTimestamp": "2020-11-02T16:38:55Z", /// "userid256": "c28b6fd9a49bd8c74767501a114784d327336f3ff861873341b5b64900125463", /// "machineName": "MyMac", /// "wasSuspended": false /// }, /// ... /// ] /// } /// ``` /// public func list(req: Request) throws -> EventLoopFuture<Page<Build>> { let params = try req.content.decode(BuildListParams.self) let query = Build.query(on: req.db) .filter(\.$startTimestamp >= params.from) .filter(\.$startTimestamp <= params.to) if params.excludeCI { query.filter(\.$isCi == false) } if let status = params.status { query.filter(\.$buildStatus == status) } if let projectName = params.projectName { query.filter(\.$projectName == projectName) } return query.sort(\.$startTimestampMicroseconds, .descending) .paginate(PageRequest(page: params.page, per: params.per)) } /// Endpoint that returns the most relevant information of a `Build`: /// `Build` data, Xcode used and list of `Target` that were built. /// - Method: `GET` /// - Route: `/v1/build/<buildId>` /// - Path parameters /// - `buildId`. Mandatory. `Build`'s identifier /// /// - Response: /// /// ``` /// { /// "build": { /// "userid": "tim", /// "warningCount": 3, /// "duration": 1.4, /// "startTimestamp": "2020-11-02T16:38:40Z", /// "isCi": false, /// "startTimestampMicroseconds": 1604335120.279242, /// "category": "incremental", /// "endTimestampMicroseconds": 1604335135.242979, /// "day": "2020-11-02T00:00:00Z", /// "compilationEndTimestamp": "2020-11-02T16:38:55Z", /// "compilationDuration": 1.48, /// "projectName": "MyProject", /// "compilationEndTimestampMicroseconds": 1604335135.128335, /// "buildStatus": "failed", /// "id": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1", /// "tag": "", /// "errorCount": 1, /// "schema": "App", /// "compiledCount": 86, /// "endTimestamp": "2020-11-02T16:38:55Z", /// "userid256": "c28b6fd9a49bd8c74767501a114784d327336f3ff861873341b5b64900125463", /// "machineName": "MyMac", /// "wasSuspended": false /// }, /// "xcode": { /// "buildNumber": "12A7209", /// "id": "6354C87F-0ADC-4354-929C-02EBE545E099", /// "buildIdentifier": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1", /// "day": "2020-11-02T00:00:00Z", /// "version": "1200" /// }, /// "targets": [ /// { /// "id": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1992", /// "category": "noop", /// "startTimestamp": "2020-11-02T10:59:09Z", /// "compilationEndTimestampMicroseconds": 1604314749.2909288, /// "endTimestampMicroseconds": 1604314982.298002, /// "endTimestamp": "2020-11-02T11:03:02Z", /// "fetchedFromCache": true, /// "errorCount": 0, /// "day": "2020-11-02T00:00:00Z", /// "warningCount": 0, /// "compilationEndTimestamp": "2020-11-02T10:59:09Z", /// "compilationDuration": 0, /// "compiledCount": 0, /// "duration": 0.000233007, /// "buildIdentifier": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1", /// "name": "Model", /// "startTimestampMicroseconds": 1604314749.2909288 /// }, /// ... /// ] /// } /// ``` /// public func build(req: Request) throws -> EventLoopFuture<BuildResponse> { guard let buildId = req.parameters.get("id") else { throw Abort(.badRequest) } return Build.query(on: req.db) .filter(\.$id == buildId) .first() .flatMapThrowing({ build -> Build in guard let build = build else { throw Abort(.notFound) } return build }) .flatMap({ build -> EventLoopFuture<([Target], Build)> in // Optimization to speed up queries if we're using tables sharded by day if let day = build.day, let sql = req.db as? SQLDatabase { return sql.raw(""" SELECT * FROM \(raw: Target.schema)_\(raw: day.xcm_toPartitionedTableFormat()) WHERE build_identifier = \(literal: buildId) ORDER BY start_timestamp_microseconds, name """) .all(decoding: Target.self) .and(value: build) } else { return Target.query(on: req.db) .filter(\.$buildIdentifier == buildId) .sort(\.$startTimestampMicroseconds) .sort(\.$name) .all() .and(value: build) } }) .and( XcodeVersion.query(on: req.db) .filter(\.$buildIdentifier == buildId) .first() ) .map({ (data, xcode: XcodeVersion?) -> (BuildResponse) in let (targets, build) = data return BuildResponse(build: build, targets: targets, xcode: xcode) }) } /// Endpoint that returns the list of errors of the given `Build` /// - Method: `GET` /// - Route: `/v1/build/error/<buildId>` /// - Path parameters /// - `buildId`. Mandatory. `Build`'s identifier /// /// - Response: /// /// ``` /// [ /// { /// "detail": "\/Users\/<redacted>\/myproject\/Sources\/MyClass.m:241:97:// / error: instance method 'fetch' not found ; did you mean 'fetchIt'?\r /// myclass:[self.myService fetch]\r ^~~~~~~~~~~~~~\r fetch\r /// 1 error generated.\r", /// "characterRangeEnd": 13815, /// "id": "3E6EF185-6AC1-4E95-87E8-E305F41916E9", /// "endingColumn": 97, /// "parentIdentifier": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_8860", /// "day": "2020-11-02T00:00:00Z", /// "type": "clangError", /// "title": "Instance method 'fetch' not found ; did you mean 'fetchIt'?", /// "endingLine": 241, /// "severity": 2, /// "startingLine": 241, /// "parentType": "step", /// "buildIdentifier": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1", /// "startingColumn": 97, /// "characterRangeStart": 0, /// "documentURL": "file:\/\/\/Users\/<redacted>\/myproject\/Sources\/MyClass.m" /// } /// ] /// ``` /// public func buildErrors(req: Request) throws -> EventLoopFuture<[BuildError]> { guard let buildId = req.parameters.get("id") else { throw Abort(.badRequest) } return BuildError.query(on: req.db) .filter(\.$buildIdentifier == buildId) .all() } /// Endpoint that returns the list of warnings of the given `Build` /// - Method: `GET` /// - Route: `/v1/build/warning/<buildId>` /// - Path parameters /// - `buildId`. Mandatory. `Build`'s identifier /// /// - Response: /// /// ``` /// [ /// { /// "detail": null, /// "characterRangeEnd": 9817, /// "documentURL": "file:\/\/\/Users\/<redacted>\/myproject\/Sources\/MyViewController.m", /// "endingColumn": 22, /// "id": "5F2011AC-F87F-4EDC-BBC6-2BBA3D789EB3", /// "parentIdentifier": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1845", /// "day": "2020-11-02T00:00:00Z", /// "type": "deprecatedWarning", /// "title": "'dimsBackgroundDuringPresentation' is deprecated: first deprecated in iOS 12.0", /// "endingLine": 235, /// "severity": 1, /// "startingLine": 235, /// "parentType": "step", /// "clangFlag": "[-Wdeprecated-declarations]", /// "startingColumn": 22, /// "buildIdentifier": "MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1", /// "characterRangeStart": 0 /// } /// ] /// ``` /// public func buildWarnings(req: Request) throws -> EventLoopFuture<[BuildWarning]> { guard let buildId = req.parameters.get("id") else { throw Abort(.badRequest) } return BuildWarning.query(on: req.db) .filter(\.$buildIdentifier == buildId) .all() } /// Endpoint that returns the data of the host used in the given `Build` /// - Method: `GET` /// - Route: `/v1/build/host/<buildId>` /// - Path parameters /// - `buildId`. Mandatory. `Build`'s identifier /// /// - Response: /// /// ``` /// { /// "id": "9DD5508D-4AD9-4C1C-AB7C-45BC2183EC51", /// "swapFreeMb": 1615.25, /// "hostOsFamily": "Darwin", /// "isVirtual": false, /// "uptimeSeconds": 1602055187, /// "hostModel": "MacBookPro14,2", /// "hostOsVersion": "10.15.7", /// "day": "2020-10-26T00:00:00Z", /// "cpuCount": 4, /// "swapTotalMb": 7168, /// "hostOs": "Mac OS X", /// "hostArchitecture": "x86_64", /// "memoryTotalMb": 16384, /// "timezone": "CET", /// "cpuModel": "Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz", /// "buildIdentifier": "MyMac_1FE14870-EDF1-4E8C-B1AA-2C2DF484842B_1", /// "memoryFreeMb": 24.5234375, /// "cpuSpeedGhz": 3.5 /// } /// ``` /// public func buildHost(req: Request) throws -> EventLoopFuture<BuildHost> { guard let buildId = req.parameters.get("id") else { throw Abort(.badRequest) } return BuildHost.query(on: req.db) .filter(\.$buildIdentifier == buildId) .first() .flatMapThrowing { buildHost -> BuildHost in guard let buildHost = buildHost else { throw Abort(.notFound) } return buildHost } } /// Endpoint that returns the metadata sent using XCMetrics plugins for the given `Build`. /// The metadata is returned as a JSON object where each data is a key-value pair of `String`s /// - Method: `GET` /// - Route: `/v1/build/metadata/<buildId>` /// - Path parameters /// - `buildId`. Mandatory. `Build`'s identifier /// /// - Response: /// /// ``` /// { /// "metadata": { /// "anotherKey": "42", /// "thirdKey": "Third value", /// "aKey": "value1" /// }, /// "id": "C1CDF2CE-0CC2-49C3-B8A2-481E67020CB8", /// "day": "2020-11-02T00:00:00Z", /// "buildIdentifier": "MyMac_0B9294B4-7E5A-4D40-91AB-5953A5075785_1" /// } /// ``` /// public func metadata(req: Request) throws -> EventLoopFuture<BuildMetadata> { guard let buildId = req.parameters.get("id") else { throw Abort(.badRequest) } return BuildMetadata.query(on: req.db) .filter(\.$buildIdentifier == buildId) .first() .flatMapThrowing { metadata -> BuildMetadata in guard let metadata = metadata else { throw Abort(.notFound) } return metadata } } /// Endpoint that returns the list of projects from which there are `Build` on the database. /// Useful if you want the list to filter the build per project using the endpoint /// `/v1/build/list` /// - Method: `GET` /// - Route: `/v1/build/project` /// - Response: /// /// ``` /// [ /// "Project1", /// "MyProject" /// ] /// ``` /// public func projects(req: Request) throws -> EventLoopFuture<[String]> { return Build.query(on: req.db) .unique() .sort(\.$projectName) .all(\.$projectName) } /// Endpoint that returns the list of `Step`s that were done to build a Target. /// /// - Method: `GET` /// - Route: `/v1/build/step/:day/:targetId` example: /// `/v1/build/step/20210129/ash22j3sdba1f0654c3f9e9a_6561690B-DFE4-4EE8-ABEE-99E4D3325E7B_15` /// - Path parameters /// - `day`. Mandatory. `Target`'s day as String in UTC. example: `20210129` /// - targetId. Mandatory. `Target`'s id. /// - Response: /// /// ``` /// [ /// { /// "id": "ash22j3sdba1f0654c3f9e9a_6561690B-DFE4-4EE8-ABEE-99E4D3325E7B_16", /// "startTimestamp": "2021-01-29T08:11:41Z", /// "endTimestamp": "2021-01-29T08:11:41Z", /// "errorCount": 0, /// "endTimestampMicroseconds": 1611907901.256928, /// "fetchedFromCache": false, /// "targetIdentifier": "ecba60d222f04c51dba1f0654c3f9e9a_6561690B-DFE8-4EE8-ABEE-99E4D3325E7B_15", /// "day": "2021-01-29T00:00:00Z", /// "type": "other", /// "title": "Create directory SPTAuthAccountsTests.xctest", /// "warningCount": 0, /// "signature": "MkDir \/Users\/<redacted>\/my_project\/build\/DerivedData\/Build\/Products\/Debug-iphonesimulator\/MyProjectTests.xctest", /// "architecture": "", /// "duration": 2.3, /// "documentURL": "", /// "buildIdentifier": "ash22j3sdba1f0654c3f9e9a_6561690B-DFE4-4EE8-ABEE-99E4D3325E7B_1", /// "startTimestampMicroseconds": 1611907901.255825 /// }, /// ... /// ] /// ``` /// public func targetSteps(req: Request) throws -> EventLoopFuture<[Step]> { guard let day = req.parameters.get("day"), let targetId = req.parameters.get("id"), Date.xcm_fromPartitionDay(day) != nil else { throw Abort(.badRequest) } guard let sql = req.db as? SQLDatabase else { throw Abort(.internalServerError) } return sql.raw(""" SELECT * FROM \(raw: Step.schema)_\(raw: day) WHERE target_identifier = \(literal: targetId) ORDER BY start_timestamp_microseconds, title """) .all(decoding: Step.self) } /// Endpoint that returns the list of `BuildMetadata`s that were added to a build /// filtered by a given key-value pair. /// - Method: `POST` /// - Route: `/v1/build/metadata/filter` /// - Request body /// /// ``` /// { /// "key": "aKey", /// "value": "value1" /// } /// ``` /// /// - Body Parameters /// - `key`. `BuildMetadata` metadata dictionary key /// - `value`. `BuildMetadata` metadata dictionary value /// /// - Response: /// /// ``` /// [ /// { /// "metadata": { /// "anotherKey": "42", /// "thirdKey": "Third value", /// "aKey": "value1" /// }, /// "id": "C1CDF2CE-0CC2-49C3-B8A2-481E67020CB8", /// "day": "2020-11-02T00:00:00Z", /// "buildIdentifier": "MyMac_0B9294B4-7E5A-4D40-91AB-5953A5075785_1" /// }, /// ... /// ] /// ``` /// public func metadataFilter(req: Request) throws -> EventLoopFuture<[BuildMetadata]> { let params = try req.content.decode(BuildMetadataFilterParams.self) guard let sql = req.db as? SQLDatabase else { throw Abort(.internalServerError) } return sql.raw(""" SELECT * FROM \(raw: BuildMetadata.schema) WHERE metadata ->> '\(raw: params.key)' = '\(raw: params.value)' """) .all(decoding: BuildMetadata.self) } } public struct BuildResponse: Content { let build: Build let targets: [Target] let xcode: XcodeVersion? init(build: Build, targets: [Target], xcode: XcodeVersion?) { self.build = build self.targets = targets self.xcode = xcode } }