zero-http 0.2.5 → 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.
Files changed (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +25 -184
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. 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
- * Organized with controllers and JSDoc comments for clarity and maintainability.
3
+ * Clean entry point — config, middleware, and routes are in separate modules.
4
4
  */
5
5
 
6
6
  // --- Crash protection ---
@@ -8,208 +8,49 @@ 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 fs = require('fs');
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
- // --- App Initialization ---
16
- const app = createApp();
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
- }
13
+ // --- Environment ---
14
+ env.load(path.join(__dirname, '..', '.env'));
85
15
 
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 });
147
-
148
- sse.on('close', () => sseClients.delete(sse));
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();
163
-
164
- apiRouter.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
165
- apiRouter.get('/info', (req, res) => res.json({
166
- secure: req.secure,
167
- protocol: req.protocol,
168
- ip: req.ip,
169
- method: req.method,
170
- url: req.url
171
- }));
16
+ // --- App ---
17
+ const app = createApp();
172
18
 
173
- app.use('/api', apiRouter);
19
+ // --- Middleware ---
20
+ const { applyMiddleware } = require('./config/middleware');
21
+ applyMiddleware(app);
174
22
 
175
- // --- Route introspection ---
176
- app.get('/debug/routes', (req, res) =>
177
- {
178
- res.set('Content-Type', 'application/json');
179
- res.send(JSON.stringify(app.routes(), null, 2));
180
- });
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);
181
29
 
182
- // --- TLS Certificates (HTTPS + WSS) ---
183
- const certPath = '/www/server/panel/vhost/cert/z-http.com/fullchain.pem';
184
- const keyPath = '/www/server/panel/vhost/cert/z-http.com/privkey.pem';
185
- const hasCerts = fs.existsSync(certPath) && fs.existsSync(keyPath);
30
+ // --- TLS ---
31
+ const { hasCerts, tlsOpts } = require('./config/tls');
186
32
 
187
- const tlsOpts = hasCerts
188
- ? { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) }
189
- : undefined;
190
-
191
- // --- Server Startup ---
33
+ // --- Start ---
192
34
  const port = process.env.PORT || 7273;
193
- const server = app.listen(port, tlsOpts, () =>
35
+ app.listen(port, tlsOpts, () =>
194
36
  {
195
37
  const proto = hasCerts ? 'https' : 'http';
196
38
  console.log(`zero-http full-server listening on ${proto}://localhost:${port}`);
197
39
  if (process.argv.includes('--test')) runTests(port).catch(console.error);
198
40
  });
199
41
 
200
-
201
42
  /** Quick smoke tests using built-in fetch */
202
43
  async function runTests(port)
203
44
  {
204
45
  const proto = hasCerts ? 'https' : 'http';
205
- const base = `${proto}://localhost:${port}`;
46
+ const base = `${proto}://localhost:${port}`;
206
47
  console.log('running smoke tests against', base);
207
48
 
208
49
  const doReq = async (label, promise) =>
209
50
  {
210
51
  try
211
52
  {
212
- const r = await promise;
53
+ const r = await promise;
213
54
  const ct = r.headers.get('content-type') || '';
214
55
  const body = ct.includes('json') ? await r.json() : await r.text();
215
56
  console.log(` ${label}`, r.status, JSON.stringify(body));
@@ -217,10 +58,10 @@ async function runTests(port)
217
58
  catch (e) { console.error(` ${label} error:`, e.message); }
218
59
  };
219
60
 
220
- await doReq('GET /', fetch(base + '/'));
221
- await doReq('POST /echo-json', fetch(base + '/echo-json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ a: 1 }) }));
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 }) }));
222
63
  await doReq('POST /echo-urlencoded', fetch(base + '/echo-urlencoded', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'foo=bar' }));
223
- await doReq('POST /echo-text', fetch(base + '/echo-text', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'hello' }));
224
- await doReq('GET /headers', fetch(base + '/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'));
225
66
  console.log('smoke tests complete');
226
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;