Skip to content
Merged
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
16 changes: 16 additions & 0 deletions examples/with-gcp/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

node_modules
42 changes: 42 additions & 0 deletions examples/with-gcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# GCP Function Example

This is an example of GCP function using Typescript and Yarn.

## Prerequisites:

- Download Google Cloud SDK
- Initialize gcloud config ($ gcloud init)
- Log in to gcloud account ($ gcloud auth login)

## Running it locally

At the first time running the project run the command:

$ yarn

Then you can build and start the local dev:

$ yarn gcp:build
$ yarn gcp:dev

Once the project is running check out http://localhost:8081.

## Deploying to GCP

Deploy the GCP function (cd examples/with-gcp)

$ gcloud functions deploy gcp-function \
--entry-point=handler \
--runtime nodejs20 \
--trigger-http \
--no-allow-unauthenticated \
--project [PROJECT ID]

## Authenticate for invocation

Since authentication is required, clicking the URL will return Error: Forbidden. Execute this line:

$ curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
[FUNCTION URL]

more info: https://cloud.google.com/functions/docs/securing/authenticating
Binary file added examples/with-gcp/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions examples/with-gcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "ingest-with-gcp",
"version": "1.0.0",
"description": "A simple boilerplate for using Ingest with GCP functions.",
"main": "dist/handler.js",
"private": true,
"scripts": {
"build": "tsc",
"dev": "functions-framework --target=handler --port=8081"
},
"dependencies": {
"@google-cloud/functions-framework": "^3.4.5",
"@stackpress/ingest": "0.3.28"
},
"devDependencies": {
"@types/node": "22.9.3",
"ts-node": "10.9.2",
"typescript": "5.7.2"
}
}
26 changes: 26 additions & 0 deletions examples/with-gcp/src/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { server } from '@stackpress/ingest/fetch';
import { HttpFunction } from '@google-cloud/functions-framework';

import pages from './routes/pages';
import user from './routes/user';
import tests from './routes/tests';
import hooks from './routes/hooks';

// Create the server
const app = server();
app.use(pages).use(user).use(hooks).use(tests);

// GCP Cloud Function Handler
export const handler: HttpFunction = async (req: any, res: any) => {
res.status(200).send('<h1>Hello, World</h1><p>from GCP Cloud Function!</p>');
};

// // Local Server
// app.create().listen(4000, () => {
// console.log('Server is running on port 4000');
// console.log('------------------------------');
// });




56 changes: 56 additions & 0 deletions examples/with-gcp/src/routes/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ResponseStatus } from '@stackpress/lib/dist/types';
import { getStatus } from '@stackpress/lib/dist/Status';
import { Exception } from '@stackpress/ingest';
import { router } from '@stackpress/ingest/fetch';

const route = router();

/**
* Error handlers
*/
route.get('/catch', function ErrorResponse(req, res) {
try {
throw Exception.for('Not implemented');
} catch (e) {
const error = e as Exception;
const status = getStatus(error.code) as ResponseStatus;
res.setError({
code: status.code,
status: status.status,
error: error.message,
stack: error.trace()
});
}
});

/**
* Error handlers
*/
route.get('/error', function ErrorResponse(req, res) {
throw Exception.for('Not implemented');
});

/**
* 404 handler
*/
route.get('/**', function NotFound(req, res) {
if (!res.code && !res.status && !res.sent) {
//send the response
res.setHTML('Not Found');
}
});

route.on('error', function Error(req, res) {
const html = [ `<h1>${res.error}</h1>` ];
const stack = res.stack?.map((log, i) => {
const { line, char } = log;
const method = log.method.replace(/</g, "&lt;").replace(/>/g, "&gt;");
const file = log.file.replace(/</g, "&lt;").replace(/>/g, "&gt;");
return `#${i + 1} ${method} - ${file}:${line}:${char}`;
}) || [];
html.push(`<pre>${stack.join('<br><br>')}</pre>`);

res.setHTML(html.join('<br>'));
});

export default route;
39 changes: 39 additions & 0 deletions examples/with-gcp/src/routes/pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { router } from '@stackpress/ingest/fetch';

