Typescript cloud function example

This commit is contained in:
cghislai 2025-09-19 15:55:37 +02:00
commit b02000ed60
19 changed files with 59968 additions and 0 deletions

11
.env.example Normal file
View File

@ -0,0 +1,11 @@
# COPY this env file to `.env` and update values to run locally
# API configuration
API_BASE_URL=https://api.nitrodev.ebitda.tech/domain-ws
API_AUTH_URL=https://sso.nitrodev.ebitda.tech/realms/nitro
API_CLIENT_ID=<service account id>
API_CLIENT_SECRET=<service account secret>
# Function configuration
DEBUG=false
DRY_RUN_SKIP_API_WRITE=true

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules/
dist/
.env
coverage/
.idea/
**/*.iml

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Typescript cloud function example
- Use the package.json scripts
- `npm run generate-api-client` To regenerate the api client with the openapi spec of nitrodev
- `npm run dev:watch` To trigger the function locally from a http request (eg GET http://localhost:18082/?a=ED)

17
generate-api-client.sh Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/bash
API_DOC_URI="${API_DOC_URI:-https://api.nitrodev.ebitda.tech/domain-ws/q/openapi?format=yaml}"
WD="$(realpath $(dirname $0))"
ROOT_DIR="${WD}"
CLIENT_DIR="${ROOT_DIR}/src/client"
BIN_DIR="${ROOT_DIR}/node_modules/.bin"
rm -rf "${CLIENT_DIR}"
mkdir -p "${CLIENT_DIR}"
${BIN_DIR}/openapi-ts \
-c "@hey-api/client-fetch" \
-i ${API_DOC_URI} \
-o "${CLIENT_DIR}"

6844
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "nitro-example-ts",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"generate-api-client": "sh generate-api-client.sh",
"prestart": "npm run build",
"deploy": "gcloud functions deploy nitroExampleTsHttp --gen2 --runtime=nodejs20 --source=. --trigger-http --allow-unauthenticated",
"deploy:event": "gcloud functions deploy nitroExampleTsEvent --gen2 --runtime=nodejs20 --source=. --trigger-event=google.cloud.storage.object.v1.finalized --trigger-resource=YOUR_BUCKET_NAME",
"clean": "rm -rf dist",
"test": "jest",
"test:watch": "jest --watch",
"dev": "npm run build && functions-framework --target=fieldRequestToFieldValueHttp --port=18082",
"dev:watch": "concurrently \"tsc -w\" \"nodemon --watch dist/ --exec functions-framework --target=nitroExampleTsHttp --port=18082\"",
"dev:event": "npm run build && functions-framework --target=nitroExampleTsEvent --signature-type=event"
},
"main": "dist/index.js",
"dependencies": {
"@google-cloud/functions-framework": "^3.0.0",
"@hey-api/client-fetch": "0.8.4",
"@hey-api/openapi-ts": "0.64.15",
"@google-cloud/storage": "^7.16.0",
"axios": "^1.6.7",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"concurrently": "^8.2.2",
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"ts-jest": "^29.1.2",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20"
},
"files": [
"dist"
]
}

18
src/client/client.gen.ts Normal file
View File

@ -0,0 +1,18 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions>({
baseUrl: 'http://localhost:48000/domain-ws'
}));

3
src/client/index.ts Normal file
View File

@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
export * from './sdk.gen';

25270
src/client/sdk.gen.ts Normal file

File diff suppressed because one or more lines are too long

27496
src/client/types.gen.ts Normal file

File diff suppressed because it is too large Load Diff

38
src/config.ts Normal file
View File

@ -0,0 +1,38 @@
/**
* Configuration for the field-request-to-field-value function
*/
import path from "path";
import * as dotenv from 'dotenv';
import {NitroClientConfig} from "./models/nitro-client-config";
// Load .env file or env variables
dotenv.config({path: path.resolve(__dirname, '../.env')});
// Export global constants
export const DEBUG = process.env.DEBUG === 'true';
export const NITRO_CLIENT_CONFIG: NitroClientConfig = {
clientId: process.env.API_CLIENT_ID,
clientSecret: process.env.API_CLIENT_SECRET,
authUrl: process.env.API_AUTH_URL,
apiUrl: process.env.API_BASE_URL,
}
/**
* Validate the configuration
* @throws Error if any required configuration is missing
*/
export function validateConfig(): void {
if (!NITRO_CLIENT_CONFIG.authUrl) {
throw new Error('API_AUTH_URL environment variable is required');
}
if (!NITRO_CLIENT_CONFIG.apiUrl) {
throw new Error('API_BASE_URL environment variable is required');
}
if (!NITRO_CLIENT_CONFIG.clientId) {
throw new Error('API_CLIENT_ID environment variable is required');
}
if (!NITRO_CLIENT_CONFIG.clientSecret) {
throw new Error('API_CLIENT_SECRET environment variable is required');
}
}

75
src/index.ts Normal file
View File

