diff --git a/Sources/VaporJsonApi/Json/JsonApiDocument.swift b/Sources/VaporJsonApi/Json/JsonApiDocument.swift index e9132c0..413f535 100644 --- a/Sources/VaporJsonApi/Json/JsonApiDocument.swift +++ b/Sources/VaporJsonApi/Json/JsonApiDocument.swift @@ -10,10 +10,22 @@ import Vapor public class JsonApiDocument: JSONRepresentable { + public fileprivate(set) var nilData: Bool = false public let data: JsonApiData? public let errors: [JsonApiErrorObject]? public let meta: JsonApiMeta? + /** + * Nil data initializer + * + */ + public init(meta: JsonApiMeta? = nil) { + self.nilData = true + self.data = nil + self.meta = meta + self.errors = nil + } + public init(data: JsonApiData, meta: JsonApiMeta? = nil) { self.data = data self.meta = meta @@ -39,6 +51,8 @@ public class JsonApiDocument: JSONRepresentable { jsonErrors.append(try error.makeJSON()) } json["errors"] = JSON(jsonErrors) + } else if nilData { + json["data"] = JSON(Node(nilLiteral: ())) } else { // This should really *never* happens because of the unambiguous initializers but this diff --git a/Sources/VaporJsonApi/Json/JsonApiResourceObject.swift b/Sources/VaporJsonApi/Json/JsonApiResourceObject.swift index e8baf78..96f2564 100644 --- a/Sources/VaporJsonApi/Json/JsonApiResourceObject.swift +++ b/Sources/VaporJsonApi/Json/JsonApiResourceObject.swift @@ -157,13 +157,13 @@ public class JsonApiRelationshipObject: JSONRepresentable { public class JsonApiLinksObject: JSONRepresentable { - public let selfLink: URI + public let selfLink: URI? public let selfMeta: JsonApiMeta? - public let relatedLink: URI + public let relatedLink: URI? public let relatedMeta: JsonApiMeta? - public init(selfLink: URI, selfMeta: JsonApiMeta? = nil, relatedLink: URI, relatedMeta: JsonApiMeta? = nil) { + public init(selfLink: URI? = nil, selfMeta: JsonApiMeta? = nil, relatedLink: URI? = nil, relatedMeta: JsonApiMeta? = nil) { self.selfLink = selfLink self.selfMeta = selfMeta @@ -172,30 +172,37 @@ public class JsonApiLinksObject: JSONRepresentable { } public func makeJSON() throws -> JSON { - let selfJson: JSON - if let selfMeta = selfMeta { - selfJson = try JSON(node: [ - "href": try selfLink.makeFoundationURL().absoluteString, - "meta": selfMeta.makeJSON() - ]) - } else { - selfJson = try JSON(Node(selfLink.makeFoundationURL().absoluteString)) + var json = JSON([:]) + + if let selfLink = selfLink { + let selfJson: JSON + if let selfMeta = selfMeta { + selfJson = try JSON(node: [ + "href": try selfLink.makeFoundationURL().absoluteString, + "meta": selfMeta.makeJSON() + ]) + } else { + selfJson = try JSON(Node(selfLink.makeFoundationURL().absoluteString)) + } + + json["self"] = selfJson } - let relatedJson: JSON - if let relatedMeta = relatedMeta { - relatedJson = try JSON(node: [ - "href": try relatedLink.makeFoundationURL().absoluteString, - "meta": relatedMeta.makeJSON() - ]) - } else { - relatedJson = try JSON(Node(relatedLink.makeFoundationURL().absoluteString)) + if let relatedLink = relatedLink { + let relatedJson: JSON + if let relatedMeta = relatedMeta { + relatedJson = try JSON(node: [ + "href": try relatedLink.makeFoundationURL().absoluteString, + "meta": relatedMeta.makeJSON() + ]) + } else { + relatedJson = try JSON(Node(relatedLink.makeFoundationURL().absoluteString)) + } + + json["related"] = relatedJson } - return try JSON(node: [ - "self": selfJson, - "related": relatedJson - ]) + return json } } diff --git a/Sources/VaporJsonApi/Resources/JsonApiChildrenModel.swift b/Sources/VaporJsonApi/Resources/JsonApiChildrenModel.swift index 51887ee..0cb2af9 100644 --- a/Sources/VaporJsonApi/Resources/JsonApiChildrenModel.swift +++ b/Sources/VaporJsonApi/Resources/JsonApiChildrenModel.swift @@ -20,12 +20,21 @@ public struct JsonApiChildrenModel { } public let getter: JsonApiChildrenGetter - public let adder: JsonApiChildrenAdder + public let adder: JsonApiChildrenAdder? public let replacer: JsonApiChildrenReplacer? - public init(getter: @escaping JsonApiChildrenGetter, adder: @escaping JsonApiChildrenAdder, replacer: JsonApiChildrenReplacer? = nil) { + public init(getter: @escaping JsonApiChildrenGetter, adder: JsonApiChildrenAdder? = nil, replacer: JsonApiChildrenReplacer? = nil) { self.getter = getter self.adder = adder self.replacer = replacer } + + public init(parent: JsonApiResourceModel, foreignKey: String?, adder: JsonApiChildrenAdder? = nil, replacer: JsonApiChildrenReplacer? = nil) { + getter = { + return parent.children(foreignKey, T.self) + } + + self.adder = adder + self.replacer = replacer + } } diff --git a/Sources/VaporJsonApi/Resources/JsonApiParentModel.swift b/Sources/VaporJsonApi/Resources/JsonApiParentModel.swift index 238782a..b9eb8dd 100644 --- a/Sources/VaporJsonApi/Resources/JsonApiParentModel.swift +++ b/Sources/VaporJsonApi/Resources/JsonApiParentModel.swift @@ -19,22 +19,18 @@ public struct JsonApiParentModel { } public let getter: JsonApiParentGetter - public let setter: JsonApiParentSetter + public let setter: JsonApiParentSetter? - public init(getter: @escaping JsonApiParentGetter, setter: @escaping JsonApiParentSetter) { + public init(getter: @escaping JsonApiParentGetter, setter: JsonApiParentSetter? = nil) { self.getter = getter self.setter = setter } - public init(child: C, parentId: Node, foreignKey: String?) { + public init(child: JsonApiResourceModel, parentId: Node, foreignKey: String? = nil, setter: JsonApiParentSetter? = nil) { getter = { return try child.parent(parentId, foreignKey, T.self) } - setter = { parent in - let p = Parent(child: child, parentId: parentId, foreignKey: foreignKey) - var par = try p.get() - try par?.save() - } + self.setter = setter } } diff --git a/Sources/VaporJsonApi/Resources/JsonApiResourceController.swift b/Sources/VaporJsonApi/Resources/JsonApiResourceController.swift index 0b389dd..2faa64e 100644 --- a/Sources/VaporJsonApi/Resources/JsonApiResourceController.swift +++ b/Sources/VaporJsonApi/Resources/JsonApiResourceController.swift @@ -9,6 +9,7 @@ import Vapor import HTTP import Fluent +import URI public protocol JsonApiResourceController { associatedtype Resource: JsonApiResourceModel @@ -33,25 +34,14 @@ public extension JsonApiResourceController { let query = req.jsonApiQuery() - let pageCount = query["page"]?["size"]?.string?.int ?? JsonApiConfig.defaultPageSize - if pageCount > JsonApiConfig.maximumPageSize { - throw JsonApiInvalidPageValueError(page: "page[size]", value: query["page"]?["size"]?.string ?? "*Nothing*") - } - let pageNumber = query["page"]?["number"]?.string?.int ?? 1 - if pageNumber < 1 { - throw JsonApiInvalidPageValueError(page: "page[number]", value: query["page"]?["number"]?.string ?? "*Nothing*") - } - let resources = try Resource.query().limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all() - - var resourceObjects = [JsonApiResourceObject]() - for r in resources { - resourceObjects.append(try r.makeResourceObject(resourceModel: r, baseUrl: req.uri)) - } + let page = try pageForQuery(query: query) + let pageNumber = page.pageNumber + let pageCount = page.pageCount - let data = JsonApiData(resourceObjects: resourceObjects) - let document = JsonApiDocument(data: data) + let resources = try Resource.query().limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all() + let jsonDocument = try document(forResources: resources, baseUrl: req.uri) - return JsonApiResponse(status: .ok, document: document) + return JsonApiResponse(status: .ok, document: jsonDocument) } /** @@ -82,6 +72,65 @@ public extension JsonApiResourceController { return JsonApiResponse(status: .ok, document: document) } + /** + * The `getRelatedResource` method is responsible for get requests to a relationship of a specific resource. + * + * Example: `/articles/5/author` for the author resource of the article with id `5`. + * + * - parameter req: The `Request` which fired this method. + * - parameter id: The id represented as a String which is the first route parameter for this request. + * - parameter relationshipType: The relationshipType represented as a String which is the relationship name as defined in the JsonApiResourceModel. + */ + func getRelatedResource(_ req: Request, _ id: String, _ relationshipType: String) throws -> ResponseRepresentable { + + guard req.fulfillsJsonApiAcceptResponsibilities() else { + throw JsonApiNotAcceptableError(mediaType: req.acceptHeaderValue() ?? "*No Accept header*") + } + + guard let resource = try Resource.find(id) else { + throw JsonApiRecordNotFoundError(id: id) + } + + let query = req.jsonApiQuery() + + if let parentModel = try resource.parentRelationships()[relationshipType] { + if let parent = try parentModel.getter().get() { + let resourceObject = try parent.makeResourceObject(resourceModel: parent, baseUrl: req.uri) + let data = JsonApiData(resourceObject: resourceObject) + let document = JsonApiDocument(data: data) + + return JsonApiResponse(status: .ok, document: document) + } else { + let document = JsonApiDocument() + return JsonApiResponse(status: .ok, document: document) + } + } else if let childrenCollection = try resource.childrenRelationships()[relationshipType] { + let children = try childrenCollection.getter() + + let page = try pageForQuery(query: query) + let pageNumber = page.pageNumber + let pageCount = page.pageCount + + let resources = try children.limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all() + let jsonDocument = try document(forResources: resources, baseUrl: req.uri) + + return JsonApiResponse(status: .ok, document: jsonDocument) + } else if let siblingsCollection = try resource.siblingsRelationships()[relationshipType] { + let siblings = try siblingsCollection.getter() + + let page = try pageForQuery(query: query) + let pageNumber = page.pageNumber + let pageCount = page.pageCount + + let resources = try siblings.limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all() + let jsonDocument = try document(forResources: resources, baseUrl: req.uri) + + return JsonApiResponse(status: .ok, document: jsonDocument) + } + + throw JsonApiRelationshipNotFoundError(relationship: relationshipType) + } + /** * The `postResource` method is responsible for post requests to a specific resource. * @@ -120,3 +169,29 @@ public extension JsonApiResourceController { return JsonApiResponse(status: .created, document: document) } } + +fileprivate extension JsonApiResourceController { + + fileprivate func pageForQuery(query: JSON) throws -> (pageCount: Int, pageNumber: Int) { + let pageCount = query["page"]?["size"]?.string?.int ?? JsonApiConfig.defaultPageSize + if pageCount > JsonApiConfig.maximumPageSize { + throw JsonApiInvalidPageValueError(page: "page[size]", value: query["page"]?["size"]?.string ?? "*Nothing*") + } + let pageNumber = query["page"]?["number"]?.string?.int ?? 1 + if pageNumber < 1 { + throw JsonApiInvalidPageValueError(page: "page[number]", value: query["page"]?["number"]?.string ?? "*Nothing*") + } + + return (pageCount: pageCount, pageNumber: pageNumber) + } + + fileprivate func document(forResources resources: [JsonApiResourceModel], baseUrl: URI) throws -> JsonApiDocument { + var resourceObjects = [JsonApiResourceObject]() + for r in resources { + resourceObjects.append(try r.makeResourceObject(resourceModel: r, baseUrl: baseUrl)) + } + + let data = JsonApiData(resourceObjects: resourceObjects) + return JsonApiDocument(data: data) + } +} diff --git a/Sources/VaporJsonApi/Resources/JsonApiResourceRepresentable.swift b/Sources/VaporJsonApi/Resources/JsonApiResourceRepresentable.swift index acc8351..bfb15cd 100644 --- a/Sources/VaporJsonApi/Resources/JsonApiResourceRepresentable.swift +++ b/Sources/VaporJsonApi/Resources/JsonApiResourceRepresentable.swift @@ -18,7 +18,7 @@ public protocol JsonApiResourceRepresentable { typealias JsonApiChildrenRelationships = [String: JsonApiChildrenModel] - typealias JsonApiSiblingsRelationships = [String: (type: JsonApiResourceModel.Type, getter: () throws -> Siblings)] + typealias JsonApiSiblingsRelationships = [String: JsonApiSiblingsModel] static var resourceType: JsonApiResourceType { get } @@ -100,8 +100,11 @@ public extension JsonApiResourceRepresentable { relationshipObjects.append(try makeSiblingsRelationshipObject(name: s.key, type: s.value.type, getter: s.value.getter, baseUrl: baseUrl, resourcePath: resourcePath)) } + let selfLink = URI(scheme: baseUrl.scheme, host: baseUrl.host, port: baseUrl.port, path: "\(resourcePath)") + let links = JsonApiLinksObject(selfLink: selfLink) + let relationshipsObject = JsonApiRelationshipsObject(relationshipObjects: relationshipObjects) - return JsonApiResourceObject(id: id, type: type, attributes: attributesObject, relationships: relationshipsObject) + return JsonApiResourceObject(id: id, type: type, attributes: attributesObject, relationships: relationshipsObject, links: links) } public func makeResourceIdentifierObject(resourceModel: JsonApiResourceModel, meta: JsonApiMeta?) throws -> JsonApiResourceIdentifierObject { diff --git a/Sources/VaporJsonApi/Resources/JsonApiSiblingsModel.swift b/Sources/VaporJsonApi/Resources/JsonApiSiblingsModel.swift index eac0bc6..5ffee0f 100644 --- a/Sources/VaporJsonApi/Resources/JsonApiSiblingsModel.swift +++ b/Sources/VaporJsonApi/Resources/JsonApiSiblingsModel.swift @@ -20,12 +20,34 @@ public struct JsonApiSiblingsModel { } public let getter: JsonApiSiblingsGetter - public let adder: JsonApiSiblingsAdder + public let adder: JsonApiSiblingsAdder? public let replacer: JsonApiSiblingsReplacer? - public init(getter: @escaping JsonApiSiblingsGetter, adder: @escaping JsonApiSiblingsAdder, replacer: JsonApiSiblingsReplacer? = nil) { + public init(getter: @escaping JsonApiSiblingsGetter, adder: JsonApiSiblingsAdder? = nil, replacer: JsonApiSiblingsReplacer? = nil) { self.getter = getter self.adder = adder self.replacer = replacer } + + public init(toSibling: S, localKey: String? = nil, foreignKey: String? = nil) { + getter = { + return try toSibling.siblings(localKey, foreignKey) + } + + // TODO: Don't allow duplicate linkage + self.adder = { siblings in + for s in siblings { + var p = Pivot(toSibling, s) + try p.save() + } + } + self.replacer = { siblings in + let oldSiblings: Siblings = try toSibling.siblings(localKey, foreignKey) + try oldSiblings.delete() + for s in siblings { + var p = Pivot(toSibling, s) + try p.save() + } + } + } } diff --git a/Sources/VaporJsonApi/Response/JsonApiError.swift b/Sources/VaporJsonApi/Response/JsonApiError.swift index 7ae6397..34b17bd 100644 --- a/Sources/VaporJsonApi/Response/JsonApiError.swift +++ b/Sources/VaporJsonApi/Response/JsonApiError.swift @@ -65,6 +65,17 @@ public class JsonApiRecordNotFoundError: JsonApiGeneralError { } } +public class JsonApiRelationshipNotFoundError: JsonApiGeneralError { + + public init(title: String, detail: String) { + super.init(status: Status.notFound, code: String(Status.notFound.statusCode), title: title, detail: detail) + } + + public convenience init(relationship: String) { + self.init(title: "Relationship not found", detail: "The relationship identified by \(relationship) could not be found.") + } +} + public class JsonApiUnsupportedMediaTypeError: JsonApiGeneralError { public init(title: String, detail: String) {