postgres.js 9.2 KB

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