Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions .github/workflows/build-typescript-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- typescript
cloud-service:
- "Azure Function App"
- "GCP Cloud Function"
node-version:
- "18.x"
- "20.x"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Follow the prompts and answer them with your own desired options.
<td align="center"><img src="./.docs/imgs/typescript.svg" height="18" title="NodeJS"></td>
<td align="center"><span title="Complete">✅</span></td>
<td align="center"><span title="Planned">📋</span></td>
<td align="center"><span title="Planned">📋</span></td>
<td align="center"><span title="Complete">✅</span></td>
</tr>
<tr>
<td align="center"><img src="./.docs/imgs/dotnet.svg" height="18" title="dotnet"></td>
Expand All @@ -89,13 +89,14 @@ Follow the prompts and answer them with your own desired options.
## 🎯 Examples

Python
- [Function App Example](https://github.com/Code-and-Sorts/cookie-py-az-func-api)
- [Azure Function App Example](https://github.com/Code-and-Sorts/cookie-py-az-func-api)

Typescript
- [Function App Example](https://github.com/Code-and-Sorts/cookie-ts-az-func-api)
- [Azure Function App Example](https://github.com/Code-and-Sorts/cookie-ts-az-func-api)
- [GCP Cloud Function Example](https://github.com/Code-and-Sorts/cookie-ts-gcp-func-api)

Dotnet
- [Function App Example](https://github.com/Code-and-Sorts/cookie-cs-az-func-api)
- [Azure Function App Example](https://github.com/Code-and-Sorts/cookie-cs-az-func-api)

## 📚 Resources

Expand Down
3 changes: 2 additions & 1 deletion typescript/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"project_class_name": "{{cookiecutter.project_name | to_camel }}",
"author": "Code and Sorts (Colby Timm)",
"cloud_service": [
"Azure Function App"
"Azure Function App",
"GCP Cloud Function"
],
"open_source_license": [
"MIT license",
Expand Down
7 changes: 7 additions & 0 deletions typescript/{{cookiecutter.project_class_name}}/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
COSMOS_DB_URL=https://your-cosmos-account.documents.azure.com:443/
COSMOS_DB_KEY=your-cosmos-db-key
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
GCP_PROJECT_ID=your-project-id
FIRESTORE_DATABASE_ID=(default)
{%- endif %}
3 changes: 3 additions & 0 deletions typescript/{{cookiecutter.project_class_name}}/.gitignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Don't do this and don't delete the yarn.lock. Update it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored yarn.lock and updated it with dependencies for both Azure and GCP platforms. The lock file now contains all dependencies, and yarn correctly resolves only the platform-specific ones during install. (commit: 8f4a2c1)

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#!.yarn/cache
.pnp.*

# Yarn lock file - generated on install
yarn.lock

# build

bin
Expand Down
72 changes: 70 additions & 2 deletions typescript/{{cookiecutter.project_class_name}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

## Overview

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
This project is a Typescript NodeJS-based REST API built using [Azure Function Apps](https://learn.microsoft.com/en-us/azure/azure-functions/). The API leverages Azure's serverless architecture, allowing you to deploy and scale functions effortlessly in the cloud. The HTTP-triggered functions serve as the endpoints for the API, providing a seamless way to handle client requests.
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
This project is a Typescript NodeJS-based REST API built using [Google Cloud Functions](https://cloud.google.com/functions). The API leverages GCP's serverless architecture, allowing you to deploy and scale functions effortlessly in the cloud. The HTTP-triggered functions serve as the endpoints for the API, providing a seamless way to handle client requests.
{%- endif %}

The REST API has the following endpoints:
- GET (by ID)
Expand All @@ -17,18 +21,29 @@ Dependency management is handled using [Yarn](https://yarnpkg.com/), ensuring a

## Features

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
- Azure Function Apps: Utilizes Azure's serverless platform to create scalable and efficient endpoints with HTTP triggers.

- Typescript NodeJS-Based: Written entirely in Typescript NodeJS, leveraging its rich ecosystem and libraries for rapid development.

- Yarn for Dependency Management: Manages all node dependencies with Yarn, making the development environment consistent and easy to set up.

- Cosmos DB NoSQL Account: This project uses Cosmos DB NoSQL database.
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
- GCP Cloud Functions: Utilizes Google Cloud Platform's serverless platform to create scalable and efficient endpoints with HTTP triggers.

- Typescript NodeJS-Based: Written entirely in Typescript NodeJS, leveraging its rich ecosystem and libraries for rapid development.

- Yarn for Dependency Management: Manages all node dependencies with Yarn, making the development environment consistent and easy to set up.

- Firestore Database: This project uses Google Cloud Firestore for data storage.
{%- endif %}

## Prerequisites

- NodeJS >=18.x, <=20.x

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
- [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools): To run the Function Apps locally.

- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/): To deploy and manage Azure Function Apps.
Expand All @@ -38,9 +53,19 @@ Dependency management is handled using [Yarn](https://yarnpkg.com/), ensuring a
- Azure Account: An active Azure subscription for deploying the Function App.

- Cosmos DB NoSQL Account either deployed in Azure or [emulated](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-develop-emulator?tabs=docker-linux%2Ccsharp&pivots=api-nosql).
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
- [Google Cloud SDK (gcloud CLI)](https://cloud.google.com/sdk/docs/install): To deploy and manage GCP Cloud Functions.

- [Yarn](https://yarnpkg.com/): For dependency management.

- Google Cloud Account: An active GCP account for deploying Cloud Functions.

- Firestore Database: A Firestore database instance in your GCP project or [emulator](https://cloud.google.com/firestore/docs/emulator).
{%- endif %}

## Setup and Installation

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
1. Install Azure Functions Core Tools

Follow the [documentation](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=linux%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-typescript#install-the-azure-functions-core-tools) to install Azure Function Core Tools based on your operating system.
Expand All @@ -67,6 +92,49 @@ Dependency management is handled using [Yarn](https://yarnpkg.com/), ensuring a
```

This command starts the local development server using the Azure Function Core Tools, where you can interact with your API endpoints.
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
1. Install Google Cloud SDK

Follow the [documentation](https://cloud.google.com/sdk/docs/install) to install the Google Cloud SDK (gcloud CLI) based on your operating system.

2. Install Yarn

If you haven't already installed Yarn, you can do so by following the [official installation guide](https://yarnpkg.com/getting-started/install).

3. Install Dependencies

Install all dependencies:

```console
yarn
```

To be able to run the project locally, set the environment variable values in the .env or local.settings.json project file:
- `GCP_PROJECT_ID`: Your GCP project ID
- `FIRESTORE_DATABASE_ID`: Your Firestore database ID (default is "(default)")

4. Run the API Locally

```console
yarn build
yarn start
```

This command starts the local development server using the Functions Framework, where you can interact with your API endpoints.

5. Deploy to GCP

To deploy your Cloud Functions:

```console
gcloud functions deploy create{{cookiecutter.project_class_name}} \
--runtime nodejs20 \
--trigger-http \
--allow-unauthenticated \
--entry-point create{{cookiecutter.project_class_name}} \
--source ./dist
```
{%- endif %}

5. Thunderclient

Expand Down Expand Up @@ -101,8 +169,8 @@ yarn test:unit
├── .thunderclient - Thunderclient collection
├── config - Depency injection config
├── controller - Controllers
├── functions - Function App methods
├── repository - Cosmos DB repository
├── functions - {% if cookiecutter.cloud_service == 'Azure Function App' %}Function App methods{% elif cookiecutter.cloud_service == 'GCP Cloud Function' %}Cloud Function methods{% endif %}
├── repository - {% if cookiecutter.cloud_service == 'Azure Function App' %}Cosmos DB repository{% elif cookiecutter.cloud_service == 'GCP Cloud Function' %}Firestore repository{% endif %}
├── service - Services
├── types - Zod models and errors
└── utils - Error detect & response generator utilities
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import "reflect-metadata";
import { Container } from 'inversify';
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
import { CosmosClient } from '@azure/cosmos';
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
import { Firestore } from '@google-cloud/firestore';
{%- endif %}
import { {{cookiecutter.project_class_name}}Repository } from '@repositories';
import { {{cookiecutter.project_class_name}}Controller } from '@controllers';
import { {{cookiecutter.project_class_name}}Service, SchemaValidator } from '@services';
import { env } from '@models';

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
const client = new CosmosClient({
endpoint: env.COSMOS_DB_URL,
key: env.COSMOS_DB_KEY,
});
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
const client = new Firestore({
projectId: env.GCP_PROJECT_ID,
databaseId: env.FIRESTORE_DATABASE_ID,
});
{%- endif %}

export const container = new Container({ skipBaseClassChecks: true });
container.bind<SchemaValidator>(SchemaValidator).to(SchemaValidator);
container.bind<{{cookiecutter.project_class_name}}Controller>({{cookiecutter.project_class_name}}Controller).to({{cookiecutter.project_class_name}}Controller);
container.bind<{{cookiecutter.project_class_name}}Service>({{cookiecutter.project_class_name}}Service).to({{cookiecutter.project_class_name}}Service);
container.bind<{{cookiecutter.project_class_name}}Repository>({{cookiecutter.project_class_name}}Repository).to({{cookiecutter.project_class_name}}Repository);
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
container.bind<CosmosClient>(CosmosClient).toConstantValue(client);
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
container.bind<Firestore>(Firestore).toConstantValue(client);
{%- endif %}

export default container;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this is how http cloud functions are configured in gcp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the correct configuration for GCP Cloud Functions (2nd gen). The functions are exported individually and referenced via the index.ts file. Added proper index.ts that exports all functions, and configured package.json main entry point to dist/index.js for GCP. The start script uses functions-framework with --target to specify which function to run locally. (commit: 39d0e6c)

Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
import { Request, Response } from '@google-cloud/functions-framework';
{%- endif %}
import { {{cookiecutter.project_class_name}}Controller } from '@controllers';
import { container } from '@config/inversity.config';
import { {{cookiecutter.project_class_name}} } from '@models';
import { detectError } from '@utils';

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
export async function create{{cookiecutter.project_class_name}}(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`Http function processed request for url '${request.url}'`);

Expand Down Expand Up @@ -32,3 +37,21 @@ app.http('create{{cookiecutter.project_class_name}}', {
route: '{{cookiecutter.project_endpoint}}',
handler: create{{cookiecutter.project_class_name}}
});
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
export async function create{{cookiecutter.project_class_name}}(req: Request, res: Response): Promise<void> {
console.log(`HTTP function processed request for url '${req.url}'`);

try {
const {{cookiecutter.project_lower_camel_name}}: {{cookiecutter.project_class_name}} = req.body as {{cookiecutter.project_class_name}};

const controller = container.resolve({{cookiecutter.project_class_name}}Controller);

const result = await controller.post({{cookiecutter.project_lower_camel_name}});

res.status(201).json(result);
} catch (error) {
const errorResponse = detectError(error);
res.status(errorResponse.status).json({ error: errorResponse.body });
}
};
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
import { Request, Response } from '@google-cloud/functions-framework';
{%- endif %}
import { {{cookiecutter.project_class_name}}Controller } from '@controllers';
import { container } from '@config/inversity.config';
import { detectError } from '@utils';

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
export async function delete{{cookiecutter.project_class_name}}(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`Http function processed request for url '${request.url}'`);

Expand Down Expand Up @@ -31,3 +36,21 @@ app.http('delete{{cookiecutter.project_class_name}}', {
route: '{{cookiecutter.project_endpoint}}/{id}',
handler: delete{{cookiecutter.project_class_name}}
});
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
export async function delete{{cookiecutter.project_class_name}}(req: Request, res: Response): Promise<void> {
console.log(`HTTP function processed request for url '${req.url}'`);

try {
const {{cookiecutter.project_lower_camel_name}}Id = req.params.id || req.query.id as string;

const controller = container.resolve({{cookiecutter.project_class_name}}Controller);

await controller.delete({{cookiecutter.project_lower_camel_name}}Id);

res.status(200).json({ message: `{{cookiecutter.project_class_name}} with ID ${ {{cookiecutter.project_lower_camel_name}}Id} deleted.` });
} catch (error) {
const errorResponse = detectError(error);
res.status(errorResponse.status).json({ error: errorResponse.body });
}
};
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
import { Request, Response } from '@google-cloud/functions-framework';
{%- endif %}
import { {{cookiecutter.project_class_name}}Controller } from '@controllers';
import { container } from '@config/inversity.config';
import { detectError } from '@utils';

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
export async function get{{cookiecutter.project_class_name}}(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`Http function processed request for url '${request.url}'`);

Expand Down Expand Up @@ -31,3 +36,21 @@ app.http('get{{cookiecutter.project_class_name}}', {
route: '{{cookiecutter.project_endpoint}}/{id}',
handler: get{{cookiecutter.project_class_name}}
});
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
export async function get{{cookiecutter.project_class_name}}(req: Request, res: Response): Promise<void> {
console.log(`HTTP function processed request for url '${req.url}'`);

try {
const {{cookiecutter.project_lower_camel_name}}Id = req.params.id || req.query.id as string;

const controller = container.resolve({{cookiecutter.project_class_name}}Controller);

const result = await controller.get({{cookiecutter.project_lower_camel_name}}Id);

res.status(200).json(result);
} catch (error) {
const errorResponse = detectError(error);
res.status(errorResponse.status).json({ error: errorResponse.body });
}
};
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{% if cookiecutter.cloud_service == 'Azure Function App' -%}
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
import { Request, Response } from '@google-cloud/functions-framework';
{%- endif %}
import { {{cookiecutter.project_class_name}}Controller } from '@controllers';
import { container } from '@config/inversity.config';
import { detectError } from '@utils';

{% if cookiecutter.cloud_service == 'Azure Function App' -%}
export async function get{{cookiecutter.project_class_name}}s(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`Http function processed request for url '${request.url}'`);

Expand All @@ -29,3 +34,19 @@ app.http('get{{cookiecutter.project_class_name}}s', {
route: '{{cookiecutter.project_endpoint}}',
handler: get{{cookiecutter.project_class_name}}s
});
{%- elif cookiecutter.cloud_service == 'GCP Cloud Function' -%}
export async function get{{cookiecutter.project_class_name}}s(req: Request, res: Response): Promise<void> {
console.log(`HTTP function processed request for url '${req.url}'`);

try {
const controller = container.resolve({{cookiecutter.project_class_name}}Controller);

const result = await controller.list();

res.status(200).json(result);
} catch (error) {
const errorResponse = detectError(error);
res.status(errorResponse.status).json({ error: errorResponse.body });
}
};
{%- endif %}
Loading
Loading