zero-http 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +28 -177
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { Database } = require('../..');
|
|
3
|
+
const Task = require('../models/Task');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Connect to the database, register all models, and sync tables.
|
|
7
|
+
* Uses SQLite with a Database/ folder for file‑based persistence.
|
|
8
|
+
* Returns the Database instance for reuse.
|
|
9
|
+
*/
|
|
10
|
+
function initDatabase()
|
|
11
|
+
{
|
|
12
|
+
const db = Database.connect('sqlite', {
|
|
13
|
+
filename: path.join(__dirname, '..', 'Database', 'tasks.db'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Register models
|
|
17
|
+
db.register(Task);
|
|
18
|
+
|
|
19
|
+
// Sync tables (non-blocking)
|
|
20
|
+
db.sync().then(() => console.log('ORM: SQLite tasks.db ready'));
|
|
21
|
+
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { initDatabase };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const {
|
|
3
|
+
cors, json, urlencoded, text,
|
|
4
|
+
static: serveStatic, logger, compress,
|
|
5
|
+
helmet, timeout, requestId, cookieParser
|
|
6
|
+
} = require('../..');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register the standard middleware stack on the app.
|
|
10
|
+
* Order matters — security & utility first, then parsers, then static.
|
|
11
|
+
*/
|
|
12
|
+
function applyMiddleware(app)
|
|
13
|
+
{
|
|
14
|
+
app.use(logger({ format: 'dev' }));
|
|
15
|
+
app.use(requestId());
|
|
16
|
+
app.use(helmet({
|
|
17
|
+
contentSecurityPolicy: {
|
|
18
|
+
directives: {
|
|
19
|
+
defaultSrc: ["'self'"],
|
|
20
|
+
baseUri: ["'self'"],
|
|
21
|
+
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
22
|
+
scriptSrcAttr: ["'none'"],
|
|
23
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
24
|
+
connectSrc: ["'self'", 'wss:', 'ws:'],
|
|
25
|
+
imgSrc: ["'self'", 'data:'],
|
|
26
|
+
fontSrc: ["'self'", 'https:', 'data:'],
|
|
27
|
+
formAction: ["'self'"],
|
|
28
|
+
frameAncestors: ["'self'"],
|
|
29
|
+
objectSrc: ["'none'"],
|
|
30
|
+
upgradeInsecureRequests: []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
app.use(cors());
|
|
35
|
+
app.use(compress());
|
|
36
|
+
app.use(timeout(30000));
|
|
37
|
+
app.use(cookieParser());
|
|
38
|
+
app.use(json());
|
|
39
|
+
app.use(urlencoded());
|
|
40
|
+
app.use(text());
|
|
41
|
+
app.use(serveStatic(path.join(__dirname, '..', 'public')));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { applyMiddleware };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const CERT_PATH = '/www/server/panel/vhost/cert/z-http.com/fullchain.pem';
|
|
4
|
+
const KEY_PATH = '/www/server/panel/vhost/cert/z-http.com/privkey.pem';
|
|
5
|
+
|
|
6
|
+
const hasCerts = fs.existsSync(CERT_PATH) && fs.existsSync(KEY_PATH);
|
|
7
|
+
|
|
8
|
+
const tlsOpts = hasCerts
|
|
9
|
+
? { cert: fs.readFileSync(CERT_PATH), key: fs.readFileSync(KEY_PATH) }
|
|
10
|
+
: undefined;
|
|
11
|
+
|
|
12
|
+
module.exports = { hasCerts, tlsOpts };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
exports.list = (req, res) =>
|
|
2
|
+
{
|
|
3
|
+
res.json({
|
|
4
|
+
cookies: req.cookies || {},
|
|
5
|
+
signedCookies: req.signedCookies || {},
|
|
6
|
+
});
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
exports.set = (req, res) =>
|
|
10
|
+
{
|
|
11
|
+
const { name, value, options } = req.body || {};
|
|
12
|
+
if (!name || typeof name !== 'string')
|
|
13
|
+
return res.status(400).json({ error: 'name is required' });
|
|
14
|
+
|
|
15
|
+
const val = value != null ? value : '';
|
|
16
|
+
const opts = {};
|
|
17
|
+
if (options)
|
|
18
|
+
{
|
|
19
|
+
if (options.maxAge != null) opts.maxAge = Number(options.maxAge);
|
|
20
|
+
if (options.httpOnly != null) opts.httpOnly = Boolean(options.httpOnly);
|
|
21
|
+
if (options.secure != null) opts.secure = Boolean(options.secure);
|
|
22
|
+
if (options.sameSite) opts.sameSite = String(options.sameSite);
|
|
23
|
+
if (options.path) opts.path = String(options.path);
|
|
24
|
+
if (options.signed) opts.signed = true;
|
|
25
|
+
}
|
|
26
|
+
res.cookie(name, typeof val === 'object' ? val : String(val), opts);
|
|
27
|
+
res.json({ set: name, value: val, options: opts });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.clear = (req, res) =>
|
|
31
|
+
{
|
|
32
|
+
res.clearCookie(req.params.name);
|
|
33
|
+
res.json({ cleared: req.params.name });
|
|
34
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const Task = require('../models/Task');
|
|
2
|
+
|
|
3
|
+
const VALID_STATUSES = ['pending', 'in-progress', 'done'];
|
|
4
|
+
|
|
5
|
+
function clampPriority(v) { return Math.max(0, Math.min(5, Number(v) || 0)); }
|
|
6
|
+
|
|
7
|
+
exports.list = async (req, res) =>
|
|
8
|
+
{
|
|
9
|
+
try
|
|
10
|
+
{
|
|
11
|
+
let q = Task.query();
|
|
12
|
+
if (req.query.status) q = q.where('status', '=', req.query.status);
|
|
13
|
+
if (req.query.priority) q = q.where('priority', '>=', Number(req.query.priority));
|
|
14
|
+
if (req.query.scope) q = q.scope(req.query.scope);
|
|
15
|
+
if (req.query.search) q = q.whereLike('title', `%${req.query.search}%`);
|
|
16
|
+
q = q.orderBy(req.query.sort || 'createdAt', req.query.order || 'desc');
|
|
17
|
+
const tasks = await q.exec();
|
|
18
|
+
const count = await Task.count();
|
|
19
|
+
res.json({ tasks, total: count });
|
|
20
|
+
}
|
|
21
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
exports.create = async (req, res) =>
|
|
25
|
+
{
|
|
26
|
+
try
|
|
27
|
+
{
|
|
28
|
+
const { title, status, priority } = req.body || {};
|
|
29
|
+
if (!title || typeof title !== 'string' || !title.trim())
|
|
30
|
+
return res.status(400).json({ error: 'title is required' });
|
|
31
|
+
|
|
32
|
+
const task = await Task.create({
|
|
33
|
+
title: title.trim().slice(0, 200),
|
|
34
|
+
status: VALID_STATUSES.includes(status) ? status : 'pending',
|
|
35
|
+
priority: clampPriority(priority),
|
|
36
|
+
});
|
|
37
|
+
res.status(201).json({ task });
|
|
38
|
+
}
|
|
39
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
exports.update = async (req, res) =>
|
|
43
|
+
{
|
|
44
|
+
try
|
|
45
|
+
{
|
|
46
|
+
const task = await Task.findById(Number(req.params.id));
|
|
47
|
+
if (!task) return res.status(404).json({ error: 'not found' });
|
|
48
|
+
|
|
49
|
+
const updates = {};
|
|
50
|
+
if (req.body.title != null) updates.title = String(req.body.title).trim().slice(0, 200);
|
|
51
|
+
if (req.body.status != null && VALID_STATUSES.includes(req.body.status)) updates.status = req.body.status;
|
|
52
|
+
if (req.body.priority != null) updates.priority = clampPriority(req.body.priority);
|
|
53
|
+
await task.update(updates);
|
|
54
|
+
res.json({ task });
|
|
55
|
+
}
|
|
56
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
exports.remove = async (req, res) =>
|
|
60
|
+
{
|
|
61
|
+
try
|
|
62
|
+
{
|
|
63
|
+
const task = await Task.findById(Number(req.params.id));
|
|
64
|
+
if (!task) return res.status(404).json({ error: 'not found' });
|
|
65
|
+
await task.delete();
|
|
66
|
+
res.json({ deleted: true, id: Number(req.params.id) });
|
|
67
|
+
}
|
|
68
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
exports.restore = async (req, res) =>
|
|
72
|
+
{
|
|
73
|
+
try
|
|
74
|
+
{
|
|
75
|
+
const rows = await Task.query().withDeleted().where('id', '=', Number(req.params.id)).exec();
|
|
76
|
+
const task = rows[0];
|
|
77
|
+
if (!task) return res.status(404).json({ error: 'not found' });
|
|
78
|
+
if (task.restore) await task.restore();
|
|
79
|
+
res.json({ task });
|
|
80
|
+
}
|
|
81
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.removeAll = async (req, res) =>
|
|
85
|
+
{
|
|
86
|
+
try
|
|
87
|
+
{
|
|
88
|
+
const all = await Task.find();
|
|
89
|
+
for (const t of all) await t.delete();
|
|
90
|
+
res.json({ deleted: all.length });
|
|
91
|
+
}
|
|
92
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
exports.stats = async (req, res) =>
|
|
96
|
+
{
|
|
97
|
+
try
|
|
98
|
+
{
|
|
99
|
+
const total = await Task.count();
|
|
100
|
+
const pending = await Task.count({ status: 'pending' });
|
|
101
|
+
const inProgress = await Task.count({ status: 'in-progress' });
|
|
102
|
+
const done = await Task.count({ status: 'done' });
|
|
103
|
+
const avgPriority = await Task.query().avg('priority');
|
|
104
|
+
const maxPriority = await Task.query().max('priority');
|
|
105
|
+
res.json({ total, pending, inProgress, done, avgPriority: avgPriority || 0, maxPriority: maxPriority || 0 });
|
|
106
|
+
}
|
|
107
|
+
catch (e) { res.status(500).json({ error: e.message }); }
|
|
108
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zero-http full-server example
|
|
3
|
-
*
|
|
3
|
+
* Clean entry point — config, middleware, and routes are in separate modules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// --- Crash protection ---
|
|
@@ -8,183 +8,34 @@ process.on('uncaughtException', (err) => { console.error('[UNCAUGHT]', err); });
|
|
|
8
8
|
process.on('unhandledRejection', (err) => { console.error('[UNHANDLED]', err); });
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const
|
|
12
|
-
const os = require('os');
|
|
13
|
-
const { createApp, cors, json, urlencoded, text, raw, multipart, static: serveStatic, fetch, logger, compress, Router } = require('..');
|
|
11
|
+
const { createApp, env, fetch } = require('..');
|
|
14
12
|
|
|
15
|
-
// ---
|
|
16
|
-
|
|
17
|
-
app.use(logger({ format: 'dev' }));
|
|
18
|
-
app.use(cors());
|
|
19
|
-
app.use(compress());
|
|
20
|
-
app.use(json());
|
|
21
|
-
app.use(urlencoded());
|
|
22
|
-
app.use(text());
|
|
23
|
-
app.use(serveStatic(path.join(__dirname, 'public')));
|
|
24
|
-
|
|
25
|
-
// --- Controllers ---
|
|
26
|
-
const rootController = require('./controllers/root');
|
|
27
|
-
const headersController = require('./controllers/headers');
|
|
28
|
-
const echoController = require('./controllers/echo');
|
|
29
|
-
const uploadsController = require('./controllers/uploads');
|
|
30
|
-
|
|
31
|
-
// --- Core Routes ---
|
|
32
|
-
app.get('/', rootController.getRoot);
|
|
33
|
-
app.get('/headers', headersController.getHeaders);
|
|
34
|
-
app.post('/echo-json', echoController.echoJson);
|
|
35
|
-
app.post('/echo', echoController.echo);
|
|
36
|
-
app.post('/echo-urlencoded', echoController.echoUrlencoded);
|
|
37
|
-
app.post('/echo-text', echoController.echoText);
|
|
38
|
-
app.post('/echo-raw', raw(), echoController.echoRaw);
|
|
39
|
-
|
|
40
|
-
// --- Uploads and Trash ---
|
|
41
|
-
const uploadsDir = path.join(__dirname, 'uploads');
|
|
42
|
-
uploadsController.ensureUploadsDir(uploadsDir);
|
|
43
|
-
|
|
44
|
-
// Serve uploaded files from /uploads path using built-in path-prefix middleware
|
|
45
|
-
app.use('/uploads', serveStatic(uploadsDir));
|
|
46
|
-
|
|
47
|
-
// Upload, delete, restore, and trash routes
|
|
48
|
-
app.post('/upload', multipart({ maxFileSize: 5 * 1024 * 1024, dir: uploadsDir }), uploadsController.upload(uploadsDir));
|
|
49
|
-
app.delete('/uploads/:name', uploadsController.deleteUpload(uploadsDir));
|
|
50
|
-
app.delete('/uploads', uploadsController.deleteAllUploads(uploadsDir));
|
|
51
|
-
app.post('/uploads/:name/restore', uploadsController.restoreUpload(uploadsDir));
|
|
52
|
-
app.get('/uploads-trash-list', uploadsController.listTrash(uploadsDir));
|
|
53
|
-
app.delete('/uploads-trash/:name', uploadsController.deleteTrashItem(uploadsDir));
|
|
54
|
-
app.delete('/uploads-trash', uploadsController.emptyTrash(uploadsDir));
|
|
55
|
-
|
|
56
|
-
// --- Trash Retention ---
|
|
57
|
-
const TRASH_RETENTION_DAYS = Number(process.env.TRASH_RETENTION_DAYS || 7);
|
|
58
|
-
const TRASH_RETENTION_MS = TRASH_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
59
|
-
|
|
60
|
-
function autoEmptyTrash()
|
|
61
|
-
{
|
|
62
|
-
try
|
|
63
|
-
{
|
|
64
|
-
const trash = path.join(uploadsDir, '.trash');
|
|
65
|
-
if (!fs.existsSync(trash)) return;
|
|
66
|
-
const now = Date.now();
|
|
67
|
-
const removed = [];
|
|
68
|
-
for (const f of fs.readdirSync(trash))
|
|
69
|
-
{
|
|
70
|
-
try
|
|
71
|
-
{
|
|
72
|
-
const p = path.join(trash, f);
|
|
73
|
-
const st = fs.statSync(p);
|
|
74
|
-
if (now - st.mtimeMs > TRASH_RETENTION_MS)
|
|
75
|
-
{
|
|
76
|
-
fs.unlinkSync(p);
|
|
77
|
-
removed.push(f);
|
|
78
|
-
try { fs.unlinkSync(path.join(trash, '.thumbs', f + '-thumb.svg')); } catch (e) { }
|
|
79
|
-
}
|
|
80
|
-
} catch (e) { }
|
|
81
|
-
}
|
|
82
|
-
if (removed.length) console.log(`autoEmptyTrash: removed ${removed.length} file(s)`);
|
|
83
|
-
} catch (e) { console.error('autoEmptyTrash error:', e); }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
autoEmptyTrash();
|
|
87
|
-
setInterval(autoEmptyTrash, 24 * 60 * 60 * 1000).unref();
|
|
88
|
-
|
|
89
|
-
// --- Uploads Listings ---
|
|
90
|
-
app.get('/uploads-list', uploadsController.listUploads(uploadsDir));
|
|
91
|
-
app.get('/uploads-all', uploadsController.listAll(uploadsDir));
|
|
92
|
-
|
|
93
|
-
// --- Temp Cleanup ---
|
|
94
|
-
const cleanupController = require('./controllers/cleanup');
|
|
95
|
-
app.post('/cleanup', cleanupController.cleanup(path.join(os.tmpdir(), 'zero-http-uploads')));
|
|
96
|
-
|
|
97
|
-
// --- Proxy ---
|
|
98
|
-
const proxyController = require('./controllers/proxy');
|
|
99
|
-
const proxyFetch = (typeof globalThis !== 'undefined' && globalThis.fetch) || fetch;
|
|
100
|
-
app.get('/proxy', proxyController.proxy(proxyFetch));
|
|
101
|
-
|
|
102
|
-
// --- WebSocket Chat ---
|
|
103
|
-
const wsClients = new Set();
|
|
104
|
-
|
|
105
|
-
app.ws('/ws/chat', { maxPayload: 64 * 1024, pingInterval: 25000 }, (ws, req) =>
|
|
106
|
-
{
|
|
107
|
-
ws.data.name = ws.query.name || 'anon';
|
|
108
|
-
wsClients.add(ws);
|
|
109
|
-
ws.send(JSON.stringify({ type: 'system', text: 'Welcome, ' + ws.data.name + '!' }));
|
|
110
|
-
|
|
111
|
-
// Broadcast join
|
|
112
|
-
for (const c of wsClients)
|
|
113
|
-
{
|
|
114
|
-
if (c !== ws && c.readyState === 1)
|
|
115
|
-
c.send(JSON.stringify({ type: 'system', text: ws.data.name + ' joined' }));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
ws.on('message', (msg) =>
|
|
119
|
-
{
|
|
120
|
-
// Broadcast to all clients including sender
|
|
121
|
-
const payload = JSON.stringify({ type: 'message', name: ws.data.name, text: String(msg) });
|
|
122
|
-
for (const c of wsClients)
|
|
123
|
-
{
|
|
124
|
-
if (c.readyState === 1) c.send(payload);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
ws.on('close', () =>
|
|
129
|
-
{
|
|
130
|
-
wsClients.delete(ws);
|
|
131
|
-
for (const c of wsClients)
|
|
132
|
-
{
|
|
133
|
-
if (c.readyState === 1)
|
|
134
|
-
c.send(JSON.stringify({ type: 'system', text: ws.data.name + ' left' }));
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// --- Server-Sent Events ---
|
|
140
|
-
const sseClients = new Set();
|
|
141
|
-
|
|
142
|
-
app.get('/sse/events', (req, res) =>
|
|
143
|
-
{
|
|
144
|
-
const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 });
|
|
145
|
-
sseClients.add(sse);
|
|
146
|
-
sse.send({ type: 'connected', clients: sseClients.size });
|
|
13
|
+
// --- Environment ---
|
|
14
|
+
env.load(path.join(__dirname, '..', '.env'));
|
|
147
15
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
app.post('/sse/broadcast', (req, res) =>
|
|
152
|
-
{
|
|
153
|
-
const data = req.body || {};
|
|
154
|
-
for (const sse of sseClients)
|
|
155
|
-
{
|
|
156
|
-
sse.event('broadcast', data);
|
|
157
|
-
}
|
|
158
|
-
res.json({ sent: sseClients.size });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// --- Router Demo (API sub-app) ---
|
|
162
|
-
const apiRouter = Router();
|
|
16
|
+
// --- App ---
|
|
17
|
+
const app = createApp();
|
|
163
18
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
protocol: req.protocol,
|
|
168
|
-
ip: req.ip,
|
|
169
|
-
method: req.method,
|
|
170
|
-
url: req.url
|
|
171
|
-
}));
|
|
19
|
+
// --- Middleware ---
|
|
20
|
+
const { applyMiddleware } = require('./config/middleware');
|
|
21
|
+
applyMiddleware(app);
|
|
172
22
|
|
|
173
|
-
|
|
23
|
+
// --- Routes ---
|
|
24
|
+
require('./routes/core')(app);
|
|
25
|
+
require('./routes/uploads')(app);
|
|
26
|
+
require('./routes/realtime')(app);
|
|
27
|
+
require('./routes/playground')(app);
|
|
28
|
+
require('./routes/api')(app);
|
|
174
29
|
|
|
175
|
-
// ---
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
res.set('Content-Type', 'application/json');
|
|
179
|
-
res.send(JSON.stringify(app.routes(), null, 2));
|
|
180
|
-
});
|
|
30
|
+
// --- TLS ---
|
|
31
|
+
const { hasCerts, tlsOpts } = require('./config/tls');
|
|
181
32
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const server = app.listen(port, () =>
|
|
33
|
+
// --- Start ---
|
|
34
|
+
const port = process.env.PORT || 7273;
|
|
35
|
+
app.listen(port, tlsOpts, () =>
|
|
186
36
|
{
|
|
187
|
-
|
|
37
|
+
const proto = hasCerts ? 'https' : 'http';
|
|
38
|
+
console.log(`zero-http full-server listening on ${proto}://localhost:${port}`);
|
|
188
39
|
if (process.argv.includes('--test')) runTests(port).catch(console.error);
|
|
189
40
|
});
|
|
190
41
|
|
|
@@ -192,14 +43,14 @@ const server = app.listen(port, () =>
|
|
|
192
43
|
async function runTests(port)
|
|
193
44
|
{
|
|
194
45
|
const proto = hasCerts ? 'https' : 'http';
|
|
195
|
-
const base
|
|
46
|
+
const base = `${proto}://localhost:${port}`;
|
|
196
47
|
console.log('running smoke tests against', base);
|
|
197
48
|
|
|
198
49
|
const doReq = async (label, promise) =>
|
|
199
50
|
{
|
|
200
51
|
try
|
|
201
52
|
{
|
|
202
|
-
const r
|
|
53
|
+
const r = await promise;
|
|
203
54
|
const ct = r.headers.get('content-type') || '';
|
|
204
55
|
const body = ct.includes('json') ? await r.json() : await r.text();
|
|
205
56
|
console.log(` ${label}`, r.status, JSON.stringify(body));
|
|
@@ -207,10 +58,10 @@ async function runTests(port)
|
|
|
207
58
|
catch (e) { console.error(` ${label} error:`, e.message); }
|
|
208
59
|
};
|
|
209
60
|
|
|
210
|
-
await doReq('GET /',
|
|
211
|
-
await doReq('POST /echo-json',
|
|
61
|
+
await doReq('GET /', fetch(base + '/'));
|
|
62
|
+
await doReq('POST /echo-json', fetch(base + '/echo-json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ a: 1 }) }));
|
|
212
63
|
await doReq('POST /echo-urlencoded', fetch(base + '/echo-urlencoded', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'foo=bar' }));
|
|
213
|
-
await doReq('POST /echo-text',
|
|
214
|
-
await doReq('GET /headers',
|
|
64
|
+
await doReq('POST /echo-text', fetch(base + '/echo-text', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'hello' }));
|
|
65
|
+
await doReq('GET /headers', fetch(base + '/headers'));
|
|
215
66
|
console.log('smoke tests complete');
|
|
216
67
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { Model, TYPES } = require('../..');
|
|
2
|
+
|
|
3
|
+
class Task extends Model
|
|
4
|
+
{
|
|
5
|
+
static table = 'tasks';
|
|
6
|
+
static timestamps = true;
|
|
7
|
+
static softDelete = true;
|
|
8
|
+
static schema = {
|
|
9
|
+
id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
|
|
10
|
+
title: { type: TYPES.STRING, required: true, maxLength: 200 },
|
|
11
|
+
status: { type: TYPES.STRING, default: 'pending', enum: ['pending', 'in-progress', 'done'] },
|
|
12
|
+
priority: { type: TYPES.INTEGER, default: 0, min: 0, max: 5 },
|
|
13
|
+
};
|
|
14
|
+
static scopes = {
|
|
15
|
+
active: (q) => q.where('status', '!=', 'done'),
|
|
16
|
+
done: (q) => q.where('status', '=', 'done'),
|
|
17
|
+
highPriority: (q) => q.where('priority', '>=', 3),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = Task;
|