From b3ddab2bf4567137e3dba39bb36544c69c581fb3 Mon Sep 17 00:00:00 2001 From: Amiel Christian Mala-ay Date: Wed, 12 Feb 2025 12:16:24 +0800 Subject: [PATCH 1/3] Add Netlify example --- examples/with-netlify/.gitignore | 4 + examples/with-netlify/icon.png | Bin 0 -> 3874 bytes examples/with-netlify/netlify.toml | 12 +++ examples/with-netlify/package.json | 19 ++++ examples/with-netlify/src/handler/handler.mts | 28 +++++ examples/with-netlify/src/routes/hooks.ts | 56 ++++++++++ examples/with-netlify/src/routes/pages.ts | 39 +++++++ examples/with-netlify/src/routes/tests.ts | 81 ++++++++++++++ examples/with-netlify/src/routes/user.ts | 102 ++++++++++++++++++ examples/with-netlify/tsconfig.json | 19 ++++ 10 files changed, 360 insertions(+) create mode 100644 examples/with-netlify/.gitignore create mode 100644 examples/with-netlify/icon.png create mode 100644 examples/with-netlify/netlify.toml create mode 100644 examples/with-netlify/package.json create mode 100755 examples/with-netlify/src/handler/handler.mts create mode 100644 examples/with-netlify/src/routes/hooks.ts create mode 100644 examples/with-netlify/src/routes/pages.ts create mode 100644 examples/with-netlify/src/routes/tests.ts create mode 100644 examples/with-netlify/src/routes/user.ts create mode 100644 examples/with-netlify/tsconfig.json diff --git a/examples/with-netlify/.gitignore b/examples/with-netlify/.gitignore new file mode 100644 index 0000000..d1d51a4 --- /dev/null +++ b/examples/with-netlify/.gitignore @@ -0,0 +1,4 @@ +# Local Netlify folder +.netlify +yarn.lock +node_modules \ No newline at end of file diff --git a/examples/with-netlify/icon.png b/examples/with-netlify/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a6b9fd3708548111e2e8f163ccbbe4c08c62c2c4 GIT binary patch literal 3874 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 literal 0 HcmV?d00001 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..68297a2 --- /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..473e0b0 --- /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..8e7c590 --- /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..ccc846e --- /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"] +} From 4f188121a1a9215ad2cdc03dd55f28ef7e395fcf Mon Sep 17 00:00:00 2001 From: Amiel Christian Mala-ay Date: Wed, 12 Feb 2025 17:24:56 +0800 Subject: [PATCH 2/3] Replaced single quotes with double quotes --- examples/with-netlify/src/handler/handler.mts | 10 +++++----- examples/with-netlify/src/routes/hooks.ts | 4 ++-- examples/with-netlify/src/routes/pages.ts | 8 ++++---- examples/with-netlify/src/routes/tests.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/with-netlify/src/handler/handler.mts b/examples/with-netlify/src/handler/handler.mts index 68297a2..40dc25a 100755 --- a/examples/with-netlify/src/handler/handler.mts +++ b/examples/with-netlify/src/handler/handler.mts @@ -1,8 +1,8 @@ -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"; +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(); diff --git a/examples/with-netlify/src/routes/hooks.ts b/examples/with-netlify/src/routes/hooks.ts index 473e0b0..b862a85 100644 --- a/examples/with-netlify/src/routes/hooks.ts +++ b/examples/with-netlify/src/routes/hooks.ts @@ -44,8 +44,8 @@ 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, ">"); + const method = log.method.replace(//g, '>'); + const file = log.file.replace(//g, '>'); return `#${i + 1} ${method} - ${file}:${line}:${char}`; }) || []; html.push(`
    ${stack.join('

    ')}
    `); diff --git a/examples/with-netlify/src/routes/pages.ts b/examples/with-netlify/src/routes/pages.ts index 8e7c590..582c94e 100644 --- a/examples/with-netlify/src/routes/pages.ts +++ b/examples/with-netlify/src/routes/pages.ts @@ -1,4 +1,4 @@ -import { router } from "@stackpress/ingest/fetch"; +import { router } from '@stackpress/ingest/fetch'; const template = ` @@ -24,14 +24,14 @@ const route = router(); /** * Home page */ -route.get("/", function HomePage(req, res) { - res.setHTML("

    Hello, World!

    from Netlify

    "); +route.get('/', function HomePage(req, res) { + res.setHTML('

    Hello, World!

    from Netlify

    '); }); /** * Login page */ -route.get("/login", function Login(req, res) { +route.get('/login', function Login(req, res) { //send the response res.setHTML(template.trim()); }); diff --git a/examples/with-netlify/src/routes/tests.ts b/examples/with-netlify/src/routes/tests.ts index ccc846e..7393637 100644 --- a/examples/with-netlify/src/routes/tests.ts +++ b/examples/with-netlify/src/routes/tests.ts @@ -63,7 +63,7 @@ route.get('/__sse__', function SSE(req, res) { .set('Access-Control-Allow-Origin', '*'); let timerId: any; - const msg = new TextEncoder().encode("data: hello\r\n\r\n"); + const msg = new TextEncoder().encode('data: hello\r\n\r\n'); res.setBody('text/event-stream', new ReadableStream({ start(controller) { timerId = setInterval(() => { From ef9c868bb086df866b2a2bc64ecd3e800d4ff342 Mon Sep 17 00:00:00 2001 From: Amiel Christian Mala-ay Date: Wed, 12 Feb 2025 17:25:15 +0800 Subject: [PATCH 3/3] Removed .gitignore from with-netlify --- examples/with-netlify/.gitignore | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 examples/with-netlify/.gitignore diff --git a/examples/with-netlify/.gitignore b/examples/with-netlify/.gitignore deleted file mode 100644 index d1d51a4..0000000 --- a/examples/with-netlify/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Local Netlify folder -.netlify -yarn.lock -node_modules \ No newline at end of file