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
Binary file added examples/with-netlify/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/with-netlify/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[functions]
external_node_modules = ["@stackpress/ingest"]

[build]
command = "yarn build"
environment = { NODE_VERSION = "20" }
functions = "src"

[[redirects]]
from = "/*"
to = "/.netlify/functions/handler"
status = 200
19 changes: 19 additions & 0 deletions examples/with-netlify/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "ingest-with-fetch",
"version": "1.0.0",
"description": "A simple boilerplate for using Ingest with fetch API.",
"private": true,
"scripts": {
"build": "tsc",
"dev": "ts-node src/server.ts"
},
"dependencies": {
"@netlify/functions": "^3.0.0",
"@stackpress/ingest": "0.3.27"
},
"devDependencies": {
"@types/node": "^22.13.1",
"ts-node": "10.9.2",
"typescript": "^4.9.5"
}
}
28 changes: 28 additions & 0 deletions examples/with-netlify/src/handler/handler.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { server } from '@stackpress/ingest/fetch';
import pages from '../routes/pages';
import user from '../routes/user';
import tests from '../routes/tests';
import hooks from '../routes/hooks';

export async function handler(event: any, context: any) {
const app = server();
await app.bootstrap();

app.use(pages).use(user).use(hooks).use(tests);

const request = new Request(event.rawUrl, {
method: event.httpMethod,
headers: event.headers,
});

const response = await app.handle(request, undefined);

return {
statusCode: response?.status,
headers: response?.headers
? Object.fromEntries(response.headers.entries())
: {},
body: await response?.text(),
isBase64Encoded: false,
};
}
56 changes: 56 additions & 0 deletions examples/with-netlify/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-netlify/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('<h1>Hello, World!</h1><p>from Netlify</p>');
});

/**
* 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-netlify/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-netlify/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;
19 changes: 19 additions & 0 deletions examples/with-netlify/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"lib": ["es2021", "es7", "es6", "dom"],
"module": "commonjs",
"noUnusedLocals": true,
"outDir": "./dist/",
"preserveConstEnums": true,
"resolveJsonModule": true,
"removeComments": true,
"sourceMap": false,
"strict": true,
"target": "es6",
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/server.mts"],
"exclude": ["dist", "node_modules"]
}