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,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/adapters/postgres
|
|
3
|
+
* @description PostgreSQL adapter using the optional `pg` driver.
|
|
4
|
+
* Requires: `npm install pg`
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const db = Database.connect('postgres', {
|
|
8
|
+
* host: '127.0.0.1', user: 'postgres', password: '', database: 'myapp',
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
const BaseSqlAdapter = require('./sql-base');
|
|
12
|
+
|
|
13
|
+
class PostgresAdapter extends BaseSqlAdapter
|
|
14
|
+
{
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {string} [options.host='localhost'] - Server hostname.
|
|
18
|
+
* @param {number} [options.port=5432] - Server port.
|
|
19
|
+
* @param {string} [options.user] - Database user.
|
|
20
|
+
* @param {string} [options.password] - Database password.
|
|
21
|
+
* @param {string} options.database - Database name.
|
|
22
|
+
* @param {number} [options.max=10] - Max pool size.
|
|
23
|
+
* @param {number} [options.idleTimeoutMillis=10000] - Idle client timeout.
|
|
24
|
+
* @param {number} [options.connectionTimeoutMillis=0] - Connection timeout (0 = no limit).
|
|
25
|
+
* @param {boolean|object} [options.ssl] - SSL mode or TLS options.
|
|
26
|
+
* @param {string} [options.connectionString] - Full connection URI (overrides individual settings).
|
|
27
|
+
* @param {string} [options.application_name] - Identify the app in pg_stat_activity.
|
|
28
|
+
* @param {number} [options.statement_timeout] - Statement timeout in ms.
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {})
|
|
31
|
+
{
|
|
32
|
+
super();
|
|
33
|
+
let pg;
|
|
34
|
+
try { pg = require('pg'); }
|
|
35
|
+
catch (e)
|
|
36
|
+
{
|
|
37
|
+
throw new Error(
|
|
38
|
+
'PostgreSQL adapter requires "pg" package.\n' +
|
|
39
|
+
'Install it with: npm install pg'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
this._pool = new pg.Pool({ max: 10, ...options });
|
|
43
|
+
this._options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_typeMap(colDef)
|
|
47
|
+
{
|
|
48
|
+
const map = {
|
|
49
|
+
string: `VARCHAR(${colDef.maxLength || 255})`, text: 'TEXT',
|
|
50
|
+
integer: 'INTEGER', float: 'DOUBLE PRECISION', boolean: 'BOOLEAN',
|
|
51
|
+
date: 'DATE', datetime: 'TIMESTAMPTZ', json: 'JSONB', blob: 'BYTEA',
|
|
52
|
+
uuid: 'UUID',
|
|
53
|
+
};
|
|
54
|
+
return map[colDef.type] || 'TEXT';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* PostgreSQL uses $1, $2, ... style parameters.
|
|
59
|
+
* Override the base class WHERE builders.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
_buildWherePg(conditions, startIdx = 1)
|
|
63
|
+
{
|
|
64
|
+
if (!conditions || Object.keys(conditions).length === 0)
|
|
65
|
+
return { clause: '', values: [], nextIdx: startIdx };
|
|
66
|
+
const parts = [];
|
|
67
|
+
const values = [];
|
|
68
|
+
let idx = startIdx;
|
|
69
|
+
for (const [k, v] of Object.entries(conditions))
|
|
70
|
+
{
|
|
71
|
+
if (v === null) { parts.push(`"${k}" IS NULL`); }
|
|
72
|
+
else { parts.push(`"${k}" = $${idx++}`); values.push(this._toSqlValue(v)); }
|
|
73
|
+
}
|
|
74
|
+
return { clause: ' WHERE ' + parts.join(' AND '), values, nextIdx: idx };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_buildWhereFromChainPg(where, startIdx = 1)
|
|
78
|
+
{
|
|
79
|
+
if (!where || where.length === 0) return { clause: '', values: [], nextIdx: startIdx };
|
|
80
|
+
const parts = [];
|
|
81
|
+
const values = [];
|
|
82
|
+
let idx = startIdx;
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < where.length; i++)
|
|
85
|
+
{
|
|
86
|
+
const w = where[i];
|
|
87
|
+
|
|
88
|
+
// Handle raw WHERE clauses (from whereRaw) — convert ? to $N
|
|
89
|
+
if (w.raw)
|
|
90
|
+
{
|
|
91
|
+
let rawExpr = w.raw;
|
|
92
|
+
if (w.params)
|
|
93
|
+
{
|
|
94
|
+
for (const p of w.params)
|
|
95
|
+
{
|
|
96
|
+
rawExpr = rawExpr.replace('?', `$${idx++}`);
|
|
97
|
+
values.push(p);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (i === 0) parts.push(rawExpr);
|
|
101
|
+
else parts.push(`${w.logic} ${rawExpr}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { field, op, value, logic } = w;
|
|
106
|
+
let expr;
|
|
107
|
+
|
|
108
|
+
if (op === 'IS NULL') expr = `"${field}" IS NULL`;
|
|
109
|
+
else if (op === 'IS NOT NULL') expr = `"${field}" IS NOT NULL`;
|
|
110
|
+
else if (op === 'IN' || op === 'NOT IN')
|
|
111
|
+
{
|
|
112
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
113
|
+
expr = op === 'IN' ? '0=1' : '1=1';
|
|
114
|
+
else
|
|
115
|
+
{
|
|
116
|
+
const placeholders = value.map(() => `$${idx++}`).join(', ');
|
|
117
|
+
expr = `"${field}" ${op} (${placeholders})`;
|
|
118
|
+
values.push(...value.map(v => this._toSqlValue(v)));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (op === 'BETWEEN')
|
|
122
|
+
{
|
|
123
|
+
expr = `"${field}" BETWEEN $${idx++} AND $${idx++}`;
|
|
124
|
+
values.push(this._toSqlValue(value[0]), this._toSqlValue(value[1]));
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
{
|
|
128
|
+
expr = `"${field}" ${op} $${idx++}`;
|
|
129
|
+
values.push(this._toSqlValue(value));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (i === 0) parts.push(expr);
|
|
133
|
+
else parts.push(`${logic} ${expr}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { clause: ' WHERE ' + parts.join(' '), values, nextIdx: idx };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async createTable(table, schema)
|
|
140
|
+
{
|
|
141
|
+
const cols = [];
|
|
142
|
+
for (const [name, def] of Object.entries(schema))
|
|
143
|
+
{
|
|
144
|
+
let line = `"${name}" ${this._typeMap(def)}`;
|
|
145
|
+
if (def.primaryKey && def.autoIncrement)
|
|
146
|
+
{
|
|
147
|
+
line = `"${name}" SERIAL PRIMARY KEY`;
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
{
|
|
151
|
+
if (def.primaryKey) line += ' PRIMARY KEY';
|
|
152
|
+
if (def.required && !def.primaryKey) line += ' NOT NULL';
|
|
153
|
+
if (def.unique) line += ' UNIQUE';
|
|
154
|
+
if (def.default !== undefined && typeof def.default !== 'function')
|
|
155
|
+
line += ` DEFAULT ${this._sqlDefault(def.default)}`;
|
|
156
|
+
}
|
|
157
|
+
cols.push(line);
|
|
158
|
+
}
|
|
159
|
+
await this._pool.query(`CREATE TABLE IF NOT EXISTS "${table}" (${cols.join(', ')})`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async dropTable(table)
|
|
163
|
+
{
|
|
164
|
+
await this._pool.query(`DROP TABLE IF EXISTS "${table}"`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async insert(table, data)
|
|
168
|
+
{
|
|
169
|
+
const keys = Object.keys(data);
|
|
170
|
+
const values = keys.map(k => this._toSqlValue(data[k]));
|
|
171
|
+
const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');
|
|
172
|
+
const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
|
173
|
+
const { rows } = await this._pool.query(sql, values);
|
|
174
|
+
return rows[0] || { ...data };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async update(table, pk, pkVal, data)
|
|
178
|
+
{
|
|
179
|
+
const keys = Object.keys(data);
|
|
180
|
+
const values = keys.map(k => this._toSqlValue(data[k]));
|
|
181
|
+
const sets = keys.map((k, i) => `"${k}" = $${i + 1}`).join(', ');
|
|
182
|
+
values.push(pkVal);
|
|
183
|
+
await this._pool.query(`UPDATE "${table}" SET ${sets} WHERE "${pk}" = $${values.length}`, values);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async updateWhere(table, conditions, data)
|
|
187
|
+
{
|
|
188
|
+
const keys = Object.keys(data);
|
|
189
|
+
const values = keys.map(k => this._toSqlValue(data[k]));
|
|
190
|
+
const sets = keys.map((k, i) => `"${k}" = $${i + 1}`).join(', ');
|
|
191
|
+
const { clause, values: whereVals } = this._buildWherePg(conditions, keys.length + 1);
|
|
192
|
+
values.push(...whereVals);
|
|
193
|
+
const { rowCount } = await this._pool.query(`UPDATE "${table}" SET ${sets}${clause}`, values);
|
|
194
|
+
return rowCount;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async remove(table, pk, pkVal)
|
|
198
|
+
{
|
|
199
|
+
await this._pool.query(`DELETE FROM "${table}" WHERE "${pk}" = $1`, [pkVal]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async deleteWhere(table, conditions)
|
|
203
|
+
{
|
|
204
|
+
const { clause, values } = this._buildWherePg(conditions);
|
|
205
|
+
const { rowCount } = await this._pool.query(`DELETE FROM "${table}"${clause}`, values);
|
|
206
|
+
return rowCount;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async execute(descriptor)
|
|
210
|
+
{
|
|
211
|
+
const { action, table, fields, where, orderBy, limit, offset, distinct } = descriptor;
|
|
212
|
+
|
|
213
|
+
if (action === 'count')
|
|
214
|
+
{
|
|
215
|
+
const { clause, values } = this._buildWhereFromChainPg(where);
|
|
216
|
+
const { rows } = await this._pool.query(`SELECT COUNT(*) as count FROM "${table}"${clause}`, values);
|
|
217
|
+
return parseInt(rows[0].count, 10);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const selectFields = fields && fields.length ? fields.map(f => `"${f}"`).join(', ') : '*';
|
|
221
|
+
const distinctStr = distinct ? 'DISTINCT ' : '';
|
|
222
|
+
let sql = `SELECT ${distinctStr}${selectFields} FROM "${table}"`;
|
|
223
|
+
const values = [];
|
|
224
|
+
let paramIdx = 1;
|
|
225
|
+
|
|
226
|
+
if (where && where.length)
|
|
227
|
+
{
|
|
228
|
+
const { clause, values: wv, nextIdx } = this._buildWhereFromChainPg(where, paramIdx);
|
|
229
|
+
sql += clause;
|
|
230
|
+
values.push(...wv);
|
|
231
|
+
paramIdx = nextIdx;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (orderBy && orderBy.length)
|
|
235
|
+
sql += ' ORDER BY ' + orderBy.map(o => `"${o.field}" ${o.dir}`).join(', ');
|
|
236
|
+
if (limit !== null && limit !== undefined)
|
|
237
|
+
{
|
|
238
|
+
sql += ` LIMIT $${paramIdx++}`;
|
|
239
|
+
values.push(limit);
|
|
240
|
+
}
|
|
241
|
+
if (offset !== null && offset !== undefined)
|
|
242
|
+
{
|
|
243
|
+
sql += ` OFFSET $${paramIdx++}`;
|
|
244
|
+
values.push(offset);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { rows } = await this._pool.query(sql, values);
|
|
248
|
+
return rows;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async close() { await this._pool.end(); }
|
|
252
|
+
async raw(sql, ...params) { const { rows } = await this._pool.query(sql, params); return rows; }
|
|
253
|
+
|
|
254
|
+
async transaction(fn)
|
|
255
|
+
{
|
|
256
|
+
const client = await this._pool.connect();
|
|
257
|
+
try
|
|
258
|
+
{
|
|
259
|
+
await client.query('BEGIN');
|
|
260
|
+
const result = await fn(client);
|
|
261
|
+
await client.query('COMMIT');
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
catch (e) { await client.query('ROLLBACK'); throw e; }
|
|
265
|
+
finally { client.release(); }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -- PostgreSQL Utilities ----------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* List all user-created tables in the current schema.
|
|
272
|
+
* @param {string} [schema='public'] - Schema name.
|
|
273
|
+
* @returns {Promise<string[]>}
|
|
274
|
+
*/
|
|
275
|
+
async tables(schema = 'public')
|
|
276
|
+
{
|
|
277
|
+
const { rows } = await this._pool.query(
|
|
278
|
+
`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = $1 ORDER BY tablename`,
|
|
279
|
+
[schema]
|
|
280
|
+
);
|
|
281
|
+
return rows.map(r => r.tablename);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get column information for a table.
|
|
286
|
+
* @param {string} table
|
|
287
|
+
* @param {string} [schema='public']
|
|
288
|
+
* @returns {Promise<Array<{ column_name: string, data_type: string, is_nullable: string, column_default: string }>>}
|
|
289
|
+
*/
|
|
290
|
+
async columns(table, schema = 'public')
|
|
291
|
+
{
|
|
292
|
+
const { rows } = await this._pool.query(
|
|
293
|
+
`SELECT column_name, data_type, is_nullable, column_default
|
|
294
|
+
FROM information_schema.columns
|
|
295
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
296
|
+
ORDER BY ordinal_position`,
|
|
297
|
+
[schema, table]
|
|
298
|
+
);
|
|
299
|
+
return rows;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get the current database size in bytes.
|
|
304
|
+
* @returns {Promise<number>}
|
|
305
|
+
*/
|
|
306
|
+
async databaseSize()
|
|
307
|
+
{
|
|
308
|
+
const { rows } = await this._pool.query('SELECT pg_database_size(current_database()) AS size');
|
|
309
|
+
return Number(rows[0].size) || 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get the row count for a table (estimated for large tables, exact for small ones).
|
|
314
|
+
* @param {string} table
|
|
315
|
+
* @returns {Promise<number>}
|
|
316
|
+
*/
|
|
317
|
+
async tableSize(table)
|
|
318
|
+
{
|
|
319
|
+
const { rows } = await this._pool.query(
|
|
320
|
+
`SELECT pg_total_relation_size($1) AS size`, [table]
|
|
321
|
+
);
|
|
322
|
+
return Number(rows[0].size) || 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get connection pool status.
|
|
327
|
+
* @returns {{ total: number, idle: number, waiting: number }}
|
|
328
|
+
*/
|
|
329
|
+
poolStatus()
|
|
330
|
+
{
|
|
331
|
+
return {
|
|
332
|
+
total: this._pool.totalCount,
|
|
333
|
+
idle: this._pool.idleCount,
|
|
334
|
+
waiting: this._pool.waitingCount,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the PostgreSQL server version string.
|
|
340
|
+
* @returns {Promise<string>}
|
|
341
|
+
*/
|
|
342
|
+
async version()
|
|
343
|
+
{
|
|
344
|
+
const { rows } = await this._pool.query('SELECT version() AS ver');
|
|
345
|
+
return rows[0].ver;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Ping the database to check connectivity.
|
|
350
|
+
* @returns {Promise<boolean>}
|
|
351
|
+
*/
|
|
352
|
+
async ping()
|
|
353
|
+
{
|
|
354
|
+
try
|
|
355
|
+
{
|
|
356
|
+
await this._pool.query('SELECT 1');
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
catch { return false; }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Execute a raw statement that doesn't return rows (INSERT, UPDATE, DDL).
|
|
364
|
+
* @param {string} sql
|
|
365
|
+
* @param {...*} params
|
|
366
|
+
* @returns {Promise<{ rowCount: number }>}
|
|
367
|
+
*/
|
|
368
|
+
async exec(sql, ...params)
|
|
369
|
+
{
|
|
370
|
+
const { rowCount } = await this._pool.query(sql, params);
|
|
371
|
+
return { rowCount: rowCount || 0 };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Run a LISTEN/NOTIFY style query. Useful for subscribing to PG notifications.
|
|
376
|
+
* @param {string} channel
|
|
377
|
+
* @param {Function} callback - Receives { channel, payload }.
|
|
378
|
+
* @returns {Promise<Function>} Unlisten function.
|
|
379
|
+
*/
|
|
380
|
+
async listen(channel, callback)
|
|
381
|
+
{
|
|
382
|
+
const client = await this._pool.connect();
|
|
383
|
+
await client.query(`LISTEN ${channel}`);
|
|
384
|
+
client.on('notification', callback);
|
|
385
|
+
return async () =>
|
|
386
|
+
{
|
|
387
|
+
await client.query(`UNLISTEN ${channel}`);
|
|
388
|
+
client.removeListener('notification', callback);
|
|
389
|
+
client.release();
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = PostgresAdapter;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/adapters/sql-base
|
|
3
|
+
* @description Base class for SQL adapters. Provides shared query-building
|
|
4
|
+
* utilities, parameterised queries (SQL injection safe), and
|
|
5
|
+
* type mapping helpers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class BaseSqlAdapter
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* Build a WHERE clause from simple { key: value } conditions.
|
|
12
|
+
* Uses parameterised queries to prevent SQL injection.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} conditions
|
|
15
|
+
* @returns {{ clause: string, values: Array }}
|
|
16
|
+
* @protected
|
|
17
|
+
*/
|
|
18
|
+
_buildWhere(conditions)
|
|
19
|
+
{
|
|
20
|
+
if (!conditions || Object.keys(conditions).length === 0)
|
|
21
|
+
{
|
|
22
|
+
return { clause: '', values: [] };
|
|
23
|
+
}
|
|
24
|
+
const parts = [];
|
|
25
|
+
const values = [];
|
|
26
|
+
for (const [k, v] of Object.entries(conditions))
|
|
27
|
+
{
|
|
28
|
+
if (v === null)
|
|
29
|
+
{
|
|
30
|
+
parts.push(`"${k}" IS NULL`);
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
{
|
|
34
|
+
parts.push(`"${k}" = ?`);
|
|
35
|
+
values.push(this._toSqlValue(v));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { clause: ' WHERE ' + parts.join(' AND '), values };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build a WHERE clause from the Query builder's where chain.
|
|
43
|
+
* Supports operators: =, !=, >, <, >=, <=, LIKE, IN, NOT IN, BETWEEN, IS NULL, IS NOT NULL.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array} where - Array of { field, op, value, logic } objects.
|
|
46
|
+
* @returns {{ clause: string, values: Array }}
|
|
47
|
+
* @protected
|
|
48
|
+
*/
|
|
49
|
+
_buildWhereFromChain(where)
|
|
50
|
+
{
|
|
51
|
+
if (!where || where.length === 0) return { clause: '', values: [] };
|
|
52
|
+
|
|
53
|
+
const parts = [];
|
|
54
|
+
const values = [];
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < where.length; i++)
|
|
57
|
+
{
|
|
58
|
+
const w = where[i];
|
|
59
|
+
|
|
60
|
+
// Handle raw WHERE clauses (from whereRaw)
|
|
61
|
+
if (w.raw)
|
|
62
|
+
{
|
|
63
|
+
const expr = w.raw;
|
|
64
|
+
if (w.params) values.push(...w.params);
|
|
65
|
+
if (i === 0) parts.push(expr);
|
|
66
|
+
else parts.push(`${w.logic} ${expr}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { field, op, value, logic } = w;
|
|
71
|
+
|
|
72
|
+
let expr;
|
|
73
|
+
if (op === 'IS NULL')
|
|
74
|
+
{
|
|
75
|
+
expr = `"${field}" IS NULL`;
|
|
76
|
+
}
|
|
77
|
+
else if (op === 'IS NOT NULL')
|
|
78
|
+
{
|
|
79
|
+
expr = `"${field}" IS NOT NULL`;
|
|
80
|
+
}
|
|
81
|
+
else if (op === 'IN' || op === 'NOT IN')
|
|
82
|
+
{
|
|
83
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
84
|
+
{
|
|
85
|
+
expr = op === 'IN' ? '0' : '1'; // IN () → false, NOT IN () → true
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
{
|
|
89
|
+
const placeholders = value.map(() => '?').join(', ');
|
|
90
|
+
expr = `"${field}" ${op} (${placeholders})`;
|
|
91
|
+
values.push(...value.map(v => this._toSqlValue(v)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (op === 'BETWEEN')
|
|
95
|
+
{
|
|
96
|
+
expr = `"${field}" BETWEEN ? AND ?`;
|
|
97
|
+
values.push(this._toSqlValue(value[0]), this._toSqlValue(value[1]));
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
{
|
|
101
|
+
expr = `"${field}" ${op} ?`;
|
|
102
|
+
values.push(this._toSqlValue(value));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (i === 0) parts.push(expr);
|
|
106
|
+
else parts.push(`${logic} ${expr}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { clause: ' WHERE ' + parts.join(' '), values };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert a JS value to a SQL-safe value.
|
|
114
|
+
* @param {*} value
|
|
115
|
+
* @returns {*}
|
|
116
|
+
* @protected
|
|
117
|
+
*/
|
|
118
|
+
_toSqlValue(value)
|
|
119
|
+
{
|
|
120
|
+
if (value instanceof Date) return value.toISOString();
|
|
121
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
122
|
+
if (typeof value === 'object' && value !== null) return JSON.stringify(value);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format a default value for SQL DDL.
|
|
128
|
+
* @param {*} val
|
|
129
|
+
* @returns {string}
|
|
130
|
+
* @protected
|
|
131
|
+
*/
|
|
132
|
+
_sqlDefault(val)
|
|
133
|
+
{
|
|
134
|
+
if (val === null) return 'NULL';
|
|
135
|
+
if (typeof val === 'number') return String(val);
|
|
136
|
+
if (typeof val === 'boolean') return val ? '1' : '0';
|
|
137
|
+
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
|
|
138
|
+
return 'NULL';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = BaseSqlAdapter;
|