Skip to content

Commit a9b29e5

Browse files
authored
Merge pull request #10 from patlouis/main
Add GCP function example
2 parents b452b4e + ffa6ad9 commit a9b29e5

12 files changed

Lines changed: 1315 additions & 13 deletions

File tree

examples/with-gcp/.gcloudignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
# If you would like to upload your .git directory, .gitignore file or files
11+
# from your .gitignore file, remove the corresponding line
12+
# below:
13+
.git
14+
.gitignore
15+
16+
node_modules

examples/with-gcp/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# GCP Function Example
2+
3+
This is an example of GCP function using Typescript and Yarn.
4+
5+
## Prerequisites:
6+
7+
- Download Google Cloud SDK
8+
- Initialize gcloud config ($ gcloud init)
9+
- Log in to gcloud account ($ gcloud auth login)
10+
11+
## Running it locally
12+
13+
At the first time running the project run the command:
14+
15+
$ yarn
16+
17+
Then you can build and start the local dev:
18+
19+
$ yarn gcp:build
20+
$ yarn gcp:dev
21+
22+
Once the project is running check out http://localhost:8081.
23+
24+
## Deploying to GCP
25+
26+
Deploy the GCP function (cd examples/with-gcp)
27+
28+
$ gcloud functions deploy gcp-function \
29+
--entry-point=handler \
30+
--runtime nodejs20 \
31+
--trigger-http \
32+
--no-allow-unauthenticated \
33+
--project [PROJECT ID]
34+
35+
## Authenticate for invocation
36+
37+
Since authentication is required, clicking the URL will return Error: Forbidden. Execute this line:
38+
39+
$ curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
40+
[FUNCTION URL]
41+
42+
more info: https://cloud.google.com/functions/docs/securing/authenticating

examples/with-gcp/icon.png

3.78 KB
Loading

examples/with-gcp/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "ingest-with-gcp",
3+
"version": "1.0.0",
4+
"description": "A simple boilerplate for using Ingest with GCP functions.",
5+
"main": "dist/handler.js",
6+
"private": true,
7+
"scripts": {
8+
"build": "tsc",
9+
"dev": "functions-framework --target=handler --port=8081"
10+
},
11+
"dependencies": {
12+
"@google-cloud/functions-framework": "^3.4.5",
13+
"@stackpress/ingest": "0.3.28"
14+
},
15+
"devDependencies": {
16+
"@types/node": "22.9.3",
17+
"ts-node": "10.9.2",
18+
"typescript": "5.7.2"
19+
}
20+
}

