Typescript cloud function example
This commit is contained in:
commit
b02000ed60
11
.env.example
Normal file
11
.env.example
Normal 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
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
coverage/
|
||||
.idea/
|
||||
**/*.iml
|
||||
5
README.md
Normal file
5
README.md
Normal 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
17
generate-api-client.sh
Normal 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
6844
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
Normal file
42
package.json
Normal 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
18
src/client/client.gen.ts
Normal 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
3
src/client/index.ts
Normal 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
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
27496
src/client/types.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
38
src/config.ts
Normal file
38
src/config.ts
Normal 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
75
src/index.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
4
src/models/cloud-event-input.ts
Normal file
4
src/models/cloud-event-input.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface CloudEventInput {
|
||||
a: string;
|
||||
b?: string;
|
||||
}
|
||||
3
src/models/function-response.ts
Normal file
3
src/models/function-response.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface FunctionResponse {
|
||||
success: boolean;
|
||||
}
|
||||
6
src/models/nitro-client-config.ts
Normal file
6
src/models/nitro-client-config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface NitroClientConfig {
|
||||
authUrl: string;
|
||||
apiUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
16
src/service/example-function.service.ts
Normal file
16
src/service/example-function.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
74
src/service/nitro-auth-service.ts
Normal file
74
src/service/nitro-auth-service.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/service/nitro-client.service.ts
Normal file
22
src/service/nitro-client.service.ts
Normal 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
18
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user