├── .DS_Store
├── .gitignore
├── Backend
├── .DS_Store
└── TodoAPI
│ ├── .DS_Store
│ ├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── contents.xcworkspacedata
│ ├── Dockerfile
│ ├── Package.resolved
│ ├── Package.swift
│ ├── README.md
│ ├── Sources
│ └── TodoAPI
│ │ ├── Extensions
│ │ └── APIGateway+Extension.swift
│ │ ├── Model
│ │ ├── APIError.swift
│ │ ├── Handler.swift
│ │ ├── Todo+DynamoDB.swift
│ │ └── Todo.swift
│ │ ├── Services
│ │ └── TodoService.swift
│ │ ├── TodoLambdaHandler.swift
│ │ ├── Utils.swift
│ │ └── main.swift
│ ├── Tests
│ ├── LinuxMain.swift
│ └── TodoAPITests
│ │ ├── TodoAPITests.swift
│ │ └── XCTestManifests.swift
│ ├── scripts
│ └── package.sh
│ └── serverless.yml
├── Client
└── Todo
│ ├── .DS_Store
│ ├── Todo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── Todo
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Bindings
│ ├── TodoFormViewModel.swift
│ └── TodoListViewModel.swift
│ ├── ContentView.swift
│ ├── Info.plist
│ ├── Model
│ ├── Todo.swift
│ └── TodoError.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── Services
│ └── TodoProvider.swift
│ ├── Todo.xcdatamodeld
│ └── Todo.xcdatamodel
│ │ └── contents
│ ├── TodoApp.swift
│ ├── TodoFormView.swift
│ └── Utils.swift
├── README.md
└── promo.jpeg
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .serverless
2 | node_modules
3 |
4 | # Xcode
5 | #
6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
7 |
8 | ## User settings
9 | xcuserdata/
10 |
11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
12 | *.xcscmblueprint
13 | *.xccheckout
14 |
15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
16 | build/
17 | DerivedData/
18 | *.moved-aside
19 | *.pbxuser
20 | !default.pbxuser
21 | *.mode1v3
22 | !default.mode1v3
23 | *.mode2v3
24 | !default.mode2v3
25 | *.perspectivev3
26 | !default.perspectivev3
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 |
31 | ## App packaging
32 | *.ipa
33 | *.dSYM.zip
34 | *.dSYM
35 |
36 | ## Playgrounds
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 | # Swift Package Manager
41 | #
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | # Packages/
44 | # Package.pins
45 | # Package.resolved
46 | # *.xcodeproj
47 | #
48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
49 | # hence it is not needed unless you have added a package configuration file to your project
50 | # .swiftpm
51 |
52 | .build/
53 |
54 | # CocoaPods
55 | #
56 | # We recommend against adding the Pods directory to your .gitignore. However
57 | # you should judge for yourself, the pros and cons are mentioned at:
58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
59 | #
60 | # Pods/
61 | #
62 | # Add this line if you want to avoid checking in source code from the Xcode workspace
63 | # *.xcworkspace
64 |
65 | # Carthage
66 | #
67 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
68 | # Carthage/Checkouts
69 |
70 | Carthage/Build/
71 |
72 | # Accio dependency management
73 | Dependencies/
74 | .accio/
75 |
76 | # fastlane
77 | #
78 | # It is recommended to not store the screenshots in the git repo.
79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
80 | # For more information about the recommended setup visit:
81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
82 |
83 | fastlane/report.xml
84 | fastlane/Preview.html
85 | fastlane/screenshots/**/*.png
86 | fastlane/test_output
87 |
88 | # Code Injection
89 | #
90 | # After new code Injection tools there's a generated folder /iOSInjectionProject
91 | # https://github.com/johnno1962/injectionforxcode
92 |
93 | iOSInjectionProject/
--------------------------------------------------------------------------------
/Backend/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/Backend/.DS_Store
--------------------------------------------------------------------------------
/Backend/TodoAPI/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/Backend/TodoAPI/.DS_Store
--------------------------------------------------------------------------------
/Backend/TodoAPI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM swiftlang/swift:nightly-amazonlinux2
2 |
3 | RUN yum -y install \
4 | git \
5 | libuuid-devel \
6 | libicu-devel \
7 | libedit-devel \
8 | libxml2-devel \
9 | sqlite-devel \
10 | python-devel \
11 | ncurses-devel \
12 | curl-devel \
13 | openssl-devel \
14 | tzdata \
15 | libtool \
16 | jq \
17 | tar \
18 | zip
19 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "async-http-client",
6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "037b70291941fe43de668066eb6fb802c5e181d2",
10 | "version": "1.1.1"
11 | }
12 | },
13 | {
14 | "package": "aws-sdk-swift",
15 | "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "0f5a40c1331f497dd5cc29c873d9f0ee4ff0db60",
19 | "version": "5.0.0-alpha.4"
20 | }
21 | },
22 | {
23 | "package": "aws-sdk-swift-core",
24 | "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift-core.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "e0f70315488c23ea64ab63ef7b8530ce01783024",
28 | "version": "5.0.0-alpha.4"
29 | }
30 | },
31 | {
32 | "package": "swift-aws-lambda-runtime",
33 | "repositoryURL": "https://github.com/swift-server/swift-aws-lambda-runtime.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "2bac89639fffd7b1197ab597473a4d10c459a230",
37 | "version": "0.2.0"
38 | }
39 | },
40 | {
41 | "package": "swift-backtrace",
42 | "repositoryURL": "https://github.com/swift-server/swift-backtrace.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "f2fd8c4845a123419c348e0bc4b3839c414077d5",
46 | "version": "1.2.0"
47 | }
48 | },
49 | {
50 | "package": "swift-log",
51 | "repositoryURL": "https://github.com/apple/swift-log.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
55 | "version": "1.2.0"
56 | }
57 | },
58 | {
59 | "package": "swift-metrics",
60 | "repositoryURL": "https://github.com/apple/swift-metrics.git",
61 | "state": {
62 | "branch": null,
63 | "revision": "708b960b4605abb20bc55d65abf6bad607252200",
64 | "version": "2.0.0"
65 | }
66 | },
67 | {
68 | "package": "swift-nio",
69 | "repositoryURL": "https://github.com/apple/swift-nio.git",
70 | "state": {
71 | "branch": null,
72 | "revision": "120acb15c39aa3217e9888e515de160378fbcc1e",
73 | "version": "2.18.0"
74 | }
75 | },
76 | {
77 | "package": "swift-nio-extras",
78 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
79 | "state": {
80 | "branch": null,
81 | "revision": "7cd24c0efcf9700033f671b6a8eaa64a77dd0b72",
82 | "version": "1.5.1"
83 | }
84 | },
85 | {
86 | "package": "swift-nio-ssl",
87 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
88 | "state": {
89 | "branch": null,
90 | "revision": "f0b118d9af6c4e78bc4f3f4fbb464020172b0bf4",
91 | "version": "2.7.5"
92 | }
93 | },
94 | {
95 | "package": "swift-nio-transport-services",
96 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git",
97 | "state": {
98 | "branch": null,
99 | "revision": "2ac8fde712c1b1a147ecb7065824a40d2c09d0cb",
100 | "version": "1.6.0"
101 | }
102 | }
103 | ]
104 | },
105 | "version": 1
106 | }
107 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "TodoAPI",
7 | platforms: [
8 | .macOS(.v10_14)
9 | ],
10 | dependencies: [
11 | .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.2.0"),
12 | .package(url: "https://github.com/swift-aws/aws-sdk-swift.git", from: "5.0.0-alpha.4")
13 | ],
14 | targets: [
15 | .target(
16 | name: "TodoAPI",
17 | dependencies: [
18 | .product(name: "AWSDynamoDB", package: "aws-sdk-swift"),
19 | .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
20 | .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime")
21 | ]),
22 | .testTarget(
23 | name: "TodoAPITests",
24 | dependencies: ["TodoAPI"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/README.md:
--------------------------------------------------------------------------------
1 | # TodoAPI
2 |
3 | A description of this package.
4 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Extensions/APIGateway+Extension.swift:
--------------------------------------------------------------------------------
1 | // Created by Alfian Losari on 02/07/20.
2 | //
3 |
4 | import Foundation
5 | import AWSLambdaEvents
6 |
7 | extension APIGateway.Request {
8 |
9 | private static let jsonDecoder: JSONDecoder = {
10 | let decoder = JSONDecoder()
11 | decoder.dateDecodingStrategy = .formatted(Utils.iso8601Formatter)
12 | return decoder
13 | }()
14 |
15 | func bodyObject() throws -> D {
16 | guard let jsonData = body?.data(using: .utf8) else {
17 | throw APIError.requestError
18 | }
19 | let object = try APIGateway.Request.jsonDecoder.decode(D.self, from: jsonData)
20 | return object
21 | }
22 | }
23 |
24 | extension APIGateway.Response {
25 |
26 | private static let jsonEncoder: JSONEncoder = {
27 | let encoder = JSONEncoder()
28 | encoder.dateEncodingStrategy = .formatted(Utils.iso8601Formatter)
29 | return encoder
30 | }()
31 |
32 | public static let defaultHeaders = [
33 | "Content-Type": "application/json",
34 | "Access-Control-Allow-Origin": "*",
35 | "Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE",
36 | "Access-Control-Allow-Credentials": "true",
37 | ]
38 |
39 | public init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
40 | self.init(
41 | statusCode: statusCode,
42 | headers: APIGateway.Response.defaultHeaders,
43 | multiValueHeaders: nil,
44 | body: "{\"error\":\"\(String(describing: error))\"}",
45 | isBase64Encoded: false
46 | )
47 | }
48 |
49 | public init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
50 | var body: String = "{}"
51 | if let data = try? Self.jsonEncoder.encode(object) {
52 | body = String(data: data, encoding: .utf8) ?? body
53 | }
54 | self.init(
55 | statusCode: statusCode,
56 | headers: APIGateway.Response.defaultHeaders,
57 | multiValueHeaders: nil,
58 | body: body,
59 | isBase64Encoded: false
60 | )
61 | }
62 | }
63 |
64 | struct EmptyResponse: Encodable {}
65 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Model/APIError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 30/06/20.
6 | //
7 |
8 | import Foundation
9 |
10 | enum APIError: Error {
11 | case decodingError
12 | case requestError
13 | case todoNotFound
14 | }
15 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Model/Handler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import AWSLambdaRuntime
9 |
10 | enum Handler: String {
11 |
12 | case create
13 | case update
14 | case delete
15 | case read
16 | case list
17 |
18 | static var current: Handler? {
19 | guard let handler = Lambda.env("_HANDLER") else {
20 | return nil
21 | }
22 | return Handler(rawValue: handler)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Model/Todo+DynamoDB.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 29/06/20.
6 | //
7 |
8 | import AWSDynamoDB
9 | import Foundation
10 |
11 | extension Todo {
12 |
13 | public var dynamoDbDictionary: [String: DynamoDB.AttributeValue] {
14 | var dictionary = [
15 | DynamoDBField.id: DynamoDB.AttributeValue(s: id),
16 | DynamoDBField.name: DynamoDB.AttributeValue(s: name),
17 | DynamoDBField.isCompleted: DynamoDB.AttributeValue(bool: isCompleted)
18 | ]
19 |
20 | if let dueDate = dueDate {
21 | dictionary[DynamoDBField.dueDate] = DynamoDB.AttributeValue(s: Utils.iso8601Formatter.string(from: dueDate))
22 | }
23 |
24 | if let createdAt = createdAt {
25 | dictionary[DynamoDBField.createdAt] = DynamoDB.AttributeValue(s: Utils.iso8601Formatter.string(from: createdAt))
26 | }
27 |
28 | if let updatedAt = updatedAt {
29 | dictionary[DynamoDBField.updatedAt] = DynamoDB.AttributeValue(s: Utils.iso8601Formatter.string(from: updatedAt))
30 | }
31 |
32 | return dictionary
33 | }
34 |
35 | public init(dictionary: [String: DynamoDB.AttributeValue]) throws {
36 | guard let id = dictionary[DynamoDBField.id]?.s,
37 | let name = dictionary[DynamoDBField.name]?.s,
38 | let isCompleted = dictionary[DynamoDBField.isCompleted]?.bool,
39 | let dueDateValue = dictionary[DynamoDBField.dueDate]?.s,
40 | let dueDate = Utils.iso8601Formatter.date(from: dueDateValue),
41 | let createdAtValue = dictionary[DynamoDBField.createdAt]?.s,
42 | let createdAt = Utils.iso8601Formatter.date(from: createdAtValue),
43 | let updatedAtValue = dictionary[DynamoDBField.updatedAt]?.s,
44 | let updatedAt = Utils.iso8601Formatter.date(from: updatedAtValue) else {
45 | throw APIError.decodingError
46 | }
47 |
48 | self.id = id
49 | self.name = name
50 | self.isCompleted = isCompleted
51 | self.dueDate = dueDate
52 | self.createdAt = createdAt
53 | self.updatedAt = updatedAt
54 | }
55 |
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Model/Todo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 29/06/20.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct Todo: Codable {
11 | public let id: String
12 | public let name: String
13 | public let isCompleted: Bool
14 | public var dueDate: Date?
15 | public var createdAt: Date?
16 | public var updatedAt: Date?
17 |
18 | public struct DynamoDBField {
19 | static let id = "id"
20 | static let name = "name"
21 | static let isCompleted = "isCompleted"
22 | static let dueDate = "dueDate"
23 | static let createdAt = "createdAt"
24 | static let updatedAt = "updatedAt"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Services/TodoService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 30/06/20.
6 | //
7 |
8 | import AWSDynamoDB
9 | import Foundation
10 |
11 | public class TodoService {
12 |
13 | let db: DynamoDB
14 | let tableName: String
15 |
16 | public init(db: DynamoDB, tableName: String) {
17 | self.db = db
18 | self.tableName = tableName
19 | }
20 |
21 | public func getAllTodos() -> EventLoopFuture<[Todo]> {
22 | let input = DynamoDB.ScanInput(tableName: tableName)
23 |
24 | return db.scan(input).flatMapThrowing { (output) -> [Todo] in
25 | try output.items?.compactMap { try Todo(dictionary: $0) } ?? []
26 | }
27 | }
28 |
29 | public func getTodo(id: String) -> EventLoopFuture {
30 | let input = DynamoDB.GetItemInput(key: [Todo.DynamoDBField.id: DynamoDB.AttributeValue(s: id)], tableName: tableName)
31 |
32 | return db.getItem(input).flatMapThrowing { (output) -> Todo in
33 | if output.item == nil { throw APIError.todoNotFound }
34 | return try Todo(dictionary: output.item ?? [:])
35 | }
36 | }
37 |
38 | public func createTodo(todo: Todo) -> EventLoopFuture {
39 | var todo = todo
40 | let currentDate = Date()
41 |
42 | todo.updatedAt = currentDate
43 | todo.createdAt = currentDate
44 |
45 | let input = DynamoDB.PutItemInput(item: todo.dynamoDbDictionary, tableName: tableName)
46 |
47 | return db.putItem(input).map { (_) -> Todo in
48 | todo
49 | }
50 | }
51 |
52 | public func updateTodo(todo: Todo) -> EventLoopFuture {
53 | var todo = todo
54 |
55 | todo.updatedAt = Date()
56 |
57 | let input = DynamoDB.UpdateItemInput(
58 | expressionAttributeNames: [
59 | "#name": Todo.DynamoDBField.name,
60 | "#isCompleted": Todo.DynamoDBField.isCompleted,
61 | "#dueDate": Todo.DynamoDBField.dueDate,
62 | "#updatedAt": Todo.DynamoDBField.updatedAt
63 | ],
64 | expressionAttributeValues: [
65 | ":name": DynamoDB.AttributeValue(s: todo.name),
66 | ":isCompleted": DynamoDB.AttributeValue(bool: todo.isCompleted),
67 | ":dueDate": DynamoDB.AttributeValue(s: todo.dueDate?.iso8601 ?? ""),
68 | ":updatedAt": DynamoDB.AttributeValue(s: todo.updatedAt?.iso8601 ?? ""),
69 |
70 | ],
71 | key: [Todo.DynamoDBField.id: DynamoDB.AttributeValue(s: todo.id)],
72 | returnValues: DynamoDB.ReturnValue.allNew,
73 | tableName: tableName,
74 | updateExpression: "SET #name = :name, #isCompleted = :isCompleted, #dueDate = :dueDate, #updatedAt = :updatedAt"
75 | )
76 |
77 | return db.updateItem(input).flatMap { (output) in
78 | self.getTodo(id: todo.id)
79 | }
80 | }
81 |
82 | public func deleteTodo(id: String) -> EventLoopFuture {
83 | let input = DynamoDB.DeleteItemInput(
84 | key: [Todo.DynamoDBField.id: DynamoDB.AttributeValue(s: id)],
85 | tableName: tableName
86 | )
87 |
88 | return db.deleteItem(input).map { _ in }
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/TodoLambdaHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import Foundation
9 | import AWSLambdaEvents
10 | import AWSLambdaRuntime
11 | import AsyncHTTPClient
12 | import NIO
13 | import AWSDynamoDB
14 |
15 | struct TodoLamdaHandler: EventLoopLambdaHandler {
16 |
17 | typealias In = APIGateway.Request
18 | typealias Out = APIGateway.Response
19 |
20 | let db: AWSDynamoDB.DynamoDB
21 | let todoService: TodoService
22 | let httpClient: HTTPClient
23 |
24 | init(context: Lambda.InitializationContext) throws {
25 | let timeout = HTTPClient.Configuration.Timeout(
26 | connect: .seconds(30),
27 | read: .seconds(30)
28 | )
29 |
30 | let httpClient = HTTPClient(
31 | eventLoopGroupProvider: .shared(context.eventLoop),
32 | configuration: HTTPClient.Configuration(timeout: timeout)
33 | )
34 |
35 | let tableName = Lambda.env("TODOS_TABLE_NAME") ?? ""
36 | let region: Region
37 | if let envRegion = Lambda.env("AWS_REGION") {
38 | region = Region(rawValue: envRegion)
39 | } else {
40 | region = .uswest2
41 | }
42 |
43 | let db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(httpClient))
44 | let todoService = TodoService(db: db, tableName: tableName)
45 |
46 | self.httpClient = httpClient
47 | self.db = db
48 | self.todoService = todoService
49 | }
50 |
51 | func handle(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture {
52 | guard let handler = Handler.current else {
53 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest))
54 | }
55 |
56 | switch handler {
57 | case .create:
58 | return handleCreate(context: context, event: event)
59 | case .read:
60 | return handleRead(context: context, event: event)
61 | case .update:
62 | return handleUpdate(context: context, event: event)
63 | case .delete:
64 | return handleDelete(context: context, event: event)
65 | case .list:
66 | return handleList(context: context, event: event)
67 | }
68 | }
69 |
70 | func handleCreate(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture {
71 | guard let newTodo: Todo = try? event.bodyObject() else {
72 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest))
73 | }
74 | return todoService.createTodo(todo: newTodo)
75 | .map { todo in
76 | APIGateway.Response(with: todo, statusCode: .ok)
77 | }.flatMapError {
78 | self.catchError(context: context, error: $0)
79 | }
80 | }
81 |
82 | func handleRead(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture {
83 | guard let id = event.pathParameters?[Todo.DynamoDBField.id] else {
84 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .notFound))
85 | }
86 | return todoService.getTodo(id: id)
87 | .map { todo in
88 | APIGateway.Response(with: todo, statusCode: .ok)
89 | }.flatMapError {
90 | self.catchError(context: context, error: $0)
91 | }
92 | }
93 |
94 | func handleUpdate(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture {
95 | guard let updatedTodo: Todo = try? event.bodyObject() else {
96 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest))
97 | }
98 | return todoService.updateTodo(todo: updatedTodo)
99 | .map { todo in
100 | APIGateway.Response(with: todo, statusCode: .ok)
101 | }.flatMapError {
102 | self.catchError(context: context, error: $0)
103 | }
104 | }
105 |
106 | func handleDelete(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture {
107 | guard let id = event.pathParameters?[Todo.DynamoDBField.id] else {
108 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest))
109 | }
110 | return todoService.deleteTodo(id: id)
111 | .map {
112 | APIGateway.Response(with: EmptyResponse(), statusCode: .ok)
113 | }.flatMapError {
114 | self.catchError(context: context, error: $0)
115 | }
116 | }
117 |
118 | func handleList(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture {
119 | return todoService.getAllTodos()
120 | .map { todos in
121 | APIGateway.Response(with: todos, statusCode: .ok)
122 | }.flatMapError {
123 | self.catchError(context: context, error: $0)
124 | }
125 | }
126 |
127 | func catchError(context: Lambda.Context, error: Error) -> EventLoopFuture {
128 | let response = APIGateway.Response(with: error, statusCode: .notFound)
129 | return context.eventLoop.makeSucceededFuture(response)
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Alfian Losari on 29/06/20.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct Utils {
11 |
12 | public static let iso8601Formatter: DateFormatter = {
13 | let formatter = DateFormatter()
14 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
15 | formatter.timeZone = TimeZone(secondsFromGMT: 0)
16 | return formatter
17 | }()
18 |
19 | }
20 |
21 | extension Date {
22 |
23 | var iso8601: String {
24 | Utils.iso8601Formatter.string(from: self)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Sources/TodoAPI/main.swift:
--------------------------------------------------------------------------------
1 | import AWSLambdaRuntime
2 |
3 | Lambda.run(TodoLamdaHandler.init)
4 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import TodoAPITests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += TodoAPITests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Tests/TodoAPITests/TodoAPITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import class Foundation.Bundle
3 |
4 | final class TodoAPITests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 |
10 | // Some of the APIs that we use below are available in macOS 10.13 and above.
11 | guard #available(macOS 10.13, *) else {
12 | return
13 | }
14 |
15 | let fooBinary = productsDirectory.appendingPathComponent("TodoAPI")
16 |
17 | let process = Process()
18 | process.executableURL = fooBinary
19 |
20 | let pipe = Pipe()
21 | process.standardOutput = pipe
22 |
23 | try process.run()
24 | process.waitUntilExit()
25 |
26 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
27 | let output = String(data: data, encoding: .utf8)
28 |
29 | XCTAssertEqual(output, "Hello, world!\n")
30 | }
31 |
32 | /// Returns path to the built products directory.
33 | var productsDirectory: URL {
34 | #if os(macOS)
35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
36 | return bundle.bundleURL.deletingLastPathComponent()
37 | }
38 | fatalError("couldn't find the products directory")
39 | #else
40 | return Bundle.main.bundleURL
41 | #endif
42 | }
43 |
44 | static var allTests = [
45 | ("testExample", testExample),
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/Tests/TodoAPITests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(TodoAPITests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Backend/TodoAPI/scripts/package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 |
5 | executable=$1
6 |
7 | target=.build/lambda/$executable
8 | rm -rf "$target"
9 | mkdir -p "$target"
10 | cp ".build/release/$executable" "$target/"
11 | cp -Pv \
12 | /usr/lib/swift/linux/libBlocksRuntime.so \
13 | /usr/lib/swift/linux/libFoundation*.so \
14 | /usr/lib/swift/linux/libdispatch.so \
15 | /usr/lib/swift/linux/libicu* \
16 | /usr/lib/swift/linux/libswiftCore.so \
17 | /usr/lib/swift/linux/libswiftDispatch.so \
18 | /usr/lib/swift/linux/libswiftGlibc.so \
19 | "$target"
20 | cd "$target"
21 | ln -s "$executable" "bootstrap"
22 | zip --symlinks lambda.zip *
--------------------------------------------------------------------------------
/Backend/TodoAPI/serverless.yml:
--------------------------------------------------------------------------------
1 | service: alf-todoapi
2 |
3 | package:
4 | artifact: .build/lambda/TodoAPI/lambda.zip
5 |
6 | custom:
7 | todosTableName: todos-${self:provider.stage}
8 |
9 | provider:
10 | name: aws
11 | runtime: provided
12 | stage: ${opt:stage, 'dev'}
13 | region: ${opt:region, 'us-west-2'}
14 | environment:
15 | TODOS_TABLE_NAME: "${self:custom.todosTableName}"
16 | iamRoleStatements:
17 | - Effect: Allow
18 | Action:
19 | - logs:CreateLogGroup
20 | - logs:CreateLogStream
21 | - logs:PutLogEvents
22 | Resource: "*"
23 | - Effect: Allow
24 | Action:
25 | - dynamodb:UpdateItem
26 | - dynamodb:PutItem
27 | - dynamodb:GetItem
28 | - dynamodb:DeleteItem
29 | - dynamodb:Query
30 | - dynamodb:Scan
31 | - dynamodb:DescribeTable
32 | Resource:
33 | - { Fn::GetAtt: [TodosTable, Arn] }
34 |
35 | functions:
36 | createTodo:
37 | handler: create
38 | memorySize: 256
39 | events:
40 | - http:
41 | path: /todos
42 | method: post
43 | cors: true
44 | readTodo:
45 | handler: read
46 | memorySize: 256
47 | events:
48 | - http:
49 | path: /todos/{id}
50 | method: get
51 | cors: true
52 | updateTodo:
53 | handler: update
54 | memorySize: 256
55 | events:
56 | - http:
57 | path: /todos/{id}
58 | method: put
59 | cors: true
60 | deleteTodo:
61 | handler: delete
62 | memorySize: 256
63 | events:
64 | - http:
65 | path: /todos/{id}
66 | method: delete
67 | cors: true
68 | listTodos:
69 | handler: list
70 | memorySize: 256
71 | events:
72 | - http:
73 | path: /todos
74 | method: get
75 | cors: true
76 |
77 | resources:
78 | Resources:
79 | TodosTable:
80 | Type: AWS::DynamoDB::Table
81 | Properties:
82 | TableName: ${self:custom.todosTableName}
83 | AttributeDefinitions:
84 | - AttributeName: id
85 | AttributeType: S
86 | KeySchema:
87 | - AttributeName: id
88 | KeyType: HASH
89 | BillingMode: PAY_PER_REQUEST
90 |
--------------------------------------------------------------------------------
/Client/Todo/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/Client/Todo/.DS_Store
--------------------------------------------------------------------------------
/Client/Todo/Todo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8B7FBA9724AD8D4700167A90 /* TodoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9624AD8D4700167A90 /* TodoApp.swift */; };
11 | 8B7FBA9924AD8D4700167A90 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9824AD8D4700167A90 /* ContentView.swift */; };
12 | 8B7FBA9B24AD8D4800167A90 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9A24AD8D4800167A90 /* Assets.xcassets */; };
13 | 8B7FBA9E24AD8D4800167A90 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9D24AD8D4800167A90 /* Preview Assets.xcassets */; };
14 | 8B7FBAA724AD8D6600167A90 /* Todo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAA524AD8D6600167A90 /* Todo.xcdatamodeld */; };
15 | 8B7FBAA924AD8E3500167A90 /* TodoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAA824AD8E3500167A90 /* TodoProvider.swift */; };
16 | 8B7FBAAD24AD8EF300167A90 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAAC24AD8EF300167A90 /* Todo.swift */; };
17 | 8B7FBAAF24AD8F4600167A90 /* TodoError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAAE24AD8F4600167A90 /* TodoError.swift */; };
18 | 8B7FBAB124AD8FE500167A90 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAB024AD8FE500167A90 /* Utils.swift */; };
19 | 8B7FBAB424ADB7C700167A90 /* TodoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAB324ADB7C700167A90 /* TodoListViewModel.swift */; };
20 | 8BC92DD524ADDF1900FFE329 /* TodoFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC92DD424ADDF1900FFE329 /* TodoFormView.swift */; };
21 | 8BC92DD724ADF8B400FFE329 /* TodoFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC92DD624ADF8B400FFE329 /* TodoFormViewModel.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXFileReference section */
25 | 8B7FBA9324AD8D4700167A90 /* Todo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Todo.app; sourceTree = BUILT_PRODUCTS_DIR; };
26 | 8B7FBA9624AD8D4700167A90 /* TodoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoApp.swift; sourceTree = ""; };
27 | 8B7FBA9824AD8D4700167A90 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
28 | 8B7FBA9A24AD8D4800167A90 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
29 | 8B7FBA9D24AD8D4800167A90 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
30 | 8B7FBA9F24AD8D4800167A90 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
31 | 8B7FBAA624AD8D6600167A90 /* Todo.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Todo.xcdatamodel; sourceTree = ""; };
32 | 8B7FBAA824AD8E3500167A90 /* TodoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoProvider.swift; sourceTree = ""; };
33 | 8B7FBAAC24AD8EF300167A90 /* Todo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; };
34 | 8B7FBAAE24AD8F4600167A90 /* TodoError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoError.swift; sourceTree = ""; };
35 | 8B7FBAB024AD8FE500167A90 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
36 | 8B7FBAB324ADB7C700167A90 /* TodoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListViewModel.swift; sourceTree = ""; };
37 | 8BC92DD424ADDF1900FFE329 /* TodoFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoFormView.swift; sourceTree = ""; };
38 | 8BC92DD624ADF8B400FFE329 /* TodoFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoFormViewModel.swift; sourceTree = ""; };
39 | /* End PBXFileReference section */
40 |
41 | /* Begin PBXFrameworksBuildPhase section */
42 | 8B7FBA9024AD8D4700167A90 /* Frameworks */ = {
43 | isa = PBXFrameworksBuildPhase;
44 | buildActionMask = 2147483647;
45 | files = (
46 | );
47 | runOnlyForDeploymentPostprocessing = 0;
48 | };
49 | /* End PBXFrameworksBuildPhase section */
50 |
51 | /* Begin PBXGroup section */
52 | 8B7FBA8A24AD8D4700167A90 = {
53 | isa = PBXGroup;
54 | children = (
55 | 8B7FBA9524AD8D4700167A90 /* Todo */,
56 | 8B7FBA9424AD8D4700167A90 /* Products */,
57 | );
58 | sourceTree = "";
59 | };
60 | 8B7FBA9424AD8D4700167A90 /* Products */ = {
61 | isa = PBXGroup;
62 | children = (
63 | 8B7FBA9324AD8D4700167A90 /* Todo.app */,
64 | );
65 | name = Products;
66 | sourceTree = "";
67 | };
68 | 8B7FBA9524AD8D4700167A90 /* Todo */ = {
69 | isa = PBXGroup;
70 | children = (
71 | 8B7FBA9624AD8D4700167A90 /* TodoApp.swift */,
72 | 8B7FBA9824AD8D4700167A90 /* ContentView.swift */,
73 | 8BC92DD424ADDF1900FFE329 /* TodoFormView.swift */,
74 | 8B7FBAB024AD8FE500167A90 /* Utils.swift */,
75 | 8B7FBAB224ADB7B900167A90 /* Bindings */,
76 | 8B7FBAAB24AD8E9300167A90 /* Services */,
77 | 8B7FBAAA24AD8E8A00167A90 /* Model */,
78 | 8B7FBAA524AD8D6600167A90 /* Todo.xcdatamodeld */,
79 | 8B7FBA9A24AD8D4800167A90 /* Assets.xcassets */,
80 | 8B7FBA9F24AD8D4800167A90 /* Info.plist */,
81 | 8B7FBA9C24AD8D4800167A90 /* Preview Content */,
82 | );
83 | path = Todo;
84 | sourceTree = "";
85 | };
86 | 8B7FBA9C24AD8D4800167A90 /* Preview Content */ = {
87 | isa = PBXGroup;
88 | children = (
89 | 8B7FBA9D24AD8D4800167A90 /* Preview Assets.xcassets */,
90 | );
91 | path = "Preview Content";
92 | sourceTree = "";
93 | };
94 | 8B7FBAAA24AD8E8A00167A90 /* Model */ = {
95 | isa = PBXGroup;
96 | children = (
97 | 8B7FBAAC24AD8EF300167A90 /* Todo.swift */,
98 | 8B7FBAAE24AD8F4600167A90 /* TodoError.swift */,
99 | );
100 | path = Model;
101 | sourceTree = "";
102 | };
103 | 8B7FBAAB24AD8E9300167A90 /* Services */ = {
104 | isa = PBXGroup;
105 | children = (
106 | 8B7FBAA824AD8E3500167A90 /* TodoProvider.swift */,
107 | );
108 | path = Services;
109 | sourceTree = "";
110 | };
111 | 8B7FBAB224ADB7B900167A90 /* Bindings */ = {
112 | isa = PBXGroup;
113 | children = (
114 | 8B7FBAB324ADB7C700167A90 /* TodoListViewModel.swift */,
115 | 8BC92DD624ADF8B400FFE329 /* TodoFormViewModel.swift */,
116 | );
117 | path = Bindings;
118 | sourceTree = "";
119 | };
120 | /* End PBXGroup section */
121 |
122 | /* Begin PBXNativeTarget section */
123 | 8B7FBA9224AD8D4700167A90 /* Todo */ = {
124 | isa = PBXNativeTarget;
125 | buildConfigurationList = 8B7FBAA224AD8D4800167A90 /* Build configuration list for PBXNativeTarget "Todo" */;
126 | buildPhases = (
127 | 8B7FBA8F24AD8D4700167A90 /* Sources */,
128 | 8B7FBA9024AD8D4700167A90 /* Frameworks */,
129 | 8B7FBA9124AD8D4700167A90 /* Resources */,
130 | );
131 | buildRules = (
132 | );
133 | dependencies = (
134 | );
135 | name = Todo;
136 | productName = Todo;
137 | productReference = 8B7FBA9324AD8D4700167A90 /* Todo.app */;
138 | productType = "com.apple.product-type.application";
139 | };
140 | /* End PBXNativeTarget section */
141 |
142 | /* Begin PBXProject section */
143 | 8B7FBA8B24AD8D4700167A90 /* Project object */ = {
144 | isa = PBXProject;
145 | attributes = {
146 | LastSwiftUpdateCheck = 1200;
147 | LastUpgradeCheck = 1200;
148 | TargetAttributes = {
149 | 8B7FBA9224AD8D4700167A90 = {
150 | CreatedOnToolsVersion = 12.0;
151 | };
152 | };
153 | };
154 | buildConfigurationList = 8B7FBA8E24AD8D4700167A90 /* Build configuration list for PBXProject "Todo" */;
155 | compatibilityVersion = "Xcode 9.3";
156 | developmentRegion = en;
157 | hasScannedForEncodings = 0;
158 | knownRegions = (
159 | en,
160 | Base,
161 | );
162 | mainGroup = 8B7FBA8A24AD8D4700167A90;
163 | productRefGroup = 8B7FBA9424AD8D4700167A90 /* Products */;
164 | projectDirPath = "";
165 | projectRoot = "";
166 | targets = (
167 | 8B7FBA9224AD8D4700167A90 /* Todo */,
168 | );
169 | };
170 | /* End PBXProject section */
171 |
172 | /* Begin PBXResourcesBuildPhase section */
173 | 8B7FBA9124AD8D4700167A90 /* Resources */ = {
174 | isa = PBXResourcesBuildPhase;
175 | buildActionMask = 2147483647;
176 | files = (
177 | 8B7FBA9E24AD8D4800167A90 /* Preview Assets.xcassets in Resources */,
178 | 8B7FBA9B24AD8D4800167A90 /* Assets.xcassets in Resources */,
179 | );
180 | runOnlyForDeploymentPostprocessing = 0;
181 | };
182 | /* End PBXResourcesBuildPhase section */
183 |
184 | /* Begin PBXSourcesBuildPhase section */
185 | 8B7FBA8F24AD8D4700167A90 /* Sources */ = {
186 | isa = PBXSourcesBuildPhase;
187 | buildActionMask = 2147483647;
188 | files = (
189 | 8B7FBAB424ADB7C700167A90 /* TodoListViewModel.swift in Sources */,
190 | 8B7FBA9924AD8D4700167A90 /* ContentView.swift in Sources */,
191 | 8B7FBAA724AD8D6600167A90 /* Todo.xcdatamodeld in Sources */,
192 | 8B7FBAA924AD8E3500167A90 /* TodoProvider.swift in Sources */,
193 | 8B7FBAB124AD8FE500167A90 /* Utils.swift in Sources */,
194 | 8B7FBA9724AD8D4700167A90 /* TodoApp.swift in Sources */,
195 | 8B7FBAAF24AD8F4600167A90 /* TodoError.swift in Sources */,
196 | 8B7FBAAD24AD8EF300167A90 /* Todo.swift in Sources */,
197 | 8BC92DD724ADF8B400FFE329 /* TodoFormViewModel.swift in Sources */,
198 | 8BC92DD524ADDF1900FFE329 /* TodoFormView.swift in Sources */,
199 | );
200 | runOnlyForDeploymentPostprocessing = 0;
201 | };
202 | /* End PBXSourcesBuildPhase section */
203 |
204 | /* Begin XCBuildConfiguration section */
205 | 8B7FBAA024AD8D4800167A90 /* Debug */ = {
206 | isa = XCBuildConfiguration;
207 | buildSettings = {
208 | ALWAYS_SEARCH_USER_PATHS = NO;
209 | CLANG_ANALYZER_NONNULL = YES;
210 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
211 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
212 | CLANG_CXX_LIBRARY = "libc++";
213 | CLANG_ENABLE_MODULES = YES;
214 | CLANG_ENABLE_OBJC_ARC = YES;
215 | CLANG_ENABLE_OBJC_WEAK = YES;
216 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
217 | CLANG_WARN_BOOL_CONVERSION = YES;
218 | CLANG_WARN_COMMA = YES;
219 | CLANG_WARN_CONSTANT_CONVERSION = YES;
220 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
221 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
222 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
223 | CLANG_WARN_EMPTY_BODY = YES;
224 | CLANG_WARN_ENUM_CONVERSION = YES;
225 | CLANG_WARN_INFINITE_RECURSION = YES;
226 | CLANG_WARN_INT_CONVERSION = YES;
227 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
228 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
229 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
230 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
231 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
232 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
233 | CLANG_WARN_STRICT_PROTOTYPES = YES;
234 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
235 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
236 | CLANG_WARN_UNREACHABLE_CODE = YES;
237 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
238 | COPY_PHASE_STRIP = NO;
239 | DEBUG_INFORMATION_FORMAT = dwarf;
240 | ENABLE_STRICT_OBJC_MSGSEND = YES;
241 | ENABLE_TESTABILITY = YES;
242 | GCC_C_LANGUAGE_STANDARD = gnu11;
243 | GCC_DYNAMIC_NO_PIC = NO;
244 | GCC_NO_COMMON_BLOCKS = YES;
245 | GCC_OPTIMIZATION_LEVEL = 0;
246 | GCC_PREPROCESSOR_DEFINITIONS = (
247 | "DEBUG=1",
248 | "$(inherited)",
249 | );
250 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
251 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
252 | GCC_WARN_UNDECLARED_SELECTOR = YES;
253 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
254 | GCC_WARN_UNUSED_FUNCTION = YES;
255 | GCC_WARN_UNUSED_VARIABLE = YES;
256 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
257 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
258 | MTL_FAST_MATH = YES;
259 | ONLY_ACTIVE_ARCH = YES;
260 | SDKROOT = iphoneos;
261 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
262 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
263 | };
264 | name = Debug;
265 | };
266 | 8B7FBAA124AD8D4800167A90 /* Release */ = {
267 | isa = XCBuildConfiguration;
268 | buildSettings = {
269 | ALWAYS_SEARCH_USER_PATHS = NO;
270 | CLANG_ANALYZER_NONNULL = YES;
271 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
273 | CLANG_CXX_LIBRARY = "libc++";
274 | CLANG_ENABLE_MODULES = YES;
275 | CLANG_ENABLE_OBJC_ARC = YES;
276 | CLANG_ENABLE_OBJC_WEAK = YES;
277 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
278 | CLANG_WARN_BOOL_CONVERSION = YES;
279 | CLANG_WARN_COMMA = YES;
280 | CLANG_WARN_CONSTANT_CONVERSION = YES;
281 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
282 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
283 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
284 | CLANG_WARN_EMPTY_BODY = YES;
285 | CLANG_WARN_ENUM_CONVERSION = YES;
286 | CLANG_WARN_INFINITE_RECURSION = YES;
287 | CLANG_WARN_INT_CONVERSION = YES;
288 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
289 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
290 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
291 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
292 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
293 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
294 | CLANG_WARN_STRICT_PROTOTYPES = YES;
295 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
296 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
297 | CLANG_WARN_UNREACHABLE_CODE = YES;
298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
299 | COPY_PHASE_STRIP = NO;
300 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
301 | ENABLE_NS_ASSERTIONS = NO;
302 | ENABLE_STRICT_OBJC_MSGSEND = YES;
303 | GCC_C_LANGUAGE_STANDARD = gnu11;
304 | GCC_NO_COMMON_BLOCKS = YES;
305 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
306 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
307 | GCC_WARN_UNDECLARED_SELECTOR = YES;
308 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
309 | GCC_WARN_UNUSED_FUNCTION = YES;
310 | GCC_WARN_UNUSED_VARIABLE = YES;
311 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
312 | MTL_ENABLE_DEBUG_INFO = NO;
313 | MTL_FAST_MATH = YES;
314 | SDKROOT = iphoneos;
315 | SWIFT_COMPILATION_MODE = wholemodule;
316 | SWIFT_OPTIMIZATION_LEVEL = "-O";
317 | VALIDATE_PRODUCT = YES;
318 | };
319 | name = Release;
320 | };
321 | 8B7FBAA324AD8D4800167A90 /* Debug */ = {
322 | isa = XCBuildConfiguration;
323 | buildSettings = {
324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
325 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
326 | CODE_SIGN_STYLE = Automatic;
327 | DEVELOPMENT_ASSET_PATHS = "\"Todo/Preview Content\"";
328 | DEVELOPMENT_TEAM = 5C2XD9H2JS;
329 | ENABLE_PREVIEWS = YES;
330 | INFOPLIST_FILE = Todo/Info.plist;
331 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
332 | LD_RUNPATH_SEARCH_PATHS = (
333 | "$(inherited)",
334 | "@executable_path/Frameworks",
335 | );
336 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.Todo;
337 | PRODUCT_NAME = "$(TARGET_NAME)";
338 | SWIFT_VERSION = 5.0;
339 | TARGETED_DEVICE_FAMILY = "1,2";
340 | };
341 | name = Debug;
342 | };
343 | 8B7FBAA424AD8D4800167A90 /* Release */ = {
344 | isa = XCBuildConfiguration;
345 | buildSettings = {
346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
347 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
348 | CODE_SIGN_STYLE = Automatic;
349 | DEVELOPMENT_ASSET_PATHS = "\"Todo/Preview Content\"";
350 | DEVELOPMENT_TEAM = 5C2XD9H2JS;
351 | ENABLE_PREVIEWS = YES;
352 | INFOPLIST_FILE = Todo/Info.plist;
353 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
354 | LD_RUNPATH_SEARCH_PATHS = (
355 | "$(inherited)",
356 | "@executable_path/Frameworks",
357 | );
358 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.Todo;
359 | PRODUCT_NAME = "$(TARGET_NAME)";
360 | SWIFT_VERSION = 5.0;
361 | TARGETED_DEVICE_FAMILY = "1,2";
362 | };
363 | name = Release;
364 | };
365 | /* End XCBuildConfiguration section */
366 |
367 | /* Begin XCConfigurationList section */
368 | 8B7FBA8E24AD8D4700167A90 /* Build configuration list for PBXProject "Todo" */ = {
369 | isa = XCConfigurationList;
370 | buildConfigurations = (
371 | 8B7FBAA024AD8D4800167A90 /* Debug */,
372 | 8B7FBAA124AD8D4800167A90 /* Release */,
373 | );
374 | defaultConfigurationIsVisible = 0;
375 | defaultConfigurationName = Release;
376 | };
377 | 8B7FBAA224AD8D4800167A90 /* Build configuration list for PBXNativeTarget "Todo" */ = {
378 | isa = XCConfigurationList;
379 | buildConfigurations = (
380 | 8B7FBAA324AD8D4800167A90 /* Debug */,
381 | 8B7FBAA424AD8D4800167A90 /* Release */,
382 | );
383 | defaultConfigurationIsVisible = 0;
384 | defaultConfigurationName = Release;
385 | };
386 | /* End XCConfigurationList section */
387 |
388 | /* Begin XCVersionGroup section */
389 | 8B7FBAA524AD8D6600167A90 /* Todo.xcdatamodeld */ = {
390 | isa = XCVersionGroup;
391 | children = (
392 | 8B7FBAA624AD8D6600167A90 /* Todo.xcdatamodel */,
393 | );
394 | currentVersion = 8B7FBAA624AD8D6600167A90 /* Todo.xcdatamodel */;
395 | path = Todo.xcdatamodeld;
396 | sourceTree = "";
397 | versionGroupType = wrapper.xcdatamodel;
398 | };
399 | /* End XCVersionGroup section */
400 | };
401 | rootObject = 8B7FBA8B24AD8D4700167A90 /* Project object */;
402 | }
403 |
--------------------------------------------------------------------------------
/Client/Todo/Todo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Client/Todo/Todo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Bindings/TodoFormViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoFormViewModel.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | class TodoFormViewModel: ObservableObject {
11 |
12 | let todoService: TodoProvider
13 | @Published var isUploaded = false
14 | @Published var isUploading = false
15 | @Published var error: Error?
16 |
17 | init(todoService: TodoProvider = TodoProvider.shared) {
18 | self.todoService = todoService
19 | }
20 |
21 | func update(todo: Todo) {
22 | self.error = nil
23 | self.isUploaded = false
24 | self.isUploading = true
25 |
26 | todoService.updateTodo(todo) { [weak self] (result) in
27 | DispatchQueue.main.async {
28 | self?.isUploading = false
29 | switch result {
30 | case .success:
31 | self?.isUploaded = true
32 | case .failure(let error):
33 | self?.error = error
34 | }
35 | }
36 | }
37 | }
38 |
39 | func create(todo: TodoProperties) {
40 | self.error = nil
41 | self.isUploaded = false
42 | self.isUploading = true
43 |
44 | todoService.createTodo(todo) { [weak self] (result) in
45 | DispatchQueue.main.async {
46 | self?.isUploading = false
47 | switch result {
48 | case .success:
49 | self?.isUploaded = true
50 | case .failure(let error):
51 | self?.error = error
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Bindings/TodoListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoListViewModel.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | class TodoListViewModel: ObservableObject {
11 |
12 | let service: TodoProvider
13 | @Published var error: Error?
14 | @Published var lastUpdatedAt: Date?
15 |
16 | init(service: TodoProvider = TodoProvider.shared) {
17 | self.service = service
18 | }
19 |
20 | func loadTodoList() {
21 | self.error = nil
22 | service.fetchTodoList { error in
23 | DispatchQueue.main.async { [weak self] in
24 | self?.error = error
25 | self?.service.resetContext()
26 | self?.lastUpdatedAt = error == nil ? Date() : nil
27 | }
28 | }
29 | }
30 |
31 | func deleteTodo(todo: Todo) {
32 | service.deleteTodo(todo: todo) { (_) in
33 | // DispatchQueue.main.async { [weak self] in
34 | //
35 | // }
36 |
37 | }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum FormType: Identifiable {
11 |
12 | var id: String {
13 | switch self {
14 | case .add: return "add"
15 | case .edit(let todo): return "edit-\(todo.id ?? "")"
16 | }
17 | }
18 |
19 | case add
20 | case edit(Todo)
21 | }
22 |
23 | struct ContentView: View {
24 |
25 | @StateObject var todoList = TodoListViewModel()
26 | @State var formType: FormType?
27 |
28 | var body: some View {
29 | NavigationView {
30 | TodoListView(lastUpdatedAt: self.todoList.lastUpdatedAt, formType: $formType, onDelete: { todo in
31 | todoList.deleteTodo(todo: todo)
32 | })
33 | .navigationTitle("Todos-DynamoDB")
34 | .navigationBarItems(leading: Button(action: todoList.loadTodoList, label: {
35 | Image(systemName: "arrow.2.circlepath")
36 | }), trailing: Button(action: {
37 | formType = .add
38 | }, label: {
39 | Image(systemName: "plus")
40 | }))
41 | .sheet(item: $formType) { (formType) in
42 | switch formType {
43 | case .add:
44 | TodoFormView(name: "", isCompleted: false, dueDate: Date())
45 | case .edit(let todoEdit):
46 | TodoFormView(name: todoEdit.name ?? "", isCompleted: todoEdit.isCompleted, dueDate: todoEdit.dueDate ?? Date(), todoEdit: todoEdit)
47 | }
48 | }
49 | .onAppear {
50 | todoList.loadTodoList()
51 | }
52 | }
53 | }
54 | }
55 |
56 | struct TodoListView: View {
57 |
58 | let lastUpdatedAt: Date?
59 | @Binding var formType: FormType?
60 | @State var selection: Todo?
61 | var onDelete: (Todo) -> ()
62 |
63 | @FetchRequest(
64 | entity: Todo.entity(),
65 | sortDescriptors: [
66 | NSSortDescriptor(keyPath: \Todo.isCompleted, ascending: true),
67 | NSSortDescriptor(keyPath: \Todo.dueDate, ascending: false)
68 | ]
69 | )
70 | var todos: FetchedResults
71 |
72 | var body: some View {
73 | List(selection: $selection) {
74 | ForEach(todos) { (todo: Todo) in
75 |
76 | Section {
77 | Button {
78 | self.formType = .edit(todo)
79 | } label: {
80 | HStack {
81 | VStack(alignment: .leading, spacing: 8) {
82 | Text(todo.name ?? "")
83 | .font(.headline)
84 | Text("Due \(todo.formattedDueDateText)")
85 | .font(.subheadline)
86 | .foregroundColor(Color(UIColor.secondaryLabel))
87 | }
88 |
89 | Spacer()
90 |
91 | if todo.isCompleted {
92 | Image(systemName: "checkmark.circle.fill")
93 | .foregroundColor(.green)
94 | } else {
95 | Image(systemName: "chevron.right")
96 | .foregroundColor(Color(UIColor.systemBlue))
97 |
98 | }
99 | }
100 | }
101 | .listRowInsets(EdgeInsets.init(top: 12, leading: 16, bottom: 12, trailing: 16))
102 | }
103 |
104 | }
105 |
106 | .onDelete { indexSet in
107 | if let index = indexSet.first {
108 | let deletedTodo = todos[index]
109 | self.onDelete(deletedTodo)
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
116 |
117 | struct ContentView_Previews: PreviewProvider {
118 | static var previews: some View {
119 | ContentView()
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Model/Todo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Todo.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Todo {
11 |
12 | func update(with todoDictionary: [String: Any]) throws {
13 | guard let newId = todoDictionary["id"] as? String,
14 | let newName = todoDictionary["name"] as? String,
15 | let newIsCompleted = todoDictionary["isCompleted"] as? Bool,
16 | let newDueDate = todoDictionary["dueDate"] as? Date,
17 | let newUpdatedAt = todoDictionary["updatedAt"] as? Date,
18 | let newCreatedAt = todoDictionary["createdAt"] as? Date else {
19 | throw TodoError.decodingError
20 | }
21 |
22 | id = newId
23 | name = newName
24 | isCompleted = newIsCompleted
25 | dueDate = newDueDate
26 | createdAt = newCreatedAt
27 | updatedAt = newUpdatedAt
28 | }
29 | }
30 |
31 | extension Todo: Identifiable {
32 |
33 | var formattedDueDateText: String {
34 | Utils.listDateFormatter.string(from: dueDate ?? Date())
35 | }
36 |
37 | }
38 |
39 | struct TodoProperties: Decodable {
40 |
41 | let id: String
42 | let name: String
43 | let isCompleted: Bool
44 | let dueDate: Date
45 | let createdAt: Date
46 | let updatedAt: Date
47 |
48 | var dictionary: [String: Any] {
49 | return [
50 | "id": id,
51 | "name": name,
52 | "isCompleted": isCompleted,
53 | "dueDate": dueDate,
54 | "createdAt": createdAt,
55 | "updatedAt": updatedAt
56 | ]
57 | }
58 |
59 | var uploadDictionary: [String: Any] {
60 | return [
61 | "id": id,
62 | "name": name,
63 | "isCompleted": isCompleted,
64 | "dueDate": Utils.iso8601Formatter.string(from: dueDate)
65 | ]
66 | }
67 |
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Model/TodoError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoError.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import Foundation
9 |
10 | enum TodoError: Error {
11 | case decodingError
12 | case invalidRequest
13 | case noConnection
14 | case importError
15 | case deleteError
16 | }
17 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Services/TodoProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoProvider.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import CoreData
9 |
10 | class TodoProvider {
11 |
12 | static let shared = TodoProvider()
13 | private init() {}
14 |
15 | static let jsonDecoder: JSONDecoder = {
16 | let decoder = JSONDecoder()
17 | decoder.dateDecodingStrategy = .formatted(Utils.iso8601Formatter)
18 | return decoder
19 | }()
20 |
21 | let baseURL = "PASTE_YOUR_ENDPOINT_HERE/dev/todos"
22 |
23 | lazy var persistentContainer: NSPersistentContainer = {
24 | let container = NSPersistentContainer(name: "Todo")
25 | container.loadPersistentStores { storeDesription, error in
26 | guard error == nil else {
27 | fatalError("Unresolved error \(error!)")
28 | }
29 | }
30 | container.viewContext.automaticallyMergesChangesFromParent = false
31 | container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
32 | container.viewContext.undoManager = UndoManager()
33 | container.viewContext.shouldDeleteInaccessibleFaults = true
34 | return container
35 | }()
36 |
37 | private func newTaskContext() -> NSManagedObjectContext {
38 | let taskContext = persistentContainer.newBackgroundContext()
39 | taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
40 | taskContext.undoManager = nil
41 | return taskContext
42 | }
43 |
44 | func createTodo(_ todo: TodoProperties, completionHandler: @escaping (Result) -> ()) {
45 | guard let url = URL(string: baseURL), let data = try? JSONSerialization.data(withJSONObject: todo.uploadDictionary, options: []) else {
46 | completionHandler(.failure(TodoError.invalidRequest))
47 | return
48 | }
49 |
50 | var request = URLRequest(url: url)
51 | request.httpMethod = "POST"
52 | request.httpBody = data
53 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
54 |
55 | let session = URLSession(configuration: .default)
56 | session.dataTask(with: request) { (data, response, urlSessionError) in
57 | DispatchQueue.main.async {
58 | if let error = urlSessionError {
59 | completionHandler(.failure(error))
60 | return
61 | }
62 |
63 | guard let data = data else {
64 | completionHandler(.failure(TodoError.noConnection))
65 | return
66 | }
67 |
68 | do {
69 | let todoProperty = try TodoProvider.jsonDecoder.decode(TodoProperties.self, from: data)
70 | let todo = Todo(entity: Todo.entity(), insertInto: self.persistentContainer.viewContext)
71 |
72 | try todo.update(with: todoProperty.dictionary)
73 |
74 | try? self.persistentContainer.viewContext.save()
75 | completionHandler(.success(todo))
76 | } catch {
77 | completionHandler(.failure(error))
78 | }
79 | }
80 | }.resume()
81 | }
82 |
83 | func updateTodo(_ todo: Todo, completionHandler: @escaping (Result) -> ()) {
84 | let todoProperties = TodoProperties(id: todo.id ?? "", name: todo.name ?? "", isCompleted: todo.isCompleted, dueDate: todo.dueDate ?? Date(), createdAt: Date(), updatedAt: Date())
85 |
86 | guard let url = URL(string: baseURL)?.appendingPathComponent(todoProperties.id),
87 | let data = try? JSONSerialization.data(withJSONObject: todoProperties.uploadDictionary, options: []) else {
88 | completionHandler(.failure(TodoError.invalidRequest))
89 | return
90 | }
91 |
92 | var request = URLRequest(url: url)
93 | request.httpMethod = "PUT"
94 | request.httpBody = data
95 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
96 |
97 | let session = URLSession(configuration: .default)
98 | session.dataTask(with: request) { (data, response, urlSessionError) in
99 | DispatchQueue.main.async {
100 | if let error = urlSessionError {
101 | self.persistentContainer.viewContext.undo()
102 | completionHandler(.failure(error))
103 | return
104 | }
105 |
106 | guard let data = data else {
107 | self.persistentContainer.viewContext.undo()
108 | completionHandler(.failure(TodoError.noConnection))
109 | return
110 | }
111 |
112 | do {
113 | let todoProperty = try TodoProvider.jsonDecoder.decode(TodoProperties.self, from: data)
114 | todo.createdAt = todoProperty.createdAt
115 | todo.updatedAt = todoProperty.updatedAt
116 | try? self.persistentContainer.viewContext.save()
117 | completionHandler(.success(todo))
118 | } catch {
119 | completionHandler(.failure(error))
120 | }
121 | }
122 | }.resume()
123 | }
124 |
125 |
126 | func deleteTodo(todo: Todo, completionHandler: @escaping (Result) -> ()) {
127 | guard let url = URL(string: baseURL)?.appendingPathComponent(todo.id ?? "") else {
128 | completionHandler(.failure(TodoError.invalidRequest))
129 | return
130 | }
131 |
132 | self.persistentContainer.viewContext.delete(todo)
133 |
134 | var request = URLRequest(url: url)
135 | request.httpMethod = "DELETE"
136 |
137 | let session = URLSession(configuration: .default)
138 | session.dataTask(with: request) { (data, response, urlSessionError) in
139 | DispatchQueue.main.async {
140 | if let error = urlSessionError {
141 | self.persistentContainer.viewContext.undo()
142 | completionHandler(.failure(error))
143 | return
144 | }
145 |
146 | guard let _ = data else {
147 | self.persistentContainer.viewContext.undo()
148 | completionHandler(.failure(TodoError.noConnection))
149 | return
150 | }
151 |
152 | try? self.persistentContainer.viewContext.save()
153 | completionHandler(.success(()))
154 | }
155 | }.resume()
156 | }
157 |
158 | func fetchTodoList(completionHandler: @escaping (Error?) -> Void) {
159 | guard let jsonURL = URL(string: baseURL) else {
160 | completionHandler(TodoError.invalidRequest)
161 | return
162 | }
163 | let session = URLSession(configuration: .default)
164 |
165 | let task = session.dataTask(with: jsonURL) { data, _, urlSessionError in
166 | guard urlSessionError == nil else {
167 | completionHandler(urlSessionError)
168 | return
169 | }
170 |
171 | guard let data = data else {
172 | completionHandler(TodoError.noConnection)
173 | return
174 | }
175 |
176 | self.deleteAll { (error) in
177 | if let error = error {
178 | completionHandler(error)
179 | return
180 | }
181 |
182 | do {
183 | let todos = try TodoProvider.jsonDecoder.decode([TodoProperties].self, from: data)
184 | try self.importTodos(from: todos)
185 |
186 | } catch {
187 | completionHandler(error)
188 | return
189 | }
190 | completionHandler(nil)
191 | }
192 | }
193 | task.resume()
194 | }
195 |
196 | private func importTodos(from todos: [TodoProperties]) throws {
197 | guard !todos.isEmpty else { return }
198 |
199 | var performError: Error?
200 | let todosDictionaryList = todos.map { $0.dictionary }
201 |
202 | let taskContext = newTaskContext()
203 | taskContext.performAndWait {
204 | let batchInsert = self.newBatchInsertRequest(with: todosDictionaryList)
205 | batchInsert.resultType = .statusOnly
206 |
207 | if let batchInsertResult = try? taskContext.execute(batchInsert) as? NSBatchInsertResult,
208 | let success = batchInsertResult.result as? Bool, success {
209 | return
210 | }
211 | performError = TodoError.importError
212 | }
213 |
214 | if let error = performError {
215 | throw error
216 | }
217 | }
218 |
219 | private func newBatchInsertRequest(with todoDictionaryList: [[String: Any]]) -> NSBatchInsertRequest {
220 | let batchInsert: NSBatchInsertRequest
221 | var index = 0
222 | let total = todoDictionaryList.count
223 | batchInsert = NSBatchInsertRequest(entityName: "Todo", dictionaryHandler: { dictionary in
224 | guard index < total else { return true }
225 | dictionary.addEntries(from: todoDictionaryList[index])
226 | index += 1
227 | return false
228 | })
229 | return batchInsert
230 | }
231 |
232 | func deleteAll(completionHandler: @escaping (Error?) -> Void) {
233 | let taskContext = newTaskContext()
234 | taskContext.perform {
235 | let fetchRequest = NSFetchRequest(entityName: "Todo")
236 | let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
237 | batchDeleteRequest.resultType = .resultTypeCount
238 |
239 | if let batchDeleteResult = try? taskContext.execute(batchDeleteRequest) as? NSBatchDeleteResult,
240 | batchDeleteResult.result != nil {
241 | completionHandler(nil)
242 |
243 | } else {
244 | completionHandler(TodoError.deleteError)
245 | }
246 | }
247 | }
248 |
249 | func resetContext() {
250 | persistentContainer.viewContext.reset()
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Todo.xcdatamodeld/Todo.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/TodoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoApp.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct TodoApp: App {
12 | var body: some Scene {
13 | let viewContext = TodoProvider.shared.persistentContainer.viewContext
14 |
15 | return WindowGroup {
16 | ContentView()
17 | .environment(\.managedObjectContext, viewContext)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/TodoFormView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoFormView.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct TodoFormView: View {
11 |
12 | @State var name: String
13 | @State var isCompleted: Bool
14 | @State var dueDate: Date
15 |
16 | var todoEdit: Todo?
17 |
18 | @StateObject var todoFormVM = TodoFormViewModel()
19 |
20 | @Environment(\.presentationMode) var presentationMode
21 |
22 | var body: some View {
23 | NavigationView {
24 | Form {
25 |
26 | Section {
27 | TextEditor(text: $name)
28 | .frame(height: 160)
29 | }
30 |
31 |
32 |
33 | Toggle("Completed", isOn: $isCompleted)
34 | DatePicker("Due date", selection: self.$dueDate)
35 | }
36 |
37 | .navigationBarItems(leading: Button(action: {
38 | presentationMode.wrappedValue.dismiss()
39 | }, label: {
40 | Text("Cancel")
41 | }), trailing: Button(action: {
42 | guard !name.isEmpty else { return }
43 | if let todoEdit = self.todoEdit {
44 | todoEdit.name = name
45 | todoEdit.isCompleted = isCompleted
46 | todoEdit.dueDate = dueDate
47 | todoFormVM.update(todo: todoEdit)
48 | } else {
49 | let todo = TodoProperties(id: UUID().uuidString, name: name, isCompleted: isCompleted, dueDate: dueDate, createdAt: Date(), updatedAt: Date())
50 | todoFormVM.create(todo: todo)
51 | }
52 | }, label: {
53 | Text("Save")
54 | }))
55 | .navigationTitle(todoEdit == nil ? "Create Todo" : "Update Todo")
56 | .onChange(of: self.todoFormVM.isUploaded) { value in
57 | guard value else {
58 | return
59 | }
60 | presentationMode.wrappedValue.dismiss()
61 | }
62 | .overlay(Group {
63 | if todoFormVM.isUploading {
64 | ProgressView("Submitting...")
65 | } else {
66 | EmptyView()
67 | }
68 | })
69 | .disabled(todoFormVM.isUploading)
70 | }
71 | }
72 |
73 | }
74 |
75 | struct TodoFormView_Previews: PreviewProvider {
76 | static var previews: some View {
77 | TodoFormView(name: "", isCompleted: false, dueDate: Date())
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Client/Todo/Todo/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | // Todo
4 | //
5 | // Created by Alfian Losari on 02/07/20.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct Utils {
11 |
12 | public static let iso8601Formatter: DateFormatter = {
13 | let formatter = DateFormatter()
14 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
15 | formatter.timeZone = TimeZone(secondsFromGMT: 0)
16 | return formatter
17 | }()
18 |
19 | public static let listDateFormatter: DateFormatter = {
20 | let formatter = DateFormatter()
21 | formatter.dateFormat = "EEE MMM d"
22 | formatter.timeZone = TimeZone(secondsFromGMT: 0)
23 | return formatter
24 | }()
25 |
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift Todo REST API with AWS Lambda
2 |
3 | 
4 |
5 | Source code for Tutorial on building a Swift REST API to perform CRUD operations for Todo Items, learn how to persist data to AWS DynamoDB using AWS Swift SDK, handle events using Swift AWS Lambda Runtime library, and deploy to AWS Lambda using Serverless framework.
6 |
7 | ## Tutorial Video
8 | Youtube link at https://youtu.be/HHg3fVfpj6M
9 |
10 | ## Backend App Requirement
11 | - Xcode 11.5
12 | - AWS Credentials to provision resources
13 | - Serverless Framework for deployment. https://www.serverless.com
14 | - Docker for build and packaging. https://www.docker.com/products/docker-desktop
15 |
16 | ## Getting Started - Backend
17 | - Copy and Clone the project
18 | - Create the container using Dockerfile
19 | - Build the project in release mode using the docker container
20 | - Run the script inside scripts/package.sh to package the app into Lambda.zip inside the build folder
21 | - Update the serverless by providing your own unique service, dynamo db table
22 | - Deploy using sls -v deploy
23 |
24 | ## Backend Endpoints
25 | - List Todos: /todos (GET)
26 | - Read Todo: /todos/{id} (GET)
27 | ```
28 | // Response JSON Body
29 | {
30 | "id": "String",
31 | "name": "String",
32 | "isCompleted": "Boolean",
33 | "dueDate": "ISO8601 formatted String",
34 | "createdAt": "ISO8601 formatted String",
35 | "updatedAt": "ISO8601 formatted String"
36 | }
37 | ```
38 | - Create Todo: /todos (POST)
39 | ```
40 | // Request JSON Body
41 | {
42 | "id": "String",
43 | "name": "String",
44 | "isCompleted": "Boolean",
45 | "dueDate": "ISO8601 formatted String"
46 | }
47 | ```
48 | - Update Todo: /todos/{id} (PUT)
49 | ```
50 | // Request JSON Body
51 | {
52 | "name": "String",
53 | "isCompleted": "Boolean",
54 | "dueDate": "ISO8601 formatted String"
55 | }
56 | ```
57 | - Delete Todo: /todos/{id} (DELETE)
58 |
59 |
60 | ## Front end App Requirement
61 | - Xcode 12
62 |
63 | ## Getting Started - Frontend
64 | - Paste the endpoint url from backend deployment into TodoProvider.swift
65 |
--------------------------------------------------------------------------------
/promo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/promo.jpeg
--------------------------------------------------------------------------------