@ -0,0 +1,75 @@
// Validate configuration on startup
import {validateConfig} from "./config";
import {CloudEvent, cloudEvent, http} from "@google-cloud/functions-framework";
import {FunctionResponse} from "./models/function-response";
import {ExampleFunctionService} from "./service/example-function.service";
import {CloudEventInput} from "./models/cloud-event-input";
try {
validateConfig();
} catch (error) {
console.error('Configuration error:', error instanceof Error ? error.message : String(error));
// Don't throw here to allow the function to start, but it will fail when executed
}
/**
* cloud function HTTP endpoint
*/
http('nitroExampleTsHttp', async (req, res): Promise<void> => {
try {
// Extract request parameters 'a' and 'b' from the query string or body
const queryParamA = req.query.a as string ?? req.body.projectId as string;
const queryParamB = req.query.b as string ?? req.body.projectId as string;
if (!queryParamA) {
res.status(400).json({
success: false,
error: 'Parameter "a" is required'
});
return;
}
// Do something
const service = new ExampleFunctionService();
const result = await service.doSomething(queryParamA, queryParamB);
// Format and return the response
const response: FunctionResponse = {
success: result
};
res.status(200).json(response);
} catch (error) {
console.error('Error processing request:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
error: errorMessage
});
}
});
/**
* Cloud Event handler
*/
cloudEvent('nitroExampleTsEvent', async (event: CloudEvent<CloudEventInput>): Promise<void> => {
try {
console.log('Received event:', event.type);
// Extract request parameters from the event
const {a, b} = event.data || {};
if (!a) {
throw new Error('Parameter "a" is required');
}
// Do something
const service = new ExampleFunctionService();
const result = await service.doSomething(a, b);
console.log('completed successfully');
} catch (error) {
console.error('Error processing event:', error);
throw error;
}
});

View File

@ -0,0 +1,4 @@
export interface CloudEventInput {
a: string;
b?: string;
}

View File

@ -0,0 +1,3 @@
export interface FunctionResponse {
success: boolean;
}

View File

@ -0,0 +1,6 @@
export interface NitroClientConfig {
authUrl: string;
apiUrl: string;
clientId: string;
clientSecret: string;
}

View File

@ -0,0 +1,16 @@
import {NitroClientService} from "./nitro-client.service";
import {NitroAuthService} from "./nitro-auth-service";
import {NITRO_CLIENT_CONFIG} from "../config";
export class ExampleFunctionService {
nitroAuthService: NitroAuthService = new NitroAuthService(NITRO_CLIENT_CONFIG);
nitroClientService: NitroClientService = new NitroClientService(this.nitroAuthService);
async doSomething(a: string, b?: string): Promise<boolean> {
const response = await this.nitroClientService.listCustomers(a, 100);
const resultList = response.data?.itemList ?? []
console.log(resultList)
return resultList.length > 0;
}
}

View File

@ -0,0 +1,74 @@
import axios from "axios";
import {Client, createClient, createConfig, Options} from "@hey-api/client-fetch";
import {NitroClientConfig} from "../models/nitro-client-config";
export class NitroAuthService {
private authUri: string;
private clientId: string;
private clientSecret: string;
private accessToken: string | null = null;
private expiresIn: number = 0;
private obtainedAt: number = 0;
public client: Client;
constructor(
clientConfig: NitroClientConfig
) {
this.authUri = clientConfig.authUrl;
this.clientId = clientConfig.clientId;
this.clientSecret = clientConfig.clientSecret
this.client = createClient(createConfig({
baseUrl: clientConfig.apiUrl,
}))
this.client.interceptors.request.use(async (request: Request, options: Options) => {
const token = await this.getAuthToken();
request.headers.set('Authorization', `Bearer ${token}`);
return request;
})
}
async getAuthToken() {
if (this.checkTokenValid()) {
return this.accessToken;
}
return await this.obtainNewToken();
}
private checkTokenValid(): boolean {
if (!this.accessToken || !this.expiresIn || !this.obtainedAt) {
return false;
}
const now = Date.now();
const expirationTime = this.obtainedAt + (this.expiresIn * 1000); // expiresIn is in seconds
// Add a small buffer (e.g., 60 seconds) to avoid using an expired token
return expirationTime - now > 60 * 1000;
}
private async obtainNewToken(): Promise<string> {
const tokenUrl = `${this.authUri}/protocol/openid-connect/token`;
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('client_id', this.clientId);
params.append('client_secret', this.clientSecret);
try {
const response = await axios.post(tokenUrl, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
this.accessToken = response.data.access_token;
this.expiresIn = response.data.expires_in;
this.obtainedAt = Date.now();
return this.accessToken!;
} catch (error) {
console.error('Error obtaining new token:', error);
throw new Error('Failed to obtain new token');
}
}
}

View File

@ -0,0 +1,22 @@
import {NitroAuthService} from "./nitro-auth-service";
import {postCurrencySearch} from "../client/index";
export class NitroClientService {
private authService: NitroAuthService;
constructor(authService: NitroAuthService) {
this.authService = authService;
}
async listCustomers(query: string, length: number) {
return postCurrencySearch({
client: this.authService.client,
body: {
nameContains: query
},
query: {
length: length
}
})
}
}

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}