postgres.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. }
  76. }
  77. /**
  78. * Get all of a project's chunk ids
  79. */
  80. async function getProjectChunkIds(projectId) {
  81. projectId = parseInt(projectId, 10)
  82. assert.integer(projectId, 'bad projectId')
  83. const records = await knex('chunks').select('id').where('doc_id', projectId)
  84. return records.map(record => record.id)
  85. }
  86. /**
  87. * Insert a pending chunk before sending it to object storage.
  88. */
  89. async function insertPendingChunk(projectId, chunk) {
  90. projectId = parseInt(projectId, 10)
  91. assert.integer(projectId, 'bad projectId')
  92. const result = await knex.first(
  93. knex.raw("nextval('chunks_id_seq'::regclass)::integer as chunkid")
  94. )
  95. const chunkId = result.chunkid
  96. await knex('pending_chunks').insert({
  97. id: chunkId,
  98. doc_id: projectId,
  99. end_version: chunk.getEndVersion(),
  100. start_version: chunk.getStartVersion(),
  101. end_timestamp: chunk.getEndTimestamp(),
  102. })
  103. return chunkId
  104. }
  105. /**
  106. * Record that a new chunk was created.
  107. */
  108. async function confirmCreate(projectId, chunk, chunkId) {
  109. projectId = parseInt(projectId, 10)
  110. assert.integer(projectId, 'bad projectId')
  111. await knex.transaction(async tx => {
  112. await Promise.all([
  113. _deletePendingChunk(tx, projectId, chunkId),
  114. _insertChunk(tx, projectId, chunk, chunkId),
  115. ])
  116. })
  117. }
  118. /**
  119. * Record that a chunk was replaced by a new one.
  120. */
  121. async function confirmUpdate(projectId, oldChunkId, newChunk, newChunkId) {
  122. projectId = parseInt(projectId, 10)
  123. assert.integer(projectId, 'bad projectId')
  124. await knex.transaction(async tx => {
  125. await _deleteChunks(tx, { doc_id: projectId, id: oldChunkId })
  126. await Promise.all([
  127. _deletePendingChunk(tx, projectId, newChunkId),
  128. _insertChunk(tx, projectId, newChunk, newChunkId),
  129. ])
  130. })
  131. }
  132. async function _deletePendingChunk(tx, projectId, chunkId) {
  133. await tx('pending_chunks')
  134. .where({
  135. doc_id: projectId,
  136. id: chunkId,
  137. })
  138. .del()
  139. }
  140. async function _insertChunk(tx, projectId, chunk, chunkId) {
  141. const startVersion = chunk.getStartVersion()
  142. const endVersion = chunk.getEndVersion()
  143. try {
  144. await tx('chunks').insert({
  145. id: chunkId,
  146. doc_id: projectId,
  147. start_version: startVersion,
  148. end_version: endVersion,
  149. end_timestamp: chunk.getEndTimestamp(),
  150. })
  151. } catch (err) {
  152. if (err.code === DUPLICATE_KEY_ERROR_CODE) {
  153. throw new ChunkVersionConflictError(
  154. 'chunk start or end version is not unique',
  155. { projectId, chunkId, startVersion, endVersion }
  156. )
  157. }
  158. throw err
  159. }
  160. }
  161. /**
  162. * Delete a chunk.
  163. *
  164. * @param {number} projectId
  165. * @param {number} chunkId
  166. * @return {Promise}
  167. */
  168. async function deleteChunk(projectId, chunkId) {
  169. projectId = parseInt(projectId, 10)
  170. assert.integer(projectId, 'bad projectId')
  171. assert.integer(chunkId, 'bad chunkId')
  172. await _deleteChunks(knex, { doc_id: projectId, id: chunkId })
  173. }
  174. /**
  175. * Delete all of a project's chunks
  176. */
  177. async function deleteProjectChunks(projectId) {
  178. projectId = parseInt(projectId, 10)
  179. assert.integer(projectId, 'bad projectId')
  180. knex.transaction(async tx => {
  181. await _deleteChunks(knex, { doc_id: projectId })
  182. })
  183. }
  184. async function _deleteChunks(tx, whereClause) {
  185. const rows = await tx('chunks').returning('*').where(whereClause).del()
  186. if (rows.length === 0) {
  187. return
  188. }
  189. const oldChunks = rows.map(row => ({
  190. doc_id: row.doc_id,
  191. chunk_id: row.id,
  192. start_version: row.start_version,
  193. end_version: row.end_version,
  194. end_timestamp: row.end_timestamp,
  195. deleted_at: tx.fn.now(),
  196. }))
  197. await tx('old_chunks').insert(oldChunks)
  198. }
  199. /**
  200. * Get a batch of old chunks for deletion
  201. */
  202. async function getOldChunksBatch(count, minAgeSecs) {
  203. const maxDeletedAt = new Date(Date.now() - minAgeSecs * 1000)
  204. const records = await knex('old_chunks')
  205. .whereNull('deleted_at')
  206. .orWhere('deleted_at', '<', maxDeletedAt)
  207. .orderBy('chunk_id')
  208. .limit(count)
  209. return records.map(oldChunk => ({
  210. projectId: oldChunk.doc_id.toString(),
  211. chunkId: oldChunk.chunk_id,
  212. }))
  213. }
  214. /**
  215. * Delete a batch of old chunks from the database
  216. */
  217. async function deleteOldChunks(chunkIds) {
  218. await knex('old_chunks').whereIn('chunk_id', chunkIds).del()
  219. }
  220. /**
  221. * Generate a new project id
  222. */
  223. async function generateProjectId() {
  224. const record = await knex.first(
  225. knex.raw("nextval('docs_id_seq'::regclass)::integer as doc_id")
  226. )
  227. return record.doc_id.toString()
  228. }
  229. module.exports = {
  230. getLatestChunk,
  231. getChunkForVersion,
  232. getChunkForTimestamp,
  233. getProjectChunkIds,
  234. insertPendingChunk,
  235. confirmCreate,
  236. confirmUpdate,
  237. deleteChunk,
  238. deleteProjectChunks,
  239. getOldChunksBatch,
  240. deleteOldChunks,
  241. generateProjectId,
  242. }