examples/with-gcp/src/handler.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { server } from '@stackpress/ingest/fetch';
2+
import { HttpFunction } from '@google-cloud/functions-framework';
3+
4+
import pages from './routes/pages';
5+
import user from './routes/user';
6+
import tests from './routes/tests';
7+
import hooks from './routes/hooks';
8+
9+
// Create the server
10+
const app = server();
11+
app.use(pages).use(user).use(hooks).use(tests);
12+
13+
// GCP Cloud Function Handler
14+
export const handler: HttpFunction = async (req: any, res: any) => {
15+
res.status(200).send('<h1>Hello, World</h1><p>from GCP Cloud Function!</p>');
16+
};
17+
18+
// // Local Server
19+
// app.create().listen(4000, () => {
20+
// console.log('Server is running on port 4000');
21+
// console.log('------------------------------');
22+
// });
23+
24+
25+
26+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { ResponseStatus } from '@stackpress/lib/dist/types';
2+
import { getStatus } from '@stackpress/lib/dist/Status';
3+
import { Exception } from '@stackpress/ingest';
4+
import { router } from '@stackpress/ingest/fetch';
5+
6+
const route = router();
7+
8+
/**
9+
* Error handlers
10+
*/
11+
route.get('/catch', function ErrorResponse(req, res) {
12+
try {
13+
throw Exception.for('Not implemented');
14+
} catch (e) {
15+
const error = e as Exception;
16+
const status = getStatus(error.code) as ResponseStatus;
17+
res.setError({
18+
code: status.code,
19+
status: status.status,
20+
error: error.message,
21+
stack: error.trace()
22+
});
23+
}
24+
});
25+
26+
/**
27+
* Error handlers
28+
*/
29+
route.get('/error', function ErrorResponse(req, res) {
30+
throw Exception.for('Not implemented');
31+
});
32+
33+
/**
34+
* 404 handler
35+
*/
36+
route.get('/**', function NotFound(req, res) {
37+
if (!res.code && !res.status && !res.sent) {
38+
//send the response
39+
res.setHTML('Not Found');
40+
}
41+
});
42+
43+
route.on('error', function Error(req, res) {
44+
const html = [ `<h1>${res.error}</h1>` ];
45+
const stack = res.stack?.map((log, i) => {
46+
const { line, char } = log;
47+
const method = log.method.replace(/</g, "&lt;").replace(/>/g, "&gt;");
48+
const file = log.file.replace(/</g, "&lt;").replace(/>/g, "&gt;");
49+
return `#${i + 1} ${method} - ${file}:${line}:${char}`;
50+
}) || [];
51+
html.push(`<pre>${stack.join('<br><br>')}</pre>`);
52+
53+
res.setHTML(html.join('<br>'));
54+
});
55+
56+
export default route;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { router } from '@stackpress/ingest/fetch';
2+
3+
const template = `
4+
<!DOCTYPE html>
5+
<html>
6+
<head>
7+
<title>Login</title>
8+
</head>
9+
<body>
10+
<h1>Login</h1>
11+
<form action="/user/login" method="POST">
12+
<label for="username">Username:</label>
13+
<input type="text" id="username" name="username" required>
14+
<label for="password">Password:</label>
15+
<input type="password" id="password" name="password" required>
16+
<button type="submit">Login</button>
17+
</form>
18+
</body>
19+
</html>
20+
`;
21+
22+
const route = router();
23+
24+
/**
25+
* Home page
26+
*/
27+
route.get('/', function HomePage(req, res) {
28+
res.setHTML('Hello, World');
29+
});
30+
31+
/**
32+
* Login page
33+
*/
34+
route.get('/login', function Login(req, res) {
35+
//send the response
36+
res.setHTML(template.trim());
37+
});
38+
39+
export default route;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
import { router } from '@stackpress/ingest/fetch';
5+
6+
const template = `
7+
<!DOCTYPE html>
8+
<html>
9+
<head>
10+
<title>SSE</title>
11+
</head>
12+
<body>
13+
<ul></ul>
14+
<script>
15+
const ul = document.querySelector('ul');
16+
const evtSource = new EventSource('/__sse__');
17+
evtSource.onmessage = (event) => {
18+
const li = document.createElement('li');
19+
li.textContent = event.data;
20+
ul.appendChild(li);
21+
};
22+
</script>
23+
</body>
24+
</html>
25+
`;
26+
27+
const route = router();
28+
29+
/**
30+
* Redirect test
31+
*/
32+
route.get('/redirect', function Redirect(req, res) {
33+
res.redirect('/user');
34+
});
35+
36+
/**
37+
* Static file test
38+
*/
39+
route.get('/icon.png', function Icon(req, res) {
40+
if (res.code || res.status || res.body) return;
41+
const file = path.resolve(process.cwd(), 'icon.png');
42+
if (fs.existsSync(file)) {
43+
res.setBody('image/png', fs.createReadStream(file));
44+
}
45+
});
46+
47+
/**
48+
* Stream template for SSE test
49+
*/
50+
route.get('/stream', function Stream(req, res) {
51+
//send the response
52+
res.setHTML(template.trim());
53+
});
54+
55+
/**
56+
* SSE test
57+
*/
58+
route.get('/__sse__', function SSE(req, res) {
59+
res.headers
60+
.set('Cache-Control', 'no-cache')
61+
.set('Content-Encoding', 'none')
62+
.set('Connection', 'keep-alive')
63+
.set('Access-Control-Allow-Origin', '*');
64+
65+
let timerId: any;
66+
const msg = new TextEncoder().encode("data: hello\r\n\r\n");
67+
res.setBody('text/event-stream', new ReadableStream({
68+
start(controller) {
69+
timerId = setInterval(() => {
70+
controller.enqueue(msg);
71+
}, 2500);
72+
},
73+
cancel() {
74+
if (typeof timerId === 'number') {
75+
clearInterval(timerId);
76+
}
77+
},
78+
}));
79+
});
80+
81+
export default route;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { router } from '@stackpress/ingest/fetch';
2+
3+
const route = router();
4+
5+
let id = 0;
6+
7+
/**
8+
* Example user API search
9+
*/
10+
route.get('/user', function UserSearch(req, res) {
11+
//get filters
12+
//const filters = req.query.get<Record<string, unknown>>('filter');
13+
//maybe get from database?
14+
const results = [
15+
{
16+
id: 1,
17+
name: 'John Doe',
18+
age: 21,
19+
created: new Date().toISOString()
20+
},
21+
{
22+
id: 2,
23+
name: 'Jane Doe',
24+
age: 30,
25+
created: new Date().toISOString()
26+
}
27+
];
28+
//send the response
29+
res.setRows(results, 100);
30+
});
31+
32+
/**
33+
* Example user API create (POST)
34+
* Need to use Postman to see this...
35+
*/
36+
route.post('/user', function UserCreate(req, res) {
37+
//get form body
38+
const form = req.data();
39+
//maybe insert into database?
40+
const results = { ...form, id: ++id, created: new Date().toISOString() };
41+
//send the response
42+
res.setResults(results);
43+
});
44+
45+
/**
46+
* Example user API detail
47+
*/
48+
route.get('/user/:id', function UserDetail(req, res) {
49+
//get params
50+
const id = parseInt(req.data('id') || '');
51+
if (!id) {
52+
res.setError('ID is required');
53+
return;
54+
}
55+
//maybe get from database?
56+
const results = {
57+
id: id,
58+
name: 'John Doe',
59+
age: 21,
60+
created: new Date().toISOString()
61+
};
62+
//send the response
63+
res.setResults(results);
64+
});
65+
route.put('/user/:id', function UserUpdate(req, res) {
66+
//get params
67+
const id = parseInt(req.data('id') || '');
68+
if (!id) {
69+
res.setError('ID is required');
70+
return;
71+
}
72+
//get form body
73+
const form = req.post();
74+
//maybe insert into database?
75+
const results = { ...form, id, created: new Date().toISOString() };
76+
//send the response
77+
res.setResults(results);
78+
});
79+
80+
/**
81+
* Example user API delete (DELETE)
82+
* Need to use Postman to see this...
83+
*/
84+
route.delete('/user/:id', function UserRemove(req, res) {
85+
//get params
86+
const id = parseInt(req.data('id') || '');
87+
if (!id) {
88+
res.setError('ID is required');
89+
return;
90+
}
91+
//maybe get from database?
92+
const results = {
93+
id: 1,
94+
name: 'John Doe',
95+
age: 21,
96+
created: new Date().toISOString()
97+
};
98+
//send the response
99+
res.setResults(results);
100+
});
101+
102+
export default route;

0 commit comments

Comments
 (0)