@aidra/pmx-io 0.0.0-stage → 9.13.0
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 +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- package/standalone.mjs +17 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Shared by the ioredis and node-redis integrations (82 §6, §10): command statements (ITG-032),
|
|
5
|
+
// error codes (ITG-123), the XADD field carrier and the parsing of XREAD/XREADGROUP replies for
|
|
6
|
+
// Redis streams (ITG-210). Same results as pmx-io's _redis.py.
|
|
7
|
+
|
|
8
|
+
const c = require('./common')
|
|
9
|
+
|
|
10
|
+
const MAX_PLACEHOLDERS = 10
|
|
11
|
+
const MAX_ARG = 100
|
|
12
|
+
const MAX_STATEMENT = 2000
|
|
13
|
+
const MAX_PIPELINE_LINES = 20
|
|
14
|
+
const STREAM_COMMANDS = new Set(['XADD', 'XREAD', 'XREADGROUP'])
|
|
15
|
+
|
|
16
|
+
function text (v) {
|
|
17
|
+
if (typeof v === 'string') return v
|
|
18
|
+
if (v instanceof Uint8Array) return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('utf8')
|
|
19
|
+
return String(v)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function secretIndexes (name, args) {
|
|
23
|
+
const out = new Set()
|
|
24
|
+
if (name === 'AUTH') {
|
|
25
|
+
for (let i = 0; i < args.length; i++) out.add(i)
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
const up = args.map((a) => (typeof a === 'string' || a instanceof Uint8Array ? text(a).toUpperCase() : ''))
|
|
29
|
+
if (name === 'HELLO') {
|
|
30
|
+
up.forEach((t, i) => { if (t === 'AUTH') { out.add(i + 1); out.add(i + 2) } })
|
|
31
|
+
} else if (name === 'MIGRATE') {
|
|
32
|
+
up.forEach((t, i) => {
|
|
33
|
+
if (t === 'AUTH') out.add(i + 1)
|
|
34
|
+
else if (t === 'AUTH2') { out.add(i + 1); out.add(i + 2) }
|
|
35
|
+
})
|
|
36
|
+
} else if (name === 'CONFIG' && up[0] === 'SET') {
|
|
37
|
+
for (let i = 2; i < args.length; i += 2) out.add(i)
|
|
38
|
+
} else if (name === 'ACL' && up[0] === 'SETUSER') {
|
|
39
|
+
// the rules carry passwords (`>pw`, `<pw`) and hashes (`#…`, `!…`)
|
|
40
|
+
for (let i = 2; i < args.length; i++) out.add(i)
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** ITG-032: `SET ? ?` (sanitized), the arguments as text (raw, secrets as `?`), or undefined (off). */
|
|
46
|
+
function statement (name, args, mode) {
|
|
47
|
+
if (mode === 'off') return undefined
|
|
48
|
+
args = args || []
|
|
49
|
+
if (mode !== 'raw') {
|
|
50
|
+
const n = args.length
|
|
51
|
+
return name + ' ?'.repeat(Math.min(n, MAX_PLACEHOLDERS)) + (n > MAX_PLACEHOLDERS ? ' …' : '')
|
|
52
|
+
}
|
|
53
|
+
const secret = secretIndexes(name, args)
|
|
54
|
+
const parts = [name]
|
|
55
|
+
for (let i = 0; i < args.length; i++) {
|
|
56
|
+
if (secret.has(i)) {
|
|
57
|
+
parts.push('?')
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
let t = text(args[i])
|
|
61
|
+
if (t.length > MAX_ARG) t = t.slice(0, MAX_ARG) + '…'
|
|
62
|
+
parts.push(t)
|
|
63
|
+
}
|
|
64
|
+
let s = parts.join(' ')
|
|
65
|
+
if (s.length > MAX_STATEMENT) s = s.slice(0, MAX_STATEMENT) + '…'
|
|
66
|
+
return s
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** ITG-032 for a pipeline/transaction: commands = [[name, args]]. */
|
|
70
|
+
function pipelineStatement (commands, mode) {
|
|
71
|
+
if (mode === 'off') return undefined
|
|
72
|
+
const lines = []
|
|
73
|
+
for (const [name, args] of commands.slice(0, MAX_PIPELINE_LINES)) lines.push(statement(name, args, mode) || '')
|
|
74
|
+
if (commands.length > MAX_PIPELINE_LINES) lines.push('… (' + (commands.length - MAX_PIPELINE_LINES) + ' more)')
|
|
75
|
+
let s = lines.join('\n')
|
|
76
|
+
if (s.length > MAX_STATEMENT) s = s.slice(0, MAX_STATEMENT) + '…'
|
|
77
|
+
return s
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** ITG-123: the first word of a Redis error reply (WRONGTYPE, ERR, …), or undefined. */
|
|
81
|
+
function errorCode (err) {
|
|
82
|
+
if (!err || typeof err.message !== 'string') return undefined
|
|
83
|
+
// ioredis ReplyError; node-redis ErrorReply and its subclasses (SimpleError, BlobError)
|
|
84
|
+
let reply = /Reply/.test(String(err.name || ''))
|
|
85
|
+
for (let p = Object.getPrototypeOf(err); !reply && p && p !== Error.prototype; p = Object.getPrototypeOf(p)) {
|
|
86
|
+
if (p.constructor && /Reply/.test(p.constructor.name)) reply = true
|
|
87
|
+
}
|
|
88
|
+
if (!reply) return undefined
|
|
89
|
+
const first = err.message.split(/\s/)[0]
|
|
90
|
+
return /^[A-Z][A-Z0-9_]*$/.test(first) ? first : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const XADD_FLAGS = { NOMKSTREAM: 0, KEEPREF: 0, DELREF: 0, ACKED: 0, IDMPAUTO: 1, IDMP: 2, LIMIT: 1 }
|
|
94
|
+
|
|
95
|
+
/** Index in `args` (the arguments after XADD, key first) of the first field, or -1. */
|
|
96
|
+
function xaddFieldsStart (args) {
|
|
97
|
+
let i = 1
|
|
98
|
+
const n = args.length
|
|
99
|
+
while (i < n) {
|
|
100
|
+
const t = text(args[i]).toUpperCase()
|
|
101
|
+
if (Object.prototype.hasOwnProperty.call(XADD_FLAGS, t)) {
|
|
102
|
+
i += 1 + XADD_FLAGS[t]
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (t === 'MAXLEN' || t === 'MINID') {
|
|
106
|
+
i++
|
|
107
|
+
if (i < n && (text(args[i]) === '=' || text(args[i]) === '~')) i++
|
|
108
|
+
i++
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
const start = i + 1
|
|
112
|
+
if (start >= n || (n - start) % 2) return -1
|
|
113
|
+
return start
|
|
114
|
+
}
|
|
115
|
+
return -1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** ITG-210 with `redisStreamFields`: the trace fields the entry lacks, as a flat [f, v, …]. */
|
|
119
|
+
function xaddExtraFields (args, span) {
|
|
120
|
+
const start = xaddFieldsStart(args)
|
|
121
|
+
if (start < 0) return []
|
|
122
|
+
const present = new Set()
|
|
123
|
+
for (let j = start; j < args.length; j += 2) present.add(text(args[j]).toLowerCase())
|
|
124
|
+
const extra = []
|
|
125
|
+
for (const [k, v] of c.traceHeaders(span)) if (!present.has(k)) extra.push(k, v)
|
|
126
|
+
return extra
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** (stream names, group) of an XREAD / XREADGROUP argument list. */
|
|
130
|
+
function streamNames (name, args) {
|
|
131
|
+
const up = args.map((a) => (typeof a === 'string' || a instanceof Uint8Array ? text(a).toUpperCase() : ''))
|
|
132
|
+
let group
|
|
133
|
+
if (name === 'XREADGROUP') {
|
|
134
|
+
const g = up.indexOf('GROUP')
|
|
135
|
+
if (g !== -1 && g + 1 < args.length) group = text(args[g + 1])
|
|
136
|
+
}
|
|
137
|
+
const s = up.indexOf('STREAMS')
|
|
138
|
+
if (s === -1) return { names: [], group }
|
|
139
|
+
const rest = args.slice(s + 1)
|
|
140
|
+
const half = Math.floor(rest.length / 2)
|
|
141
|
+
return { names: rest.slice(0, half).map(text), group }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Each entry's fields of an XREAD/XREADGROUP reply, as objects or flat arrays. */
|
|
145
|
+
function * streamEntries (resp, depth) {
|
|
146
|
+
depth = depth || 0
|
|
147
|
+
if (depth > 5 || resp === null || resp === undefined) return
|
|
148
|
+
if (Array.isArray(resp)) {
|
|
149
|
+
// an entry: [id, [f, v, …]] or [id, {f: v}]
|
|
150
|
+
if (resp.length === 2 && (typeof resp[0] === 'string' || resp[0] instanceof Uint8Array) &&
|
|
151
|
+
(Array.isArray(resp[1]) || (resp[1] && typeof resp[1] === 'object' && !(resp[1] instanceof Uint8Array))) &&
|
|
152
|
+
isEntryId(resp[0])) {
|
|
153
|
+
yield resp[1]
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
for (const v of resp) if (v && typeof v === 'object') yield * streamEntries(v, depth + 1)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (resp instanceof Map) {
|
|
160
|
+
for (const v of resp.values()) yield * streamEntries(v, depth + 1)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (typeof resp === 'object') {
|
|
164
|
+
// node-redis: { name, messages: [{ id, message: {f: v} }] } or { id, message }
|
|
165
|
+
if (resp.message && typeof resp.message === 'object' && resp.id !== undefined) {
|
|
166
|
+
yield resp.message
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(resp.messages)) {
|
|
170
|
+
yield * streamEntries(resp.messages, depth + 1)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
for (const k of Object.keys(resp)) yield * streamEntries(resp[k], depth + 1)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isEntryId (v) {
|
|
178
|
+
return /^\d+-\d+$/.test(text(v))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function field (fields, name) {
|
|
182
|
+
if (Array.isArray(fields)) {
|
|
183
|
+
for (let i = 0; i + 1 < fields.length; i += 2) if (text(fields[i]).toLowerCase() === name) return text(fields[i + 1])
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
if (fields instanceof Map) {
|
|
187
|
+
for (const [k, v] of fields) if (text(k).toLowerCase() === name) return text(v)
|
|
188
|
+
return undefined
|
|
189
|
+
}
|
|
190
|
+
for (const k of Object.keys(fields)) if (k.toLowerCase() === name) return text(fields[k])
|
|
191
|
+
return undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** { contexts, count } of the entries of an XREAD/XREADGROUP reply. */
|
|
195
|
+
function streamContexts (resp) {
|
|
196
|
+
const contexts = []
|
|
197
|
+
let count = 0
|
|
198
|
+
for (const fields of streamEntries(resp)) {
|
|
199
|
+
count++
|
|
200
|
+
const tp = field(fields, 'traceparent')
|
|
201
|
+
if (tp === undefined) continue
|
|
202
|
+
const carrier = { traceparent: tp }
|
|
203
|
+
const ts = field(fields, 'tracestate')
|
|
204
|
+
if (ts) carrier.tracestate = ts
|
|
205
|
+
const bg = field(fields, 'baggage')
|
|
206
|
+
if (bg) carrier.baggage = bg
|
|
207
|
+
const ctx = c.extract(carrier)
|
|
208
|
+
if (ctx) contexts.push(ctx)
|
|
209
|
+
}
|
|
210
|
+
return { contexts, count }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The messaging span of a stream command (ITG-210), or null when messaging is off / no span:
|
|
215
|
+
* { span, extra (fields to append to XADD), done(result, err) }.
|
|
216
|
+
*/
|
|
217
|
+
function streamCall (name, args, peer) {
|
|
218
|
+
if (!c.msgTracer()) return null
|
|
219
|
+
if (name === 'XADD') {
|
|
220
|
+
const dest = args.length ? text(args[0]) : undefined
|
|
221
|
+
const span = c.producer('redis', dest, peer)
|
|
222
|
+
if (!span) return { span: null, extra: [], done () {} }
|
|
223
|
+
const t = c.msgTracer()
|
|
224
|
+
const extra = t && t.messaging.redisStreamFields ? xaddExtraFields(args, span) : []
|
|
225
|
+
return {
|
|
226
|
+
span,
|
|
227
|
+
extra,
|
|
228
|
+
done (result, err) {
|
|
229
|
+
if (!err && result !== null && result !== undefined) span.setAttribute('messaging.message.id', text(result))
|
|
230
|
+
c.end(span, err)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const { names, group } = streamNames(name, args)
|
|
235
|
+
const tags = Object.assign({}, peer)
|
|
236
|
+
if (group) tags['messaging.consumer.group.name'] = group
|
|
237
|
+
// created now (the span covers only the call), kept only when entries came back
|
|
238
|
+
const span = c.batchConsumer('redis', names.length === 1 ? names[0] : undefined, null, tags, 'receive')
|
|
239
|
+
return {
|
|
240
|
+
span,
|
|
241
|
+
extra: [],
|
|
242
|
+
done (result, err) {
|
|
243
|
+
if (!span) return
|
|
244
|
+
if (err) return c.end(span, err)
|
|
245
|
+
const { contexts, count } = streamContexts(result)
|
|
246
|
+
if (count === 0) return // nothing returned: no span (ITG-023)
|
|
247
|
+
if (count >= 2) span.setAttribute('messaging.batch.message_count', String(count))
|
|
248
|
+
for (const ctx of contexts) span.addLink(ctx)
|
|
249
|
+
c.end(span)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
statement,
|
|
256
|
+
pipelineStatement,
|
|
257
|
+
errorCode,
|
|
258
|
+
xaddFieldsStart,
|
|
259
|
+
xaddExtraFields,
|
|
260
|
+
streamNames,
|
|
261
|
+
streamContexts,
|
|
262
|
+
streamCall,
|
|
263
|
+
text,
|
|
264
|
+
STREAM_COMMANDS
|
|
265
|
+
}
|
package/lib/itg/sql.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Statement helpers for database spans (80 PYI-122/PYI-123, 82 ITG-030/ITG-031; same results as
|
|
5
|
+
// pmx-io's _sql.py): the operation keyword and the sanitised statement text (literals → `?`, bind
|
|
6
|
+
// placeholders kept, comments removed, whitespace collapsed, 2000 characters). One pass over the
|
|
7
|
+
// text; results cached per statement (bounded).
|
|
8
|
+
|
|
9
|
+
const MAX_STATEMENT = 2000
|
|
10
|
+
const CACHE_MAX = 512
|
|
11
|
+
// statements longer than this are not cached: a cache of 512 multi-megabyte INSERT texts (bulk
|
|
12
|
+
// loads with inline values, all distinct) would keep them all alive
|
|
13
|
+
const CACHE_KEY_MAX = 8192
|
|
14
|
+
|
|
15
|
+
const isWs = (c) => c === ' ' || c === '\t' || c === '\r' || c === '\n' || c === '\f' || c === '\v'
|
|
16
|
+
const isIdentStart = (c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'
|
|
17
|
+
const isDigit = (c) => c >= '0' && c <= '9'
|
|
18
|
+
const isIdent = (c) => isIdentStart(c) || isDigit(c) || c === '$'
|
|
19
|
+
const isHexD = (c) => isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c === '_'
|
|
20
|
+
const isAlpha = (c) => /\p{L}/u.test(c)
|
|
21
|
+
|
|
22
|
+
const opCache = new Map()
|
|
23
|
+
const pgCache = new Map()
|
|
24
|
+
const myCache = new Map()
|
|
25
|
+
|
|
26
|
+
function cached (map, key, compute) {
|
|
27
|
+
if (key.length > CACHE_KEY_MAX) return compute(key)
|
|
28
|
+
let v = map.get(key)
|
|
29
|
+
if (v !== undefined) return v
|
|
30
|
+
v = compute(key)
|
|
31
|
+
if (map.size >= CACHE_MAX) map.clear()
|
|
32
|
+
map.set(key, v)
|
|
33
|
+
return v
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function blockCommentEnd (s, i) {
|
|
37
|
+
// PostgreSQL block comments nest
|
|
38
|
+
let depth = 0
|
|
39
|
+
const n = s.length
|
|
40
|
+
while (i < n) {
|
|
41
|
+
if (s.startsWith('/*', i)) {
|
|
42
|
+
depth++
|
|
43
|
+
i += 2
|
|
44
|
+
} else if (s.startsWith('*/', i)) {
|
|
45
|
+
depth--
|
|
46
|
+
i += 2
|
|
47
|
+
if (depth === 0) return i
|
|
48
|
+
} else {
|
|
49
|
+
i++
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return n
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function skipWsComments (s, i) {
|
|
56
|
+
const n = s.length
|
|
57
|
+
while (i < n) {
|
|
58
|
+
const c = s[i]
|
|
59
|
+
if (isWs(c) || c === '(') i++
|
|
60
|
+
else if (c === '-' && s.startsWith('--', i)) {
|
|
61
|
+
const j = s.indexOf('\n', i)
|
|
62
|
+
i = j < 0 ? n : j + 1
|
|
63
|
+
} else if (c === '/' && s.startsWith('/*', i)) {
|
|
64
|
+
i = blockCommentEnd(s, i)
|
|
65
|
+
} else break
|
|
66
|
+
}
|
|
67
|
+
return i
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** PYI-122: the first keyword after leading whitespace and comments, uppercased, ≤ 16 chars. */
|
|
71
|
+
function operation (statement) {
|
|
72
|
+
if (typeof statement !== 'string') return ''
|
|
73
|
+
return cached(opCache, statement, (s) => {
|
|
74
|
+
const i = skipWsComments(s, 0)
|
|
75
|
+
let j = i
|
|
76
|
+
while (j < s.length && isAlpha(s[j]) && j - i < 16) j++
|
|
77
|
+
return s.slice(i, j).toUpperCase()
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function truncate (text) {
|
|
82
|
+
if (text.length > MAX_STATEMENT) return text.slice(0, MAX_STATEMENT - 1) + '…'
|
|
83
|
+
return text
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function quotedEnd (s, i, quote, backslash) {
|
|
87
|
+
const n = s.length
|
|
88
|
+
i++
|
|
89
|
+
while (i < n) {
|
|
90
|
+
const c = s[i]
|
|
91
|
+
if (backslash && c === '\\') {
|
|
92
|
+
i += 2
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (c === quote) {
|
|
96
|
+
if (i + 1 < n && s[i + 1] === quote) {
|
|
97
|
+
i += 2
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
return i + 1
|
|
101
|
+
}
|
|
102
|
+
i++
|
|
103
|
+
}
|
|
104
|
+
return n
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dollarTag (s, i) {
|
|
108
|
+
const n = s.length
|
|
109
|
+
let j = i + 1
|
|
110
|
+
if (j < n && s[j] === '$') return '$$'
|
|
111
|
+
if (j < n && isIdentStart(s[j])) {
|
|
112
|
+
j++
|
|
113
|
+
while (j < n && isIdent(s[j]) && s[j] !== '$') j++
|
|
114
|
+
if (j < n && s[j] === '$') return s.slice(i, j + 1)
|
|
115
|
+
}
|
|
116
|
+
return ''
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function numberEnd (s, i) {
|
|
120
|
+
const n = s.length
|
|
121
|
+
if (s[i] === '0' && i + 1 < n && 'xXoObB'.includes(s[i + 1])) {
|
|
122
|
+
let j = i + 2
|
|
123
|
+
while (j < n && isHexD(s[j])) j++
|
|
124
|
+
return j
|
|
125
|
+
}
|
|
126
|
+
let j = i
|
|
127
|
+
while (j < n && (isDigit(s[j]) || s[j] === '_')) j++
|
|
128
|
+
if (j < n && s[j] === '.') {
|
|
129
|
+
j++
|
|
130
|
+
while (j < n && (isDigit(s[j]) || s[j] === '_')) j++
|
|
131
|
+
}
|
|
132
|
+
if (j < n && (s[j] === 'e' || s[j] === 'E')) {
|
|
133
|
+
let k = j + 1
|
|
134
|
+
if (k < n && (s[k] === '+' || s[k] === '-')) k++
|
|
135
|
+
if (k < n && isDigit(s[k])) {
|
|
136
|
+
j = k
|
|
137
|
+
while (j < n && isDigit(s[j])) j++
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return j
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class Out {
|
|
144
|
+
constructor () {
|
|
145
|
+
this.parts = []
|
|
146
|
+
this.pending = false
|
|
147
|
+
this.len = 0
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
emit (text) {
|
|
151
|
+
if (this.pending && this.parts.length) {
|
|
152
|
+
this.parts.push(' ')
|
|
153
|
+
this.len++
|
|
154
|
+
}
|
|
155
|
+
this.pending = false
|
|
156
|
+
this.parts.push(text)
|
|
157
|
+
this.len += text.length
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
last () {
|
|
161
|
+
const p = this.parts[this.parts.length - 1]
|
|
162
|
+
return p ? p[p.length - 1] : ''
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** True once the text is past the statement limit (the rest of the input is not read). */
|
|
166
|
+
full () {
|
|
167
|
+
return this.len > MAX_STATEMENT
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
text () {
|
|
171
|
+
return truncate(this.parts.join(''))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const PG_PREFIXES = new Set(['e', 'E', 'b', 'B', 'x', 'X', 'n', 'N'])
|
|
176
|
+
|
|
177
|
+
function sanitizePg (s) {
|
|
178
|
+
const out = new Out()
|
|
179
|
+
const n = s.length
|
|
180
|
+
let i = 0
|
|
181
|
+
while (i < n && !out.full()) {
|
|
182
|
+
const c = s[i]
|
|
183
|
+
if (isWs(c)) {
|
|
184
|
+
out.pending = true
|
|
185
|
+
i++
|
|
186
|
+
} else if (c === '-' && s.startsWith('--', i)) {
|
|
187
|
+
const j = s.indexOf('\n', i)
|
|
188
|
+
i = j < 0 ? n : j
|
|
189
|
+
out.pending = true
|
|
190
|
+
} else if (c === '/' && s.startsWith('/*', i)) {
|
|
191
|
+
i = blockCommentEnd(s, i)
|
|
192
|
+
out.pending = true
|
|
193
|
+
} else if (c === "'") {
|
|
194
|
+
i = quotedEnd(s, i, "'", false)
|
|
195
|
+
out.emit('?')
|
|
196
|
+
} else if (c === '"') {
|
|
197
|
+
const j = quotedEnd(s, i, '"', false)
|
|
198
|
+
out.emit(s.slice(i, j))
|
|
199
|
+
i = j
|
|
200
|
+
} else if (c === '$') {
|
|
201
|
+
const tag = dollarTag(s, i)
|
|
202
|
+
if (tag && !(isIdent(out.last()) && !out.pending)) {
|
|
203
|
+
const end = s.indexOf(tag, i + tag.length)
|
|
204
|
+
i = end < 0 ? n : end + tag.length
|
|
205
|
+
out.emit('?')
|
|
206
|
+
} else {
|
|
207
|
+
let j = i + 1
|
|
208
|
+
while (j < n && isDigit(s[j])) j++
|
|
209
|
+
out.emit(s.slice(i, j)) // bind placeholder $1 (or a lone '$')
|
|
210
|
+
i = j
|
|
211
|
+
}
|
|
212
|
+
} else if (isIdentStart(c)) {
|
|
213
|
+
let j = i + 1
|
|
214
|
+
while (j < n && isIdent(s[j])) j++
|
|
215
|
+
const word = s.slice(i, j)
|
|
216
|
+
if (j < n && s[j] === "'" && PG_PREFIXES.has(word)) {
|
|
217
|
+
i = quotedEnd(s, j, "'", word === 'e' || word === 'E')
|
|
218
|
+
out.emit('?')
|
|
219
|
+
} else if (j + 1 < n && s[j] === '&' && s[j + 1] === "'" && (word === 'u' || word === 'U')) {
|
|
220
|
+
i = quotedEnd(s, j + 1, "'", false)
|
|
221
|
+
out.emit('?')
|
|
222
|
+
} else {
|
|
223
|
+
out.emit(word)
|
|
224
|
+
i = j
|
|
225
|
+
}
|
|
226
|
+
} else if (isDigit(c) || (c === '.' && i + 1 < n && isDigit(s[i + 1]))) {
|
|
227
|
+
i = numberEnd(s, i)
|
|
228
|
+
out.emit('?')
|
|
229
|
+
} else {
|
|
230
|
+
out.emit(c)
|
|
231
|
+
i++
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out.text()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const MY_PREFIXES = new Set(['x', 'X', 'b', 'B', 'n', 'N'])
|
|
238
|
+
|
|
239
|
+
function sanitizeMysql (s) {
|
|
240
|
+
const out = new Out()
|
|
241
|
+
const n = s.length
|
|
242
|
+
let i = 0
|
|
243
|
+
while (i < n && !out.full()) {
|
|
244
|
+
const c = s[i]
|
|
245
|
+
if (isWs(c)) {
|
|
246
|
+
out.pending = true
|
|
247
|
+
i++
|
|
248
|
+
} else if (c === '#' || (c === '-' && s.startsWith('--', i) && (i + 2 >= n || isWs(s[i + 2])))) {
|
|
249
|
+
const j = s.indexOf('\n', i)
|
|
250
|
+
i = j < 0 ? n : j
|
|
251
|
+
out.pending = true
|
|
252
|
+
} else if (c === '/' && s.startsWith('/*', i)) {
|
|
253
|
+
const j = s.indexOf('*/', i + 2) // MySQL comments do not nest
|
|
254
|
+
i = j < 0 ? n : j + 2
|
|
255
|
+
out.pending = true
|
|
256
|
+
} else if (c === "'" || c === '"') {
|
|
257
|
+
i = quotedEnd(s, i, c, true)
|
|
258
|
+
out.emit('?')
|
|
259
|
+
} else if (c === '`') {
|
|
260
|
+
const j = quotedEnd(s, i, '`', false)
|
|
261
|
+
out.emit(s.slice(i, j))
|
|
262
|
+
i = j
|
|
263
|
+
} else if (isIdentStart(c)) {
|
|
264
|
+
let j = i + 1
|
|
265
|
+
while (j < n && isIdent(s[j])) j++
|
|
266
|
+
const word = s.slice(i, j)
|
|
267
|
+
if (j < n && s[j] === "'" && (MY_PREFIXES.has(word) || word.startsWith('_'))) {
|
|
268
|
+
i = quotedEnd(s, j, "'", !['x', 'X', 'b', 'B'].includes(word))
|
|
269
|
+
out.emit('?')
|
|
270
|
+
} else {
|
|
271
|
+
out.emit(word)
|
|
272
|
+
i = j
|
|
273
|
+
}
|
|
274
|
+
} else if (isDigit(c) || (c === '.' && i + 1 < n && isDigit(s[i + 1]))) {
|
|
275
|
+
i = numberEnd(s, i)
|
|
276
|
+
out.emit('?')
|
|
277
|
+
} else {
|
|
278
|
+
out.emit(c)
|
|
279
|
+
i++
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out.text()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** PYI-123 `sanitized` mode for PostgreSQL (cached). */
|
|
286
|
+
function sanitize (statement) {
|
|
287
|
+
return cached(pgCache, statement, sanitizePg)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** 82 ITG-031 `sanitized` mode for MySQL (cached). */
|
|
291
|
+
function sanitizeMySQL (statement) {
|
|
292
|
+
return cached(myCache, statement, sanitizeMysql)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The `db.statement` tag for `mode` (`sanitized` | `raw` | `off`), or undefined. */
|
|
296
|
+
function statementTag (statement, mode, mysql) {
|
|
297
|
+
if (mode === 'off' || typeof statement !== 'string') return undefined
|
|
298
|
+
if (mode === 'raw') return truncate(statement)
|
|
299
|
+
return mysql ? sanitizeMySQL(statement) : sanitize(statement)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = { operation, sanitize, sanitizeMySQL, statementTag, truncate, MAX_STATEMENT }
|