const template = `
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form action="/user/login" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Login</button>
</form>
</body>
</html>
`;

const route = router();

/**
* Home page
*/
route.get('/', function HomePage(req, res) {
res.setHTML('Hello, World');
});

/**
* Login page
*/
route.get('/login', function Login(req, res) {
//send the response
res.setHTML(template.trim());
});

export default route;
81 changes: 81 additions & 0 deletions examples/with-gcp/src/routes/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import fs from 'fs';
import path from 'path';

import { router } from '@stackpress/ingest/fetch';

const template = `
<!DOCTYPE html>
<html>
<head>
<title>SSE</title>
</head>
<body>
<ul></ul>
<script>
const ul = document.querySelector('ul');
const evtSource = new EventSource('/__sse__');
evtSource.onmessage = (event) => {
const li = document.createElement('li');
li.textContent = event.data;
ul.appendChild(li);
};
</script>
</body>
</html>
`;

const route = router();

/**
* Redirect test
*/
route.get('/redirect', function Redirect(req, res) {
res.redirect('/user');
});

/**
* Static file test
*/
route.get('/icon.png', function Icon(req, res) {
if (res.code || res.status || res.body) return;
const file = path.resolve(process.cwd(), 'icon.png');
if (fs.existsSync(file)) {
res.setBody('image/png', fs.createReadStream(file));
}
});

/**
* Stream template for SSE test
*/
route.get('/stream', function Stream(req, res) {
//send the response
res.setHTML(template.trim());
});

/**
* SSE test
*/
route.get('/__sse__', function SSE(req, res) {
res.headers
.set('Cache-Control', 'no-cache')
.set('Content-Encoding', 'none')
.set('Connection', 'keep-alive')
.set('Access-Control-Allow-Origin', '*');

let timerId: any;
const msg = new TextEncoder().encode("data: hello\r\n\r\n");
res.setBody('text/event-stream', new ReadableStream({
start(controller) {
timerId = setInterval(() => {
controller.enqueue(msg);
}, 2500);
},
cancel() {
if (typeof timerId === 'number') {
clearInterval(timerId);
}
},
}));
});

export default route;
102 changes: 102 additions & 0 deletions examples/with-gcp/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { router } from '@stackpress/ingest/fetch';

const route = router();

let id = 0;

/**
* Example user API search
*/
route.get('/user', function UserSearch(req, res) {
//get filters
//const filters = req.query.get<Record<string, unknown>>('filter');
//maybe get from database?
const results = [
{
id: 1,
name: 'John Doe',
age: 21,
created: new Date().toISOString()
},
{
id: 2,
name: 'Jane Doe',
age: 30,
created: new Date().toISOString()
}
];
//send the response
res.setRows(results, 100);
});

/**
* Example user API create (POST)
* Need to use Postman to see this...
*/
route.post('/user', function UserCreate(req, res) {
//get form body
const form = req.data();
//maybe insert into database?
const results = { ...form, id: ++id, created: new Date().toISOString() };
//send the response
res.setResults(results);
});

/**
* Example user API detail
*/
route.get('/user/:id', function UserDetail(req, res) {
//get params
const id = parseInt(req.data('id') || '');
if (!id) {
res.setError('ID is required');
return;
}
//maybe get from database?
const results = {
id: id,
name: 'John Doe',
age: 21,
created: new Date().toISOString()
};
//send the response
res.setResults(results);
});
route.put('/user/:id', function UserUpdate(req, res) {
//get params
const id = parseInt(req.data('id') || '');
if (!id) {
res.setError('ID is required');
return;
}
//get form body
const form = req.post();
//maybe insert into database?
const results = { ...form, id, created: new Date().toISOString() };
//send the response
res.setResults(results);
});

/**
* Example user API delete (DELETE)
* Need to use Postman to see this...
*/
route.delete('/user/:id', function UserRemove(req, res) {
//get params
const id = parseInt(req.data('id') || '');
if (!id) {
res.setError('ID is required');
return;
}
//maybe get from database?
const results = {
id: 1,
name: 'John Doe',
age: 21,
created: new Date().toISOString()
};
//send the response
res.setResults(results);
});

export default route;
Loading