Skip to content

Commit a4a4cd1

Browse files
Merge pull request #3494 from SwiftPackageIndex/custom-collections-tweaks
Custom collections tweaks
2 parents 082e486 + bb6fdac commit a4a4cd1

19 files changed

+214
-73
lines changed

Sources/App/Controllers/CustomCollectionsController.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Vapor
1919

2020
enum CustomCollectionsController {
2121

22-
static func query(on database: Database, name: String, page: Int, pageSize: Int) async throws -> Page<Joined3<Package, Repository, Version>> {
22+
static func query(on database: Database, key: String, page: Int, pageSize: Int) async throws -> Page<Joined3<Package, Repository, Version>> {
2323
try await Joined3<Package, Repository, Version>
2424
.query(on: database, version: .defaultBranch)
2525
.join(CustomCollectionPackage.self, on: \Package.$id == \CustomCollectionPackage.$package.$id)
@@ -30,7 +30,7 @@ enum CustomCollectionsController {
3030
.field(Repository.self, \.$stars)
3131
.field(Repository.self, \.$summary)
3232
.field(Version.self, \.$packageName)
33-
.filter(CustomCollection.self, \.$name == name)
33+
.filter(CustomCollection.self, \.$key == key)
3434
.sort(Repository.self, \.$name)
3535
.page(page, size: pageSize)
3636
}
@@ -56,11 +56,13 @@ enum CustomCollectionsController {
5656

5757
@Sendable
5858
static func show(req: Request) async throws -> HTML {
59-
guard let name = req.parameters.get("name") else {
59+
guard let key = req.parameters.get("key") else {
6060
throw Abort(.notFound)
6161
}
6262
let query = try req.query.decode(Query.self)
63-
let page = try await Self.query(on: req.db, name: name, page: query.page, pageSize: query.pageSize)
63+
let collection = try await CustomCollection.find(on: req.db, key: key)
64+
.unwrap(or: Abort(.notFound))
65+
let page = try await Self.query(on: req.db, key: key, page: query.page, pageSize: query.pageSize)
6466

6567
guard !page.results.isEmpty else {
6668
throw Abort(.notFound)
@@ -69,7 +71,8 @@ enum CustomCollectionsController {
6971
let packageInfo = page.results.compactMap(PackageInfo.init(package:))
7072

7173
let model = CustomCollectionShow.Model(
72-
name: name,
74+
key: collection.key,
75+
name: collection.name,
7376
packages: packageInfo,
7477
page: query.page,
7578
hasMoreResults: page.hasMoreResults

Sources/App/Controllers/PackageCollectionController.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ enum PackageCollectionController {
3232
filterBy: .author(owner),
3333
authorName: "\(owner) via the Swift Package Index"
3434
)
35-
case let .custom(name):
35+
case let .custom(key):
36+
let collection = try await CustomCollection.find(on: req.db, key: key)
37+
.unwrap(or: Abort(.notFound))
3638
return try await SignedCollection.generate(
3739
db: req.db,
38-
filterBy: .customCollection(name),
40+
filterBy: .customCollection(key),
3941
authorName: "Swift Package Index",
40-
collectionName: name,
42+
collectionName: collection.name,
4143
overview: "A custom package collection generated by the Swift Package Index"
4244
)
4345
}
@@ -53,7 +55,7 @@ enum PackageCollectionController {
5355

5456
static func getCollectionType(req: Request) -> CollectionType? {
5557
if let owner = req.parameters.get("owner") { return .author(owner) }
56-
if let name = req.parameters.get("name") { return .custom(name) }
58+
if let key = req.parameters.get("key") { return .custom(key) }
5759
return nil
5860
}
5961
}

Sources/App/Core/PackageCollection+VersionResult.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ extension PackageCollection.VersionResult {
5555
switch filter {
5656
case let .author(owner):
5757
query.filter(Repository.self, \Repository.$owner, .custom("ilike"), owner)
58-
case let .customCollection(name):
58+
case let .customCollection(key):
5959
query
6060
.join(CustomCollectionPackage.self, on: \Package.$id == \CustomCollectionPackage.$package.$id)
6161
.join(CustomCollection.self, on: \CustomCollection.$id == \CustomCollectionPackage.$customCollection.$id)
62-
.filter(CustomCollection.self, \.$name == name)
62+
.filter(CustomCollection.self, \.$key == key)
6363
case let .urls(packageURLs):
6464
query.filter(App.Package.self, \.$url ~~ packageURLs)
6565
}

Sources/App/Core/SiteURL.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ enum SiteURL: Resourceable, Sendable {
113113
case blogPost(_ slug: Parameter<String>)
114114
case buildMonitor
115115
case builds(_ id: Parameter<UUID>)
116-
case collections(_ name: Parameter<String>)
116+
case collections(_ key: Parameter<String>)
117117
case docs(Docs)
118118
case faq
119119
case home
@@ -122,7 +122,7 @@ enum SiteURL: Resourceable, Sendable {
122122
case keywords(_ keyword: Parameter<String>)
123123
case package(_ owner: Parameter<String>, _ repository: Parameter<String>, PackagePathComponents?)
124124
case packageCollectionAuthor(_ owner: Parameter<String>)
125-
case packageCollectionCustom(_ name: Parameter<String>)
125+
case packageCollectionCustom(_ key: Parameter<String>)
126126
case packageCollections
127127
case privacy
128128
case readyForSwift6
@@ -171,11 +171,11 @@ enum SiteURL: Resourceable, Sendable {
171171
case .buildMonitor:
172172
return "build-monitor"
173173

174-
case let .collections(.value(name)):
175-
return "collections/\(name.urlPathEncoded)"
174+
case let .collections(.value(key)):
175+
return "collections/\(key.urlPathEncoded)"
176176

177177
case .collections(.key):
178-
fatalError("path must not be called with a name parameter")
178+
fatalError("invalid path: \(self)")
179179

180180
case let .docs(next):
181181
return "docs/\(next.path)"
@@ -215,8 +215,8 @@ enum SiteURL: Resourceable, Sendable {
215215
case .packageCollectionAuthor(.key):
216216
fatalError("invalid path: \(self)")
217217

218-
case let .packageCollectionCustom(.value(name)):
219-
return "collections/\(name.urlPathEncoded)/collection.json"
218+
case let .packageCollectionCustom(.value(key)):
219+
return "collections/\(key.urlPathEncoded)/collection.json"
220220

221221
case .packageCollectionCustom(.key):
222222
fatalError("invalid path: \(self)")
@@ -298,7 +298,7 @@ enum SiteURL: Resourceable, Sendable {
298298
fatalError("pathComponents must not be called with a value parameter")
299299

300300
case .collections(.key):
301-
return ["collections", ":name"]
301+
return ["collections", ":key"]
302302

303303
case .collections(.value):
304304
fatalError("pathComponents must not be called with a value parameter")
@@ -325,7 +325,7 @@ enum SiteURL: Resourceable, Sendable {
325325
fatalError("pathComponents must not be called with a value parameter")
326326

327327
case .packageCollectionCustom(.key):
328-
return ["collections", ":name", "collection.json"]
328+
return ["collections", ":key", "collection.json"]
329329

330330
case .packageCollectionCustom(.value):
331331
fatalError("pathComponents must not be called with a value parameter")
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Fluent
16+
17+
struct UpdateCustomCollectionAddKey: AsyncMigration {
18+
func prepare(on database: Database) async throws {
19+
// We need to clear the custom_collections table, because the existing rows don't have
20+
// `key` values and their existence prevents the update.
21+
// They will be automatically regenerated by the next reconciliation run.
22+
try await CustomCollection.query(on: database).delete()
23+
try await database.schema("custom_collections")
24+
.field("key", .string, .required)
25+
.unique(on: "key")
26+
.update()
27+
}
28+
29+
func revert(on database: Database) async throws {
30+
try await database.schema("custom_collections")
31+
.deleteField("key")
32+
// The unique key constraint is automatically deleted when the field is deleted,
33+
// no explicit clean up is required.
34+
.update()
35+
}
36+
}

Sources/App/Models/CustomCollection.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ final class CustomCollection: @unchecked Sendable, Model, Content {
3535

3636
// data fields
3737

38+
@Field(key: "key")
39+
var key: String
40+
3841
@Field(key: "name")
3942
var name: String
4043

@@ -58,6 +61,7 @@ final class CustomCollection: @unchecked Sendable, Model, Content {
5861
self.id = id
5962
self.createdAt = createdAt
6063
self.updatedAt = updatedAt
64+
self.key = details.key
6165
self.name = details.name
6266
self.description = details.description
6367
self.badge = details.badge
@@ -68,16 +72,26 @@ final class CustomCollection: @unchecked Sendable, Model, Content {
6872

6973
extension CustomCollection {
7074
struct Details: Codable, Equatable {
75+
var key: String
7176
var name: String
7277
var description: String?
7378
var badge: String?
7479
var url: URL
7580
}
7681

82+
static func find(on database: Database, key: String) async throws -> CustomCollection? {
83+
try await CustomCollection.query(on: database)
84+
.filter(\.$key == key)
85+
.first()
86+
}
87+
7788
static func findOrCreate(on database: Database, _ details: Details) async throws -> CustomCollection {
78-
if let collection = try await CustomCollection.query(on: database)
79-
.filter(\.$url == details.url)
80-
.first() {
89+
if let collection = try await CustomCollection.find(on: database, key: details.key) {
90+
if collection.details != details {
91+
// Update the collection if any of the details have changed
92+
collection.details = details
93+
try await collection.update(on: database)
94+
}
8195
return collection
8296
} else {
8397
let collection = CustomCollection(details)
@@ -101,7 +115,20 @@ extension CustomCollection {
101115
}
102116

103117
var details: Details {
104-
.init(name: name, description: description, badge: badge, url: url)
118+
get {
119+
.init(key: key,
120+
name: name,
121+
description: description,
122+
badge: badge,
123+
url: url)
124+
}
125+
set {
126+
key = newValue.key
127+
name = newValue.name
128+
description = newValue.description
129+
badge = newValue.badge
130+
url = newValue.url
131+
}
105132
}
106133
}
107134

Sources/App/Views/CustomCollection/CustomCollectionShow+Model.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414

1515
extension CustomCollectionShow {
1616
struct Model {
17+
var key: String
1718
var name: String
1819
var packages: [PackageInfo]
1920
var page: Int
2021
var hasMoreResults: Bool
2122

22-
internal init(name: String,
23+
internal init(key: String,
24+
name: String,
2325
packages: [PackageInfo],
2426
page: Int,
2527
hasMoreResults: Bool) {
28+
self.key = key
2629
self.name = name
2730
self.packages = packages
2831
self.page = page

Sources/App/Views/CustomCollection/CustomCollectionShow+View.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ enum CustomCollectionShow {
5858
),
5959
.copyableInputForm(buttonName: "Copy Package Collection URL",
6060
eventName: "Copy Package Collection URL Button",
61-
valueToCopy: SiteURL.packageCollectionCustom(.value(model.name)).absoluteURL()),
61+
valueToCopy: SiteURL.packageCollectionCustom(.value(model.key)).absoluteURL()),
6262
.hr(.class("minor")),
6363
.ul(
6464
.id("package-list"),

Sources/App/Views/PackageController/GetRoute.Model+ext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ extension API.PackageController.GetRoute.Model {
404404
.class("custom-collections"),
405405
.forEach(customCollections, { collection in
406406
.a(
407-
.href(SiteURL.collections(.value(collection.name)).relativeURL()),
407+
.href(SiteURL.collections(.value(collection.key)).relativeURL()),
408408
.text("\(collection.name)")
409409
)
410410
})

Sources/App/configure.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ public func configure(_ app: Application) async throws -> String {
347347
do { // Migration 082 - Add `has_spi_badge` to `repositories`
348348
app.migrations.add(UpdateRepositoryAddHasSPIBadge())
349349
}
350+
do { // Migration 083 - Add `key` and unique constraint to `custom_collections`
351+
app.migrations.add(UpdateCustomCollectionAddKey())
352+
}
350353

351354
app.asyncCommands.use(Analyze.Command(), as: "analyze")
352355
app.asyncCommands.use(CreateRestfileCommand(), as: "create-restfile")

0 commit comments

Comments
 (0)