postgres.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. const { Chunk } = require('overleaf-editor-core')
  2. const assert = require('../assert')
  3. const knex = require('../knex')
  4. const { ChunkVersionConflictError } = require('./errors')
  5. const DUPLICATE_KEY_ERROR_CODE = '23505'
  6. /**
  7. * Get the latest chunk's metadata from the database
  8. */
  9. async function getLatestChunk(projectId) {
  10. projectId = parseInt(projectId, 10)
  11. assert.integer(projectId, 'bad projectId')
  12. const record = await knex('chunks')
  13. .where('doc_id', projectId)
  14. .orderBy('end_version', 'desc')
  15. .first()
  16. if (record == null) {
  17. return null
  18. }
  19. return chunkFromRecord(record)
  20. }
  21. /**
  22. * Get the metadata for the chunk that contains the given version.
  23. */
  24. async function getChunkForVersion(projectId, version) {
  25. projectId = parseInt(projectId, 10)
  26. assert.integer(projectId, 'bad projectId')
  27. const record = await knex('chunks')
  28. .where('doc_id', projectId)
  29. .where('end_version', '>=', version)
  30. .orderBy('end_version')
  31. .first()
  32. if (!record) {
  33. throw new Chunk.VersionNotFoundError(projectId, version)
  34. }
  35. return chunkFromRecord(record)
  36. }
  37. /**
  38. * Get the metadata for the chunk that contains the version that was current at
  39. * the given timestamp.
  40. */
  41. async function getChunkForTimestamp(projectId, timestamp) {
  42. projectId = parseInt(projectId, 10)
  43. assert.integer(projectId, 'bad projectId')
  44. // This query will find the latest chunk after the timestamp (query orders
  45. // in reverse chronological order), OR the latest chunk
  46. // This accounts for the case where the timestamp is ahead of the chunk's
  47. // timestamp and therefore will not return any results
  48. const whereAfterEndTimestampOrLatestChunk = knex.raw(
  49. 'end_timestamp >= ? ' +
  50. 'OR id = ( ' +
  51. 'SELECT id FROM chunks ' +
  52. 'WHERE doc_id = ? ' +
  53. 'ORDER BY end_version desc LIMIT 1' +
  54. ')',
  55. [timestamp, projectId]
  56. )
  57. const record = await knex('chunks')
  58. .where('doc_id', projectId)
  59. .where(whereAfterEndTimestampOrLatestChunk)
  60. .orderBy('end_version')
  61. .first()
  62. if (!record) {
  63. throw new Chunk.BeforeTimestampNotFoundError(projectId, timestamp)
  64. }
  65. return chunkFromRecord(record)
  66. }
  67. /**
  68. * Build a chunk metadata object from the database record
  69. */
  70. function chunkFromRecord(record) {
  71. return {
  72. id: record.id,
  73. startVersion: record.start_version,
  74. endVersion: record.end_version,
  75. endTimestamp: record.end_timestamp,
  76. }
  77. }
  78. /**
  79. * Get all of a project's chunk ids
  80. */
  81. async function getProjectChunkIds(projectId) {
  82. projectId = parseInt(projectId, 10)
  83. assert.integer(projectId, 'bad projectId')
  84. const records = await knex('chunks').select('id').where('doc_id', projectId)
  85. return records.map(record => record.id)
  86. }
  87. /**
  88. * Insert a pending chunk before sending it to object storage.
  89. */
  90. async function insertPendingChunk(projectId, chunk) {
  91. projectId = parseInt(projectId, 10)
  92. assert.integer(projectId, 'bad projectId')
  93. const result = await knex.first(
  94. knex.raw("nextval('chunks_id_seq'::regclass)::integer as chunkid")
  95. )
  96. const chunkId = result.chunkid
  97. await knex('pending_chunks').insert({
  98. id: chunkId,
  99. doc_id: projectId,
  100. end_version: chunk.getEndVersion(),
  101. start_version: chunk.getStartVersion(),
  102. end_timestamp: chunk.getEndTimestamp(),
  103. })
  104. return chunkId
  105. }
  106. /**
  107. * Record that a new chunk was created.
  108. */
  109. async function confirmCreate(projectId, chunk, chunkId) {
  110. projectId = parseInt(projectId, 10)
  111. assert.integer(projectId, 'bad projectId')
  112. await knex.transaction(async tx => {
  113. await Promise.all([
  114. _deletePendingChunk(tx, projectId, chunkId),
  115. _insertChunk(tx, projectId, chunk, chunkId),
  116. ])
  117. })
  118. }
  119. /**
  120. * Record that a chunk was replaced by a new one.
  121. */
  122. async function confirmUpdate(projectId, oldChunkId, newChunk, newChunkId) {
  123. projectId = parseInt(projectId, 10)
  124. assert.integer(projectId, 'bad projectId')
  125. await knex.transaction(async tx => {
  126. await _deleteChunks(tx, { doc_id: projectId, id: oldChunkId })
  127. await Promise.all([
  128. _deletePendingChunk(tx, projectId, newChunkId),
  129. _insertChunk(tx, projectId, newChunk, newChunkId),
  130. ])
  131. })
  132. }
  133. async function _deletePendingChunk(tx, projectId, chunkId) {
  134. await tx('pending_chunks')
  135. .where({
  136. doc_id: projectId,
  137. id: chunkId,
  138. })
  139. .del()
  140. }
  141. async function _insertChunk(tx, projectId, chunk, chunkId) {
  142. const startVersion = chunk.getStartVersion()
  143. const endVersion = chunk.getEndVersion()
  144. try {
  145. await tx('chunks').insert({
  146. id: chunkId,
  147. doc_id: projectId,
  148. start_version: startVersion,
  149. end_version: endVersion,
  150. end_timestamp: chunk.getEndTimestamp(),
  151. })
  152. } catch (err) {
  153. if (err.code === DUPLICATE_KEY_ERROR_CODE) {
  154. throw new ChunkVersionConflictError(
  155. 'chunk start or end version is not unique',
  156. { projectId, chunkId, startVersion, endVersion }
  157. )
  158. }
  159. throw err
  160. }
  161. }
  162. /**
  163. * Delete a chunk.
  164. *
  165. * @param {number} projectId
  166. * @param {number} chunkId
  167. * @return {Promise}
  168. */
  169. async function deleteChunk(projectId, chunkId) {
  170. projectId = parseInt(projectId, 10)
  171. assert.integer(projectId, 'bad projectId')
  172. assert.integer(chunkId, 'bad chunkId')
  173. await _deleteChunks(knex, { doc_id: projectId, id: chunkId })
  174. }
  175. /**
  176. * Delete all of a project's chunks
  177. */
  178. async function deleteProjectChunks(projectId) {
  179. projectId = parseInt(projectId, 10)
  180. assert.integer(projectId, 'bad projectId')
  181. await knex.transaction(async tx => {
  182. await _deleteChunks(knex, { doc_id: projectId })
  183. })
  184. }
  185. async function _deleteChunks(tx, whereClause) {
  186. const rows = await tx('chunks').returning('*').where(whereClause).del()
  187. if (rows.length === 0) {
  188. return
  189. }
  190. const oldChunks = rows.map(row => ({
  191. doc_id: row.doc_id,
  192. chunk_id: row.id,
  193. start_version: row.start_version,
  194. end_version: row.end_version,
  195. end_timestamp: row.end_timestamp,
  196. deleted_at: tx.fn.now(),
  197. }))
  198. await tx('old_chunks').insert(oldChunks)
  199. }
  200. /**
  201. * Get a batch of old chunks for deletion
  202. */
  203. async function getOldChunksBatch(count, minAgeSecs) {
  204. const maxDeletedAt = new Date(Date.now() - minAgeSecs * 1000)
  205. const records = await knex('old_chunks')
  206. .whereNull('deleted_at')
  207. .orWhere('deleted_at', '<', maxDeletedAt)
  208. .orderBy('chunk_id')
  209. .limit(count)
  210. return records.map(oldChunk => ({
  211. projectId: oldChunk.doc_id.toString(),
  212. chunkId: oldChunk.chunk_id,
  213. }))
  214. }
  215. /**
  216. * Delete a batch of old chunks from the database
  217. */
  218. async function deleteOldChunks(chunkIds) {
  219. await knex('old_chunks').whereIn('chunk_id', chunkIds).del()
  220. }
  221. /**
  222. * Generate a new project id
  223. */
  224. async function generateProjectId() {
  225. const record = await knex.first(
  226. knex.raw("nextval('docs_id_seq'::regclass)::integer as doc_id")
  227. )
  228. return record.doc_id.toString()
  229. }
  230. module.exports = {
  231. getLatestChunk,
  232. getChunkForVersion,
  233. getChunkForTimestamp,
  234. getProjectChunkIds,
  235. insertPendingChunk,
  236. confirmCreate,
  237. confirmUpdate,
  238. deleteChunk,
  239. deleteProjectChunks,
  240. getOldChunksBatch,
  241. deleteOldChunks,
  242. generateProjectId,
  243. }