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.
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 +28 -177
  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,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 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
- }
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
- 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();
16
+ // --- App ---
17
+ const app = createApp();
163
18
 
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
- }));
19
+ // --- Middleware ---
20
+ const { applyMiddleware } = require('./config/middleware');
21
+ applyMiddleware(app);
172
22
 
173
- app.use('/api', apiRouter);
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
- // --- 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
- });
30
+ // --- TLS ---
31
+ const { hasCerts, tlsOpts } = require('./config/tls');
181
32
 
182
-
183
- // --- Server Startup ---
184
- const port = process.env.PORT || 3000;
185
- const server = app.listen(port, () =>
33
+ // --- Start ---
34
+ const port = process.env.PORT || 7273;
35
+ app.listen(port, tlsOpts, () =>
186
36
  {
187
- console.log(`zero-http full-server listening on http://localhost:${port}`);
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 = `${proto}://localhost:${port}`;
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 = await promise;
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 /', fetch(base + '/'));
211
- 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 }) }));
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', fetch(base + '/echo-text', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'hello' }));
214
- 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'));
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;