Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Source code for Noogler Project - Todo App #14282

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using FirebaseAdmin;
using FirebaseAdmin.Auth;
using Google.Apis.Auth.OAuth2;
using Google.Cloud.Firestore;
using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace TodoFunctions;

/// <summary>
/// Abstract parent class for CRUD functions on the Todo application.
/// </summary>
public abstract class AbstractTodoFunction : IHttpFunction
{
private const string ProjectId = "REDACTED";
private const string DatabaseId = "todo-application";
private const string CollectionId = "todo-items";
private const FirebaseAuth FirebaseAuth = FirebaseAuth.GetAuth(FirebaseApp.Create(new AppOptions()
{
Credential = GoogleCredential.GetApplicationDefault(),
ProjectId = ProjectId,
}));
private const FirestoreDb FirebaseDb = new FirestoreDbBuilder { ProjectId = ProjectId, DatabaseId = DatabaseId }.Build();
protected const CollectionReference TodoItemsCollection = FirebaseDb.Collection(CollectionId);
protected const JsonSerializerOptions SerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(), new FirestoreTimestampConverter() }
};

/// <summary>
/// Main method for the CRUD operations. Authenticates user, validates the inputs, executes the DB operation, and prints results in the response.
/// </summary>
/// <param name="context">Object for the HTTP Request</param>
/// <returns>void</returns>
public async Task HandleAsync(HttpContext context)
{

// authenticate and parse request
TodoData todoData = await AuthenticateAndParseRequest(context);

// validate request
bool isValid = ValidateRequest(todoData);
if (!isValid)
{
await context.Response.WriteAsync("Request is not valid\n");
return;
}

// perform operation
try
{
string output = await PerformOperation(todoData);
await context.Response.WriteAsync(output);
}
catch (Exception e)
{
await context.Response.WriteAsync($"{e.Message}\n");
}
}

/// <summary>
/// Authenticates user and deserializes the request body into TodoData object and returns it
/// </summary>
/// <param name="context">Object for the HTTP Request</param>
/// <returns>TodoData object</returns>
private static async Task<TodoData> AuthenticateAndParseRequest(HttpContext context)
{

using var reader = new StreamReader(context.Request.Body);
try
{
// deserialize request body
string body = await reader.ReadToEndAsync();
TodoData todoData = JsonSerializer.Deserialize<TodoData>(body, SerializerOptions);

// authenticate user and get user email
FirebaseToken token = await FirebaseAuth.VerifyIdTokenAsync(todoData.IdToken);
UserRecord userRecord = await FirebaseAuth.GetUserAsync(token.Uid);
todoData.UserId = userRecord.Email;

// set IdToken to null to avoid printing it out or storing it
todoData.IdToken = null;

await context.Response.WriteAsync($"Request Body: {JsonSerializer.Serialize(todoData, SerializerOptions)}\n");
return todoData;
}
catch (Exception ex)
{
await context.Response.WriteAsync($"{ex.Message}\n");
throw;
}
}

/// <summary>
/// Performs validation on inputs
/// </summary>
/// <param name="todoData">TodoData object</param>
/// <returns>true if valid, false otherwise</returns>
protected abstract bool ValidateRequest(TodoData todoData);

/// <summary>
/// Calls the Firestore client to perform DB operations
/// </summary>
/// <param name="todoData">TodoData object</param>
/// <returns>Result string</returns>
protected abstract Task<string> PerformOperation(TodoData todoData);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Threading.Tasks;

namespace TodoFunctions;

