serializer.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import { Writer } from './buffer-writer'
  2. const enum code {
  3. startup = 0x70,
  4. query = 0x51,
  5. parse = 0x50,
  6. bind = 0x42,
  7. execute = 0x45,
  8. flush = 0x48,
  9. sync = 0x53,
  10. end = 0x58,
  11. close = 0x43,
  12. describe = 0x44,
  13. copyFromChunk = 0x64,
  14. copyDone = 0x63,
  15. copyFail = 0x66,
  16. }
  17. const writer = new Writer()
  18. const startup = (opts: Record<string, string>): Buffer => {
  19. // protocol version
  20. writer.addInt16(3).addInt16(0)
  21. for (const key of Object.keys(opts)) {
  22. writer.addCString(key).addCString(opts[key])
  23. }
  24. writer.addCString('client_encoding').addCString('UTF8')
  25. var bodyBuffer = writer.addCString('').flush()
  26. // this message is sent without a code
  27. var length = bodyBuffer.length + 4
  28. return new Writer().addInt32(length).add(bodyBuffer).flush()
  29. }
  30. const requestSsl = (): Buffer => {
  31. const response = Buffer.allocUnsafe(8)
  32. response.writeInt32BE(8, 0)
  33. response.writeInt32BE(80877103, 4)
  34. return response
  35. }
  36. const password = (password: string): Buffer => {
  37. return writer.addCString(password).flush(code.startup)
  38. }
  39. const sendSASLInitialResponseMessage = function (mechanism: string, initialResponse: string): Buffer {
  40. // 0x70 = 'p'
  41. writer.addCString(mechanism).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse)
  42. return writer.flush(code.startup)
  43. }
  44. const sendSCRAMClientFinalMessage = function (additionalData: string): Buffer {
  45. return writer.addString(additionalData).flush(code.startup)
  46. }
  47. const query = (text: string): Buffer => {
  48. return writer.addCString(text).flush(code.query)
  49. }
  50. type ParseOpts = {
  51. name?: string
  52. types?: number[]
  53. text: string
  54. }
  55. const emptyArray: any[] = []
  56. const parse = (query: ParseOpts): Buffer => {
  57. // expect something like this:
  58. // { name: 'queryName',
  59. // text: 'select * from blah',
  60. // types: ['int8', 'bool'] }
  61. // normalize missing query names to allow for null
  62. const name = query.name || ''
  63. if (name.length > 63) {
  64. /* eslint-disable no-console */
  65. console.error('Warning! Postgres only supports 63 characters for query names.')
  66. console.error('You supplied %s (%s)', name, name.length)
  67. console.error('This can cause conflicts and silent errors executing queries')
  68. /* eslint-enable no-console */
  69. }
  70. const types = query.types || emptyArray
  71. var len = types.length
  72. var buffer = writer
  73. .addCString(name) // name of query
  74. .addCString(query.text) // actual query text
  75. .addInt16(len)
  76. for (var i = 0; i < len; i++) {
  77. buffer.addInt32(types[i])
  78. }
  79. return writer.flush(code.parse)
  80. }
  81. type ValueMapper = (param: any, index: number) => any
  82. type BindOpts = {
  83. portal?: string
  84. binary?: boolean
  85. statement?: string
  86. values?: any[]
  87. // optional map from JS value to postgres value per parameter
  88. valueMapper?: ValueMapper
  89. }
  90. const paramWriter = new Writer()
  91. // make this a const enum so typescript will inline the value
  92. const enum ParamType {
  93. STRING = 0,
  94. BINARY = 1,
  95. }
  96. const writeValues = function (values: any[], valueMapper?: ValueMapper): void {
  97. for (let i = 0; i < values.length; i++) {
  98. const mappedVal = valueMapper ? valueMapper(values[i], i) : values[i]
  99. if (mappedVal == null) {
  100. // add the param type (string) to the writer
  101. writer.addInt16(ParamType.STRING)
  102. // write -1 to the param writer to indicate null
  103. paramWriter.addInt32(-1)
  104. } else if (mappedVal instanceof Buffer) {
  105. // add the param type (binary) to the writer
  106. writer.addInt16(ParamType.BINARY)
  107. // add the buffer to the param writer
  108. paramWriter.addInt32(mappedVal.length)
  109. paramWriter.add(mappedVal)
  110. } else {
  111. // add the param type (string) to the writer
  112. writer.addInt16(ParamType.STRING)
  113. paramWriter.addInt32(Buffer.byteLength(mappedVal))
  114. paramWriter.addString(mappedVal)
  115. }
  116. }
  117. }
  118. const bind = (config: BindOpts = {}): Buffer => {
  119. // normalize config
  120. const portal = config.portal || ''
  121. const statement = config.statement || ''
  122. const binary = config.binary || false
  123. const values = config.values || emptyArray
  124. const len = values.length
  125. writer.addCString(portal).addCString(statement)
  126. writer.addInt16(len)
  127. writeValues(values, config.valueMapper)
  128. writer.addInt16(len)
  129. writer.add(paramWriter.flush())
  130. // format code
  131. writer.addInt16(binary ? ParamType.BINARY : ParamType.STRING)
  132. return writer.flush(code.bind)
  133. }
  134. type ExecOpts = {
  135. portal?: string
  136. rows?: number
  137. }
  138. const emptyExecute = Buffer.from([code.execute, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])
  139. const execute = (config?: ExecOpts): Buffer => {
  140. // this is the happy path for most queries
  141. if (!config || (!config.portal && !config.rows)) {
  142. return emptyExecute
  143. }
  144. const portal = config.portal || ''
  145. const rows = config.rows || 0
  146. const portalLength = Buffer.byteLength(portal)
  147. const len = 4 + portalLength + 1 + 4
  148. // one extra bit for code
  149. const buff = Buffer.allocUnsafe(1 + len)
  150. buff[0] = code.execute
  151. buff.writeInt32BE(len, 1)
  152. buff.write(portal, 5, 'utf-8')
  153. buff[portalLength + 5] = 0 // null terminate portal cString
  154. buff.writeUInt32BE(rows, buff.length - 4)
  155. return buff
  156. }
  157. const cancel = (processID: number, secretKey: number): Buffer => {
  158. const buffer = Buffer.allocUnsafe(16)
  159. buffer.writeInt32BE(16, 0)
  160. buffer.writeInt16BE(1234, 4)
  161. buffer.writeInt16BE(5678, 6)
  162. buffer.writeInt32BE(processID, 8)
  163. buffer.writeInt32BE(secretKey, 12)
  164. return buffer
  165. }
  166. type PortalOpts = {
  167. type: 'S' | 'P'
  168. name?: string
  169. }
  170. const cstringMessage = (code: code, string: string): Buffer => {
  171. const stringLen = Buffer.byteLength(string)
  172. const len = 4 + stringLen + 1
  173. // one extra bit for code
  174. const buffer = Buffer.allocUnsafe(1 + len)
  175. buffer[0] = code
  176. buffer.writeInt32BE(len, 1)
  177. buffer.write(string, 5, 'utf-8')
  178. buffer[len] = 0 // null terminate cString
  179. return buffer
  180. }
  181. const emptyDescribePortal = writer.addCString('P').flush(code.describe)
  182. const emptyDescribeStatement = writer.addCString('S').flush(code.describe)
  183. const describe = (msg: PortalOpts): Buffer => {
  184. return msg.name
  185. ? cstringMessage(code.describe, `${msg.type}${msg.name || ''}`)
  186. : msg.type === 'P'
  187. ? emptyDescribePortal
  188. : emptyDescribeStatement
  189. }
  190. const close = (msg: PortalOpts): Buffer => {
  191. const text = `${msg.type}${msg.name || ''}`
  192. return cstringMessage(code.close, text)
  193. }
  194. const copyData = (chunk: Buffer): Buffer => {
  195. return writer.add(chunk).flush(code.copyFromChunk)
  196. }
  197. const copyFail = (message: string): Buffer => {
  198. return cstringMessage(code.copyFail, message)
  199. }
  200. const codeOnlyBuffer = (code: code): Buffer => Buffer.from([code, 0x00, 0x00, 0x00, 0x04])
  201. const flushBuffer = codeOnlyBuffer(code.flush)
  202. const syncBuffer = codeOnlyBuffer(code.sync)
  203. const endBuffer = codeOnlyBuffer(code.end)
  204. const copyDoneBuffer = codeOnlyBuffer(code.copyDone)
  205. const serialize = {
  206. startup,
  207. password,
  208. requestSsl,
  209. sendSASLInitialResponseMessage,
  210. sendSCRAMClientFinalMessage,
  211. query,
  212. parse,
  213. bind,
  214. execute,
  215. describe,
  216. close,
  217. flush: () => flushBuffer,
  218. sync: () => syncBuffer,
  219. end: () => endBuffer,
  220. copyData,
  221. copyDone: () => copyDoneBuffer,
  222. copyFail,
  223. cancel,
  224. }
  225. export { serialize }