diff --git a/examples/with-netlify/icon.png b/examples/with-netlify/icon.png new file mode 100644 index 0000000..a6b9fd3 Binary files /dev/null and b/examples/with-netlify/icon.png differ diff --git a/examples/with-netlify/netlify.toml b/examples/with-netlify/netlify.toml new file mode 100644 index 0000000..df5b7d3 --- /dev/null +++ b/examples/with-netlify/netlify.toml @@ -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 \ No newline at end of file diff --git a/examples/with-netlify/package.json b/examples/with-netlify/package.json new file mode 100644 index 0000000..91a66a9 --- /dev/null +++ b/examples/with-netlify/package.json @@ -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" + } +} diff --git a/examples/with-netlify/src/handler/handler.mts b/examples/with-netlify/src/handler/handler.mts new file mode 100755 index 0000000..40dc25a --- /dev/null +++ b/examples/with-netlify/src/handler/handler.mts @@ -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, + }; +} diff --git a/examples/with-netlify/src/routes/hooks.ts b/examples/with-netlify/src/routes/hooks.ts new file mode 100644 index 0000000..b862a85 --- /dev/null +++ b/examples/with-netlify/src/routes/hooks.ts @@ -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 = [ `

${res.error}

` ]; + const stack = res.stack?.map((log, i) => { + const { line, char } = log; + const method = log.method.replace(//g, '>'); + const file = log.file.replace(//g, '>'); + return `#${i + 1} ${method} - ${file}:${line}:${char}`; + }) || []; + html.push(`
${stack.join('

')}
`); + + res.setHTML(html.join('
')); +}); + +export default route; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/pages.ts b/examples/with-netlify/src/routes/pages.ts new file mode 100644 index 0000000..582c94e --- /dev/null +++ b/examples/with-netlify/src/routes/pages.ts @@ -0,0 +1,39 @@ +import { router } from '@stackpress/ingest/fetch'; + +const template = ` + + + + Login + + +

Login

+
+ + + + + +
+ + +`; + +const route = router(); + +/** + * Home page + */ +route.get('/', function HomePage(req, res) { + res.setHTML('

Hello, World!

from Netlify

'); +}); + +/** + * Login page + */ +route.get('/login', function Login(req, res) { + //send the response + res.setHTML(template.trim()); +}); + +export default route; diff --git a/examples/with-netlify/src/routes/tests.ts b/examples/with-netlify/src/routes/tests.ts new file mode 100644 index 0000000..7393637 --- /dev/null +++ b/examples/with-netlify/src/routes/tests.ts @@ -0,0 +1,81 @@ +import fs from 'fs'; +import path from 'path'; + +import { router } from '@stackpress/ingest/fetch'; + +const template = ` + + + + SSE + + + + + + +`; + +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; \ No newline at end of file diff --git a/examples/with-netlify/src/routes/user.ts b/examples/with-netlify/src/routes/user.ts new file mode 100644 index 0000000..149fbd6 --- /dev/null +++ b/examples/with-netlify/src/routes/user.ts @@ -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>('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; \ No newline at end of file diff --git a/examples/with-netlify/tsconfig.json b/examples/with-netlify/tsconfig.json new file mode 100644 index 0000000..aff717f --- /dev/null +++ b/examples/with-netlify/tsconfig.json @@ -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"] +}