/// <summary>
/// Cloud Function to create a todo item in the Todo Application.
/// </summary>
public class CreateTodo : AbstractTodoFunction
{
protected override bool ValidateRequest(TodoData todoData)
{
string userId = todoData.UserId;
string title = todoData.Title;
string description = todoData.Description;
int? priority = todoData.Priority;

// UserID, title, and description must not be null or empty. Priority must be an integer between 1 to 5.
return userId is not null
&& title is not null
&& description is not null
&& userId != ""
&& title != ""
&& description != ""
&& (priority is null || (1 <= priority && priority <= 5));
}
protected override async Task<string> PerformOperation(TodoData todoData)
{
// prepare the object before creating
todoData.Prepare(Operation.Create);

// create the document
string documentId = $"{todoData.UserId}-{todoData.TaskId}";
await todoItemsCollection.Document(documentId).CreateAsync(todoData.ToDictionary());

return $"Successfully created new task {documentId}\n";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.Firestore;
using System.Threading.Tasks;

namespace TodoFunctions;

/// <summary>
/// Cloud Function to delete a todo item in the Todo Application
/// </summary>
public class DeleteTodo : AbstractTodoFunction
{

protected override bool ValidateRequest(TodoData todoData)
{
string userId = todoData.UserId;
string taskId = todoData.TaskId;

// UserID and taskId must not be null or empty.
return userId is not null
&& taskId is not null
&& userId != ""
&& taskId != "";
}

protected override async Task<string> PerformOperation(TodoData todoData)
{
// delete the document
string documentId = $"{todoData.UserId}-{todoData.TaskId}";
await todoItemsCollection.Document(documentId).DeleteAsync(Precondition.MustExist);

return $"Successfully deleted task {documentId}\n";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.Firestore;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace TodoFunctions
{
public class FirestoreTimestampConverter : JsonConverter<Timestamp>
{
private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; // ISO 8601 format

public override void Write(Utf8JsonWriter writer, Timestamp value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}

// Convert Timestamp to ISO 8601 string (UTC)
DateTime dateTime = value.ToDateTime();
string isoString = dateTime.ToUniversalTime().ToString(TimestampFormat);
writer.WriteStringValue(isoString);
}

public override Timestamp Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string dateString = reader.GetString();
if (DateTime.TryParse(dateString, out DateTime dateTime))
{
return Timestamp.FromDateTime(dateTime.ToUniversalTime());
}
else
{
throw new JsonException($"Could not parse DateTime from string: {dateString}");
}
}

throw new JsonException($"Unexpected token type: {reader.TokenType}. Expected String.");
}
}

public class FirestoreTimestampConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(Timestamp);
}

public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new FirestoreTimestampConverter();
}
}
}
76 changes: 76 additions & 0 deletions issues/Charlotte Y - Noogler Project/TodoFunctions/ReadTodos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2025 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.Firestore;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace TodoFunctions;

/// <summary>
/// Cloud Function to read todo items in the Todo Application
/// </summary>
public class ReadTodos : AbstractTodoFunction
{
protected override bool ValidateRequest(TodoData todoData)
{
string userId = todoData.UserId;
string taskId = todoData.TaskId;
int? priority = todoData.Priority;

// UserId should not be null or empty. TaskId, if provided, should not be empty. Priority should be an integer between 1 to 5.
return userId is not null
&& userId != ""
&& (taskId is null || taskId != "")
&& (priority is null || (1 <= priority && priority <= 5));
}

protected override async Task<string> PerformOperation(TodoData todoData)
{

// Create a filter for the query based on the request
List<Filter> filterList = new() {
Filter.EqualTo("userId", todoData.UserId)
};
if (todoData.TaskId != null)
{
filterList.Add(Filter.EqualTo("taskId", todoData.TaskId));
}
if (todoData.Priority != null)
{
filterList.Add(Filter.EqualTo("priority", todoData.Priority));
}
if (todoData.Status != null)
{
filterList.Add(Filter.EqualTo("status", todoData.Status.ToString()));
}

// Query the collection
Query query = todoItemsCollection.Where(Filter.And(filterList.ToArray()));
QuerySnapshot querySS = await query.GetSnapshotAsync();

// Serialize the results
StringBuilder output = new();
foreach (DocumentSnapshot document in querySS.Documents)
{
output.Append(JsonSerializer.Serialize(document.ToDictionary(), SERIALIZER_OPTIONS));
output.Append('\n');
}

// Build the results into a string and return
return output.ToString();
}
}
Loading