mirror of
https://github.com/overleaf/overleaf.git
synced 2025-12-05 01:10:29 +00:00
Compare commits
26 Commits
ff267ca246
...
2f5e629d65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f5e629d65 | ||
|
|
cc49eeacbd | ||
|
|
a8734e191e | ||
|
|
bfd00a8151 | ||
|
|
07166bff73 | ||
|
|
0200ad7515 | ||
|
|
98069966ba | ||
|
|
d7cd65d20c | ||
|
|
daa4bd9062 | ||
|
|
aef9639405 | ||
|
|
92792828bf | ||
|
|
9b2fcbe960 | ||
|
|
53bba1807b | ||
|
|
139d7acd4f | ||
|
|
ff9be88970 | ||
|
|
e9b1c63ed7 | ||
|
|
005eba7502 | ||
|
|
46715191e3 | ||
|
|
c0c065e477 | ||
|
|
087224395c | ||
|
|
b15bd48376 | ||
|
|
09d9612a71 | ||
|
|
52c9f2b512 | ||
|
|
e3b3203410 | ||
|
|
e03d41f9a0 | ||
|
|
b705a700c8 |
7
libraries/access-token-encryptor/.jenkinsIncludeFile
Normal file
7
libraries/access-token-encryptor/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/access-token-encryptor/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
8
libraries/fetch-utils/.jenkinsIncludeFile
Normal file
8
libraries/fetch-utils/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,8 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/o-error/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
9
libraries/logger/.jenkinsIncludeFile
Normal file
9
libraries/logger/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,9 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/o-error/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
7
libraries/metrics/.jenkinsIncludeFile
Normal file
7
libraries/metrics/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/metrics/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
7
libraries/mongo-utils/.jenkinsIncludeFile
Normal file
7
libraries/mongo-utils/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/mongo-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
7
libraries/o-error/.jenkinsIncludeFile
Normal file
7
libraries/o-error/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/o-error/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
12
libraries/object-persistor/.jenkinsIncludeFile
Normal file
12
libraries/object-persistor/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,12 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/o-error/**
|
||||
libraries/object-persistor/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
8
libraries/overleaf-editor-core/.jenkinsIncludeFile
Normal file
8
libraries/overleaf-editor-core/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,8 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/o-error/**
|
||||
libraries/overleaf-editor-core/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
@@ -294,6 +294,8 @@ class Change {
|
||||
if (this.v2DocVersions) {
|
||||
snapshot.updateV2DocVersions(this.v2DocVersions)
|
||||
}
|
||||
|
||||
snapshot.setTimestamp(this.timestamp)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,8 @@ class Snapshot {
|
||||
return new Snapshot(
|
||||
FileMap.fromRaw(raw.files),
|
||||
raw.projectVersion,
|
||||
V2DocVersions.fromRaw(raw.v2DocVersions)
|
||||
V2DocVersions.fromRaw(raw.v2DocVersions),
|
||||
raw.timestamp ? new Date(raw.timestamp) : undefined
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,8 +46,15 @@ class Snapshot {
|
||||
const raw = {
|
||||
files: this.fileMap.toRaw(),
|
||||
}
|
||||
if (this.projectVersion) raw.projectVersion = this.projectVersion
|
||||
if (this.v2DocVersions) raw.v2DocVersions = this.v2DocVersions.toRaw()
|
||||
if (this.projectVersion) {
|
||||
raw.projectVersion = this.projectVersion
|
||||
}
|
||||
if (this.v2DocVersions) {
|
||||
raw.v2DocVersions = this.v2DocVersions.toRaw()
|
||||
}
|
||||
if (this.timestamp != null) {
|
||||
raw.timestamp = this.timestamp.toISOString()
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
@@ -54,13 +62,15 @@ class Snapshot {
|
||||
* @param {FileMap} [fileMap]
|
||||
* @param {string} [projectVersion]
|
||||
* @param {V2DocVersions} [v2DocVersions]
|
||||
* @param {Date} [timestamp]
|
||||
*/
|
||||
constructor(fileMap, projectVersion, v2DocVersions) {
|
||||
constructor(fileMap, projectVersion, v2DocVersions, timestamp) {
|
||||
assert.maybe.instance(fileMap, FileMap, 'bad fileMap')
|
||||
|
||||
this.fileMap = fileMap || new FileMap({})
|
||||
this.projectVersion = projectVersion
|
||||
this.v2DocVersions = v2DocVersions
|
||||
this.timestamp = timestamp ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +119,17 @@ class Snapshot {
|
||||
v2DocVersions.applyTo(this)
|
||||
}
|
||||
|
||||
getTimestamp() {
|
||||
return this.timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} timestamp
|
||||
*/
|
||||
setTimestamp(timestamp) {
|
||||
this.timestamp = timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying file map.
|
||||
* @return {FileMap}
|
||||
@@ -268,6 +289,7 @@ class Snapshot {
|
||||
files: rawFiles,
|
||||
projectVersion,
|
||||
v2DocVersions: rawV2DocVersions,
|
||||
timestamp: this.getTimestamp() ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export type RawSnapshot = {
|
||||
files: RawFileMap
|
||||
projectVersion?: string
|
||||
v2DocVersions?: RawV2DocVersions | null
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
export type RawHistory = {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const core = require('..')
|
||||
const Change = core.Change
|
||||
const File = core.File
|
||||
const Operation = core.Operation
|
||||
const {
|
||||
Change,
|
||||
File,
|
||||
Operation,
|
||||
AddFileOperation,
|
||||
Snapshot,
|
||||
Origin,
|
||||
RestoreFileOrigin,
|
||||
} = require('..')
|
||||
|
||||
describe('Change', function () {
|
||||
describe('findBlobHashes', function () {
|
||||
@@ -37,9 +42,9 @@ describe('Change', function () {
|
||||
|
||||
describe('RestoreFileOrigin', function () {
|
||||
it('should convert to and from raw', function () {
|
||||
const origin = new core.RestoreFileOrigin(1, 'path', new Date())
|
||||
const origin = new RestoreFileOrigin(1, 'path', new Date())
|
||||
const raw = origin.toRaw()
|
||||
const newOrigin = core.Origin.fromRaw(raw)
|
||||
const newOrigin = Origin.fromRaw(raw)
|
||||
expect(newOrigin).to.eql(origin)
|
||||
})
|
||||
|
||||
@@ -56,7 +61,19 @@ describe('Change', function () {
|
||||
},
|
||||
})
|
||||
|
||||
expect(change.getOrigin()).to.be.an.instanceof(core.RestoreFileOrigin)
|
||||
expect(change.getOrigin()).to.be.an.instanceof(RestoreFileOrigin)
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyTo', function () {
|
||||
it('sets the timestamp on the snapshot', function () {
|
||||
const snapshot = new Snapshot()
|
||||
snapshot.addFile('main.tex', File.fromString(''))
|
||||
const operation = new AddFileOperation('main.tex', File.fromString(''))
|
||||
const now = new Date()
|
||||
const change = new Change([operation], now)
|
||||
change.applyTo(snapshot)
|
||||
expect(snapshot.getTimestamp().toISOString()).to.equal(now.toISOString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
7
libraries/promise-utils/.jenkinsIncludeFile
Normal file
7
libraries/promise-utils/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/promise-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
7
libraries/ranges-tracker/.jenkinsIncludeFile
Normal file
7
libraries/ranges-tracker/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/ranges-tracker/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
10
libraries/redis-wrapper/.jenkinsIncludeFile
Normal file
10
libraries/redis-wrapper/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,10 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/o-error/**
|
||||
libraries/redis-wrapper/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
7
libraries/settings/.jenkinsIncludeFile
Normal file
7
libraries/settings/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/settings/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
7
libraries/stream-utils/.jenkinsIncludeFile
Normal file
7
libraries/stream-utils/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,7 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
8
libraries/validation-tools/.jenkinsIncludeFile
Normal file
8
libraries/validation-tools/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,8 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/o-error/**
|
||||
libraries/validation-tools/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -21364,17 +21364,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@writefull/core": {
|
||||
"version": "1.27.21",
|
||||
"resolved": "https://registry.npmjs.org/@writefull/core/-/core-1.27.21.tgz",
|
||||
"integrity": "sha512-lZByln15uCkLB5rezWaDuiCRLkdN6GQv4WS9uW5E9KLpluUxNwEya0fGqdxjyOjjNA9+0GatAzmQcgBPZGvCMQ==",
|
||||
"version": "1.27.24",
|
||||
"resolved": "https://registry.npmjs.org/@writefull/core/-/core-1.27.24.tgz",
|
||||
"integrity": "sha512-0f0zc4rb0+44dFBRDuognknrv/z/jfgyU15hX+s334q3H4lOH/H0N6g8HwxcYbCzvzIBQLyrWmsJA8Nr/iAs3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bugsnag/js": "^7.23.0",
|
||||
"@bugsnag/plugin-react": "^7.24.0",
|
||||
"@growthbook/growthbook": "^1.4.1",
|
||||
"@writefull/ui": "^1.27.21",
|
||||
"@writefull/utils": "^1.27.21",
|
||||
"@writefull/ui": "^1.27.24",
|
||||
"@writefull/utils": "^1.27.24",
|
||||
"axios": "^1.8.3",
|
||||
"idb": "^8.0.2",
|
||||
"inversify": "^6.0.2",
|
||||
@@ -21386,14 +21386,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@writefull/ui": {
|
||||
"version": "1.27.21",
|
||||
"resolved": "https://registry.npmjs.org/@writefull/ui/-/ui-1.27.21.tgz",
|
||||
"integrity": "sha512-ELjJIJZX00RHX3QZX0G4RXol4pieT6gVi14cthyDs+2oA8FX7K29BxI8YSRjncd0BJz87TfuXDfLVZO8eJs6OQ==",
|
||||
"version": "1.27.24",
|
||||
"resolved": "https://registry.npmjs.org/@writefull/ui/-/ui-1.27.24.tgz",
|
||||
"integrity": "sha512-Rxq5eSJIVGLkEruLPA6kXXfNdo1X7p1A2CKsFf2IIw6Kw7jfckqMu+jC9y/ItccbbzhY5pt7mQXZtLjL+U72wQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.5",
|
||||
"@writefull/utils": "^1.27.21"
|
||||
"@writefull/utils": "^1.27.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 18",
|
||||
@@ -21401,9 +21401,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@writefull/utils": {
|
||||
"version": "1.27.21",
|
||||
"resolved": "https://registry.npmjs.org/@writefull/utils/-/utils-1.27.21.tgz",
|
||||
"integrity": "sha512-TEQUSrPdWRMNlLB/sJ9Hl68hVSxgi5u8xW0L6b2SOqu1R2eI3IsHXLbh16FRLrKjf50iptbOE8pUK8+CHId2sg==",
|
||||
"version": "1.27.24",
|
||||
"resolved": "https://registry.npmjs.org/@writefull/utils/-/utils-1.27.24.tgz",
|
||||
"integrity": "sha512-b+d4hhT6Z92w3+m2itJJmm3iCNt1v0SHKXZ8jxre+ZmjmIkcno2z/naOs55Q2weo9AgGZAEKhRiir0gSTLLi3A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -52043,17 +52043,23 @@
|
||||
"@overleaf/logger": "*",
|
||||
"@overleaf/metrics": "*",
|
||||
"@overleaf/mongo-utils": "*",
|
||||
"@overleaf/o-error": "*",
|
||||
"@overleaf/promise-utils": "*",
|
||||
"@overleaf/settings": "*",
|
||||
"@overleaf/validation-tools": "*",
|
||||
"async": "^3.2.5",
|
||||
"body-parser": "^1.20.3",
|
||||
"bunyan": "^1.8.15",
|
||||
"express": "^4.21.2",
|
||||
"method-override": "^3.0.0",
|
||||
"mongodb-legacy": "6.1.3"
|
||||
"mongodb-legacy": "6.1.3",
|
||||
"zod": "^4.1.7",
|
||||
"zod-validation-error": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"mocha": "^11.1.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
@@ -52565,6 +52571,7 @@
|
||||
"nodemailer": "^6.7.0",
|
||||
"on-headers": "^1.0.2",
|
||||
"otplib": "^12.0.1",
|
||||
"overleaf-editor-core": "*",
|
||||
"p-limit": "^2.3.0",
|
||||
"p-props": "4.0.0",
|
||||
"p-queue": "^8.1.0",
|
||||
@@ -52622,6 +52629,7 @@
|
||||
"@lezer/markdown": "^1.4.3",
|
||||
"@overleaf/codemirror-tree-view": "^0.1.3",
|
||||
"@overleaf/dictionaries": "https://github.com/overleaf/dictionaries/archive/refs/tags/v0.0.3.tar.gz",
|
||||
"@overleaf/eslint-plugin": "*",
|
||||
"@overleaf/ranges-tracker": "*",
|
||||
"@overleaf/stream-utils": "*",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.16",
|
||||
@@ -52677,9 +52685,9 @@
|
||||
"@uppy/utils": "^5.7.0",
|
||||
"@uppy/xhr-upload": "^3.6.0",
|
||||
"@vitest/eslint-plugin": "1.1.44",
|
||||
"@writefull/core": "^1.27.6",
|
||||
"@writefull/ui": "^1.27.6",
|
||||
"@writefull/utils": "^1.27.6",
|
||||
"@writefull/core": "^1.27.24",
|
||||
"@writefull/ui": "^1.27.24",
|
||||
"@writefull/utils": "^1.27.24",
|
||||
"5to6-codemod": "^1.8.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"acorn": "^7.1.1",
|
||||
@@ -52752,7 +52760,6 @@
|
||||
"mock-fs": "^5.1.2",
|
||||
"nock": "^13.5.6",
|
||||
"nvd3": "^1.8.6",
|
||||
"overleaf-editor-core": "*",
|
||||
"p-reflect": "^3.1.0",
|
||||
"pdfjs-dist": "5.1.91",
|
||||
"pirates": "^4.0.1",
|
||||
|
||||
@@ -47,8 +47,8 @@ describe('LearnWiki', function () {
|
||||
|
||||
it('should render wiki page', () => {
|
||||
login(REGULAR_USER)
|
||||
|
||||
cy.visit(UPLOADING_A_PROJECT_URL)
|
||||
// Wiki content
|
||||
cy.findByRole('heading', { name: 'Uploading a project' })
|
||||
cy.contains(/how to create an Overleaf project/)
|
||||
cy.findByRole('img', { name: 'Creating a new project on Overleaf' })
|
||||
@@ -56,20 +56,28 @@ describe('LearnWiki', function () {
|
||||
.and((el: any) => {
|
||||
expect(el[0].naturalWidth, 'renders image').to.be.greaterThan(0)
|
||||
})
|
||||
// Wiki navigation
|
||||
cy.findByRole('link', { name: 'Copying a project' }).should('exist')
|
||||
|
||||
cy.visit(COPYING_A_PROJECT_URL)
|
||||
cy.findByRole('heading', { name: 'Copying a project' })
|
||||
cy.findByRole('link', {
|
||||
name: '1 How to copy a project (option 1)',
|
||||
}).should('exist')
|
||||
cy.findByRole('link', {
|
||||
name: '2 How to copy a project (option 2)',
|
||||
}).should('exist')
|
||||
})
|
||||
|
||||
it('should navigate back and forth', function () {
|
||||
it('should navigate within wiki page using table of contents', function () {
|
||||
login(REGULAR_USER)
|
||||
cy.visit(COPYING_A_PROJECT_URL)
|
||||
cy.findByRole('heading', { name: 'Copying a project' })
|
||||
cy.findByRole('link', { name: 'Uploading a project' }).click()
|
||||
cy.url().should('contain', UPLOADING_A_PROJECT_URL)
|
||||
cy.findByRole('heading', { name: 'Uploading a project' })
|
||||
cy.findByRole('link', { name: 'Copying a project' }).click()
|
||||
cy.findByRole('link', {
|
||||
name: '2 How to copy a project (option 2)',
|
||||
}).click()
|
||||
cy.url().should('contain', COPYING_A_PROJECT_URL)
|
||||
cy.findByRole('heading', { name: 'Copying a project' })
|
||||
cy.findByRole('heading', {
|
||||
name: 'How to copy a project (option 2)',
|
||||
}).should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
13
services/chat/.jenkinsIncludeFile
Normal file
13
services/chat/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,13 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/settings/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/chat/**
|
||||
@@ -12,8 +12,8 @@
|
||||
"test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
|
||||
"test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
14
services/clsi/.jenkinsIncludeFile
Normal file
14
services/clsi/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,14 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/o-error/**
|
||||
libraries/promise-utils/**
|
||||
libraries/settings/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/clsi/**
|
||||
@@ -7,5 +7,4 @@ clsi
|
||||
--esmock-loader=False
|
||||
--node-version=22.18.0
|
||||
--public-repo=True
|
||||
--script-version=4.7.0
|
||||
--use-large-ci-runner=True
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"nodemon": "node --watch app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/settings/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/contacts/**
|
||||
.eslint*
|
||||
.prettier*
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"nodemon": "node --watch app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
16
services/docstore/.jenkinsIncludeFile
Normal file
16
services/docstore/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,16 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/object-persistor/**
|
||||
libraries/promise-utils/**
|
||||
libraries/settings/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/docstore/**
|
||||
@@ -11,8 +11,8 @@
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"nodemon": "node --watch app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
17
services/document-updater/.jenkinsIncludeFile
Normal file
17
services/document-updater/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,17 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/overleaf-editor-core/**
|
||||
libraries/promise-utils/**
|
||||
libraries/ranges-tracker/**
|
||||
libraries/redis-wrapper/**
|
||||
libraries/settings/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/document-updater/**
|
||||
@@ -12,8 +12,8 @@
|
||||
"nodemon": "node --watch app.js",
|
||||
"benchmark:apply": "node benchmarks/apply",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
14
services/filestore/.jenkinsIncludeFile
Normal file
14
services/filestore/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,14 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/o-error/**
|
||||
libraries/object-persistor/**
|
||||
libraries/settings/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/filestore/**
|
||||
@@ -11,8 +11,8 @@
|
||||
"start": "node app.js",
|
||||
"nodemon": "node --watch app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
|
||||
"test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
|
||||
"lint:fix": "eslint --fix .",
|
||||
|
||||
18
services/history-v1/.jenkinsIncludeFile
Normal file
18
services/history-v1/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/object-persistor/**
|
||||
libraries/overleaf-editor-core/**
|
||||
libraries/promise-utils/**
|
||||
libraries/redis-wrapper/**
|
||||
libraries/settings/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/history-v1/**
|
||||
@@ -28,6 +28,7 @@ const withTmpDir = require('./with_tmp_dir')
|
||||
const StreamSizeLimit = require('./stream_size_limit')
|
||||
const { getProjectBlobsBatch } = require('../../storage/lib/blob_store')
|
||||
const assert = require('../../storage/lib/assert')
|
||||
const { getChunkMetadataForVersion } = require('../../storage/lib/chunk_store')
|
||||
|
||||
const pipeline = promisify(Stream.pipeline)
|
||||
|
||||
@@ -396,7 +397,28 @@ async function getSnapshotAtVersion(projectId, version) {
|
||||
chunk.getChanges(),
|
||||
chunk.getEndVersion() - version
|
||||
)
|
||||
snapshot.applyAll(changes)
|
||||
|
||||
if (changes.length > 0) {
|
||||
snapshot.applyAll(changes)
|
||||
} else {
|
||||
// There are no changes in this chunk; we need to look at the previous chunk
|
||||
// to get the snapshot's timestamp
|
||||
let chunkMetadata
|
||||
try {
|
||||
chunkMetadata = await getChunkMetadataForVersion(projectId, version)
|
||||
} catch (err) {
|
||||
if (err instanceof Chunk.VersionNotFoundError) {
|
||||
// The snapshot is the first snapshot of the first chunk, so we can't
|
||||
// find a timestamp. This shouldn't happen often. Ignore the error and
|
||||
// leave the timestamp empty.
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.setTimestamp(chunkMetadata.endTimestamp)
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
"start": "node app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
|
||||
"test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
|
||||
|
||||
@@ -108,6 +108,7 @@ describe('persistChanges', function () {
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
timestamp: thirdChange.getTimestamp().toISOString(),
|
||||
})
|
||||
const history = new History(snapshot, [thirdChange])
|
||||
const currentChunk = new Chunk(history, 2)
|
||||
|
||||
15
services/notifications/.jenkinsIncludeFile
Normal file
15
services/notifications/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,15 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/promise-utils/**
|
||||
libraries/settings/**
|
||||
libraries/validation-tools/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/notifications/**
|
||||
@@ -6,8 +6,10 @@ import logger from '@overleaf/logger'
|
||||
import express from 'express'
|
||||
import methodOverride from 'method-override'
|
||||
import { mongoClient } from './app/js/mongodb.js'
|
||||
import NotificationsController from './app/js/NotificationsController.js'
|
||||
import HealthCheckController from './app/js/HealthCheckController.js'
|
||||
import NotificationsController from './app/js/NotificationsController.ts'
|
||||
import HealthCheckController from './app/js/HealthCheckController.ts'
|
||||
import { isZodErrorLike } from 'zod-validation-error'
|
||||
import { ParamsError } from '@overleaf/validation-tools'
|
||||
|
||||
const app = express()
|
||||
|
||||
@@ -42,6 +44,22 @@ app.get('/health_check', HealthCheckController.check)
|
||||
|
||||
app.get('*', (req, res) => res.sendStatus(404))
|
||||
|
||||
app.use(handleApiError)
|
||||
|
||||
function handleApiError(err, req, res, next) {
|
||||
req.logger.addFields({ err })
|
||||
if (err instanceof ParamsError) {
|
||||
req.logger.setLevel('warn')
|
||||
res.sendStatus(404)
|
||||
} else if (isZodErrorLike(err)) {
|
||||
req.logger.setLevel('warn')
|
||||
res.sendStatus(400)
|
||||
} else {
|
||||
req.logger.setLevel('error')
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
const host = Settings.internal.notifications?.host || '127.0.0.1'
|
||||
const port = Settings.internal.notifications?.port || 3042
|
||||
try {
|
||||
@@ -51,6 +69,9 @@ try {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
app.listen(port, host, () =>
|
||||
logger.debug({}, `notifications starting up, listening on ${host}:${port}`)
|
||||
)
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
app.listen(port, host, () =>
|
||||
logger.debug({}, `notifications starting up, listening on ${host}:${port}`)
|
||||
)
|
||||
}
|
||||
export default app
|
||||
|
||||
@@ -7,14 +7,19 @@ import {
|
||||
RequestFailedError,
|
||||
} from '@overleaf/fetch-utils'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import { z, zz } from '@overleaf/validation-tools'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
const { port } = settings.internal.notifications
|
||||
|
||||
function makeUrl(userId, endPath = '') {
|
||||
return new URL(`/user/${userId}/${endPath}`, `http://127.0.0.1:${port}`)
|
||||
function makeUrl(userId: string, endPath?: string) {
|
||||
return new URL(
|
||||
`/user/${userId}${endPath ? `/${endPath}` : ''}`,
|
||||
`http://127.0.0.1:${port}`
|
||||
)
|
||||
}
|
||||
|
||||
async function makeNotification(notificationKey, userId) {
|
||||
async function makeNotification(notificationKey: string, userId: string) {
|
||||
const postOpts = {
|
||||
method: 'POST',
|
||||
json: {
|
||||
@@ -29,12 +34,23 @@ async function makeNotification(notificationKey, userId) {
|
||||
await fetchNothing(url, postOpts)
|
||||
}
|
||||
|
||||
async function getUsersNotifications(userId) {
|
||||
const getUserNotificationsResponseSchema = z
|
||||
.object({
|
||||
_id: zz.objectId(),
|
||||
key: z.string(),
|
||||
messageOpts: z.string().optional(),
|
||||
templateKey: z.string().optional(),
|
||||
user_id: zz.objectId(),
|
||||
})
|
||||
.array()
|
||||
|
||||
async function getUsersNotifications(userId: string) {
|
||||
const url = makeUrl(userId)
|
||||
try {
|
||||
return await fetchJson(url, {
|
||||
const body = await fetchJson(url, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
return getUserNotificationsResponseSchema.parse(body)
|
||||
} catch (err) {
|
||||
if (err instanceof RequestFailedError) {
|
||||
logger.err({ err }, 'Non-2xx status code received')
|
||||
@@ -45,7 +61,7 @@ async function getUsersNotifications(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function userHasNotification(userId, notificationKey) {
|
||||
async function userHasNotification(userId: string, notificationKey: string) {
|
||||
const body = await getUsersNotifications(userId)
|
||||
const hasNotification = body.some(
|
||||
notification =>
|
||||
@@ -62,11 +78,15 @@ async function userHasNotification(userId, notificationKey) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupNotifications(userId) {
|
||||
async function cleanupNotifications(userId: string) {
|
||||
await db.notifications.deleteOne({ user_id: userId })
|
||||
}
|
||||
|
||||
async function deleteNotification(userId, notificationId, notificationKey) {
|
||||
async function deleteNotification(
|
||||
userId: string,
|
||||
notificationId: string,
|
||||
notificationKey: string
|
||||
) {
|
||||
const deleteByIdUrl = makeUrl(userId, `notification/${notificationId}`)
|
||||
try {
|
||||
await fetchNothing(deleteByIdUrl, {
|
||||
@@ -87,7 +107,7 @@ async function deleteNotification(userId, notificationId, notificationKey) {
|
||||
await fetchNothing(deleteByKeyUrl, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
json: {
|
||||
key: notificationKey,
|
||||
},
|
||||
})
|
||||
@@ -100,7 +120,7 @@ async function deleteNotification(userId, notificationId, notificationKey) {
|
||||
}
|
||||
}
|
||||
|
||||
async function check(req, res) {
|
||||
async function check(req: Request, res: Response) {
|
||||
const userId = new ObjectId().toString()
|
||||
let notificationKey = `smoke-test-notification-${new ObjectId()}`
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import logger from '@overleaf/logger'
|
||||
import metrics from '@overleaf/metrics'
|
||||
import Notifications from './Notifications.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
async function getUserNotifications(req, res, next) {
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id },
|
||||
'getting user unread notifications'
|
||||
)
|
||||
metrics.inc('getUserNotifications')
|
||||
const notifications = await Notifications.getUserNotifications(
|
||||
req.params.user_id
|
||||
)
|
||||
res.json(notifications)
|
||||
}
|
||||
|
||||
async function addNotification(req, res) {
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id, notification: req.body },
|
||||
'adding notification'
|
||||
)
|
||||
metrics.inc('addNotification')
|
||||
try {
|
||||
await Notifications.addNotification(req.params.user_id, req.body)
|
||||
res.sendStatus(200)
|
||||
} catch (err) {
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeNotificationId(req, res) {
|
||||
logger.debug(
|
||||
{
|
||||
userId: req.params.user_id,
|
||||
notificationId: req.params.notification_id,
|
||||
},
|
||||
'mark id notification as read'
|
||||
)
|
||||
metrics.inc('removeNotificationId')
|
||||
await Notifications.removeNotificationId(
|
||||
req.params.user_id,
|
||||
req.params.notification_id
|
||||
)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async function removeNotificationKey(req, res) {
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id, notificationKey: req.body.key },
|
||||
'mark key notification as read'
|
||||
)
|
||||
metrics.inc('removeNotificationKey')
|
||||
await Notifications.removeNotificationKey(req.params.user_id, req.body.key)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async function removeNotificationByKeyOnly(req, res) {
|
||||
const notificationKey = req.params.key
|
||||
logger.debug({ notificationKey }, 'mark notification as read by key only')
|
||||
metrics.inc('removeNotificationKey')
|
||||
await Notifications.removeNotificationByKeyOnly(notificationKey)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async function countNotificationsByKeyOnly(req, res) {
|
||||
const notificationKey = req.params.key
|
||||
try {
|
||||
const count =
|
||||
await Notifications.countNotificationsByKeyOnly(notificationKey)
|
||||
res.json({ count })
|
||||
} catch (err) {
|
||||
logger.err({ err, notificationKey }, 'cannot count by key')
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUnreadNotificationsByKeyOnlyBulk(req, res) {
|
||||
const notificationKey = req.params.key
|
||||
try {
|
||||
const count =
|
||||
await Notifications.deleteUnreadNotificationsByKeyOnlyBulk(
|
||||
notificationKey
|
||||
)
|
||||
res.json({ count })
|
||||
} catch (err) {
|
||||
logger.err({ err, notificationKey }, 'cannot bulk remove by key')
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getUserNotifications: expressify(getUserNotifications),
|
||||
addNotification: expressify(addNotification),
|
||||
deleteUnreadNotificationsByKeyOnlyBulk: expressify(
|
||||
deleteUnreadNotificationsByKeyOnlyBulk
|
||||
),
|
||||
removeNotificationByKeyOnly: expressify(removeNotificationByKeyOnly),
|
||||
removeNotificationId: expressify(removeNotificationId),
|
||||
removeNotificationKey: expressify(removeNotificationKey),
|
||||
countNotificationsByKeyOnly: expressify(countNotificationsByKeyOnly),
|
||||
}
|
||||
160
services/notifications/app/js/NotificationsController.ts
Normal file
160
services/notifications/app/js/NotificationsController.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import logger from '@overleaf/logger'
|
||||
import metrics from '@overleaf/metrics'
|
||||
import Notifications from './Notifications.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import { validateReq, z, zz } from '@overleaf/validation-tools'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
const getUserNotificationsSchema = z.object({
|
||||
params: z.object({
|
||||
user_id: zz.objectId(),
|
||||
}),
|
||||
})
|
||||
|
||||
async function getUserNotifications(req: Request, res: Response) {
|
||||
const { params } = validateReq(req, getUserNotificationsSchema)
|
||||
logger.debug({ userId: params.user_id }, 'getting user unread notifications')
|
||||
metrics.inc('getUserNotifications')
|
||||
const notifications = await Notifications.getUserNotifications(params.user_id)
|
||||
res.json(notifications)
|
||||
}
|
||||
|
||||
const addNotificationSchema = z.object({
|
||||
params: z.object({
|
||||
user_id: zz.objectId(),
|
||||
}),
|
||||
body: z.looseObject({}),
|
||||
})
|
||||
|
||||
async function addNotification(req: Request, res: Response) {
|
||||
const { params, body } = validateReq(req, addNotificationSchema)
|
||||
logger.debug(
|
||||
{ userId: params.user_id, notification: body },
|
||||
'adding notification'
|
||||
)
|
||||
metrics.inc('addNotification')
|
||||
try {
|
||||
await Notifications.addNotification(params.user_id, body)
|
||||
res.sendStatus(200)
|
||||
} catch (err) {
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
const removeNotificationIdSchema = z.object({
|
||||
params: z.object({
|
||||
user_id: zz.objectId(),
|
||||
notification_id: zz.objectId(),
|
||||
}),
|
||||
})
|
||||
|
||||
async function removeNotificationId(req: Request, res: Response) {
|
||||
const { params } = validateReq(req, removeNotificationIdSchema)
|
||||
logger.debug(
|
||||
{
|
||||
userId: req.params.user_id,
|
||||
notificationId: req.params.notification_id,
|
||||
},
|
||||
'mark id notification as read'
|
||||
)
|
||||
metrics.inc('removeNotificationId')
|
||||
await Notifications.removeNotificationId(
|
||||
params.user_id,
|
||||
params.notification_id
|
||||
)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
const removeNotificationKeySchema = z.object({
|
||||
params: z.object({
|
||||
user_id: zz.objectId(),
|
||||
}),
|
||||
body: z.object({
|
||||
key: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
async function removeNotificationKey(req: Request, res: Response) {
|
||||
const { params, body } = validateReq(req, removeNotificationKeySchema)
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id, notificationKey: body.key },
|
||||
'mark key notification as read'
|
||||
)
|
||||
metrics.inc('removeNotificationKey')
|
||||
await Notifications.removeNotificationKey(params.user_id, body.key)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
const removeNotificationByKeyOnlySchema = z.object({
|
||||
params: z.object({
|
||||
key: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
async function removeNotificationByKeyOnly(req: Request, res: Response) {
|
||||
const { params } = validateReq(req, removeNotificationByKeyOnlySchema)
|
||||
const notificationKey = params.key
|
||||
logger.debug({ notificationKey }, 'mark notification as read by key only')
|
||||
metrics.inc('removeNotificationKey')
|
||||
await Notifications.removeNotificationByKeyOnly(notificationKey)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
const countNotificationsByKeyOnlySchema = z.object({
|
||||
params: z.object({
|
||||
key: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
async function countNotificationsByKeyOnly(req: Request, res: Response) {
|
||||
const { params } = validateReq(req, countNotificationsByKeyOnlySchema)
|
||||
const notificationKey = params.key
|
||||
try {
|
||||
const count =
|
||||
await Notifications.countNotificationsByKeyOnly(notificationKey)
|
||||
res.json({ count })
|
||||
} catch (err) {
|
||||
logger.err({ err, notificationKey }, 'cannot count by key')
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUnreadNotificationsByKeyOnlyBulkSchema = z.object({
|
||||
params: z.object({
|
||||
key: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
async function deleteUnreadNotificationsByKeyOnlyBulk(
|
||||
req: Request,
|
||||
res: Response
|
||||
) {
|
||||
const { params } = validateReq(
|
||||
req,
|
||||
deleteUnreadNotificationsByKeyOnlyBulkSchema
|
||||
)
|
||||
const notificationKey = params.key
|
||||
try {
|
||||
const count =
|
||||
await Notifications.deleteUnreadNotificationsByKeyOnlyBulk(
|
||||
notificationKey
|
||||
)
|
||||
res.json({ count })
|
||||
} catch (err) {
|
||||
logger.err({ err, notificationKey }, 'cannot bulk remove by key')
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getUserNotifications: expressify(getUserNotifications),
|
||||
addNotification: expressify(addNotification),
|
||||
deleteUnreadNotificationsByKeyOnlyBulk: expressify(
|
||||
deleteUnreadNotificationsByKeyOnlyBulk
|
||||
),
|
||||
removeNotificationByKeyOnly: expressify(removeNotificationByKeyOnly),
|
||||
removeNotificationId: expressify(removeNotificationId),
|
||||
removeNotificationKey: expressify(removeNotificationKey),
|
||||
countNotificationsByKeyOnly: expressify(countNotificationsByKeyOnly),
|
||||
}
|
||||
@@ -6,5 +6,6 @@ notifications
|
||||
--esmock-loader=False
|
||||
--node-version=22.18.0
|
||||
--public-repo=True
|
||||
--test-acceptance-vitest=True
|
||||
--test-unit-vitest=True
|
||||
--tsconfig-extra-includes=vitest.config.unit.cjs,vitest.config.acceptance.cjs
|
||||
|
||||
@@ -33,6 +33,7 @@ services:
|
||||
NODE_OPTIONS: "--unhandled-rejections=strict"
|
||||
volumes:
|
||||
- ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
|
||||
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_started
|
||||
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
- ../../node_modules:/overleaf/node_modules
|
||||
- ../../libraries:/overleaf/libraries
|
||||
- ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
|
||||
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
|
||||
working_dir: /overleaf/services/notifications
|
||||
environment:
|
||||
ELASTIC_SEARCH_DSN: es:9200
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
|
||||
"test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
|
||||
"test:acceptance:_run": "vitest --config ./vitest.config.acceptance.cjs",
|
||||
"test:acceptance": "npm run test:acceptance:_run",
|
||||
"test:unit:_run": "vitest --config ./vitest.config.unit.cjs",
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"test:unit": "npm run test:unit:_run",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit",
|
||||
"nodemon": "node --watch app.js"
|
||||
@@ -24,14 +24,18 @@
|
||||
"@overleaf/logger": "*",
|
||||
"@overleaf/metrics": "*",
|
||||
"@overleaf/mongo-utils": "*",
|
||||
"@overleaf/o-error": "*",
|
||||
"@overleaf/settings": "*",
|
||||
"@overleaf/promise-utils": "*",
|
||||
"@overleaf/validation-tools": "*",
|
||||
"async": "^3.2.5",
|
||||
"body-parser": "^1.20.3",
|
||||
"bunyan": "^1.8.15",
|
||||
"express": "^4.21.2",
|
||||
"method-override": "^3.0.0",
|
||||
"mongodb-legacy": "6.1.3"
|
||||
"mongodb-legacy": "6.1.3",
|
||||
"zod": "^4.1.7",
|
||||
"zod-validation-error": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.6",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { beforeAll, describe, it, expect } from 'vitest'
|
||||
import { fetchStringWithResponse } from '@overleaf/fetch-utils'
|
||||
import app from '../../../app.js'
|
||||
import logger from '@overleaf/logger'
|
||||
|
||||
let runAppPromise: Promise<void> | null = null
|
||||
|
||||
async function ensureRunning(hostname: string, port: number) {
|
||||
if (!runAppPromise) {
|
||||
runAppPromise = new Promise(resolve => {
|
||||
app.listen(port, hostname, () => {
|
||||
logger.info({ port, hostname }, 'notifications running in dev mode')
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
await runAppPromise
|
||||
}
|
||||
|
||||
describe('HealthCheck endpoint', () => {
|
||||
beforeAll(async () => {
|
||||
await ensureRunning('127.0.0.1', 3042)
|
||||
})
|
||||
it('should return 200 for GET /health_check', async () => {
|
||||
const { response } = await fetchStringWithResponse(
|
||||
'http://localhost:3042/health_check'
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
@@ -1,163 +0,0 @@
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
const modulePath = '../../../app/js/NotificationsController.js'
|
||||
|
||||
const userId = '51dc93e6fb625a261300003b'
|
||||
const notificationId = 'fb625a26f09d'
|
||||
const notificationKey = 'my-notification-key'
|
||||
|
||||
describe('Notifications Controller', () => {
|
||||
let controller, notifications, stubbedNotification
|
||||
beforeEach(async () => {
|
||||
notifications = {
|
||||
addNotification: vi.fn(),
|
||||
getUserNotifications: vi.fn(),
|
||||
removeNotificationByKeyOnly: vi.fn(),
|
||||
removeNotificationId: vi.fn(),
|
||||
removeNotificationKey: vi.fn(),
|
||||
}
|
||||
|
||||
vi.doMock('../../../app/js/Notifications', () => ({
|
||||
default: notifications,
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: {
|
||||
inc: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
controller = (await import(modulePath)).default
|
||||
|
||||
stubbedNotification = [
|
||||
{
|
||||
key: notificationKey,
|
||||
messageOpts: 'some info',
|
||||
templateKey: 'template-key',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
describe('getUserNotifications', () => {
|
||||
it('should ask the notifications for the users notifications', async () => {
|
||||
notifications.getUserNotifications.mockResolvedValue(stubbedNotification)
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
controller.getUserNotifications(req, {
|
||||
json: result => {
|
||||
expect(result).toBe(stubbedNotification)
|
||||
expect(notifications.getUserNotifications).toHaveBeenCalledWith(
|
||||
userId
|
||||
)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addNotification', () => {
|
||||
it('should tell the notifications to add the notification for the user', async () => {
|
||||
notifications.addNotification.mockResolvedValue()
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
body: stubbedNotification,
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
controller.addNotification(req, {
|
||||
sendStatus: code => {
|
||||
expect(notifications.addNotification).toHaveBeenCalledWith(
|
||||
userId,
|
||||
stubbedNotification
|
||||
)
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNotificationId', () => {
|
||||
it('should tell the notifications to mark the notification Id as read', async () => {
|
||||
notifications.removeNotificationId.mockResolvedValue()
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
notification_id: notificationId,
|
||||
},
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
controller.removeNotificationId(req, {
|
||||
sendStatus: code => {
|
||||
expect(notifications.removeNotificationId).toHaveBeenCalledWith(
|
||||
userId,
|
||||
notificationId
|
||||
)
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNotificationKey', () => {
|
||||
it('should tell the notifications to mark the notification Key as read', async () => {
|
||||
notifications.removeNotificationKey.mockResolvedValue()
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
body: { key: notificationKey },
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
controller.removeNotificationKey(req, {
|
||||
sendStatus: code => {
|
||||
expect(notifications.removeNotificationKey).toHaveBeenCalledWith(
|
||||
userId,
|
||||
notificationKey
|
||||
)
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNotificationByKeyOnly', () => {
|
||||
it('should tell the notifications to mark the notification Key as read', async () => {
|
||||
notifications.removeNotificationByKeyOnly.mockResolvedValue()
|
||||
const req = {
|
||||
params: {
|
||||
key: notificationKey,
|
||||
},
|
||||
}
|
||||
await new Promise(resolve =>
|
||||
controller.removeNotificationByKeyOnly(req, {
|
||||
sendStatus: code => {
|
||||
expect(
|
||||
notifications.removeNotificationByKeyOnly
|
||||
).toHaveBeenCalledWith(notificationKey)
|
||||
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,216 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import NotificationsController from '../../../app/js/NotificationsController.ts'
|
||||
import Notifications from '../../../app/js/Notifications.js'
|
||||
import { ObjectId } from 'mongodb-legacy'
|
||||
const modulePath = '../../../app/js/NotificationsController.js'
|
||||
|
||||
const userId = '51dc93e6fb625a261300003b'
|
||||
const notificationId = '51dc93e6fb625a261300003c'
|
||||
const notificationKey = 'my-notification-key'
|
||||
|
||||
vi.mock('../../../app/js/Notifications', () => ({
|
||||
default: {
|
||||
addNotification: vi.fn(),
|
||||
getUserNotifications: vi.fn(),
|
||||
removeNotificationByKeyOnly: vi.fn(),
|
||||
removeNotificationId: vi.fn(),
|
||||
removeNotificationKey: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
interface InputNotification {
|
||||
user_id: string
|
||||
key: string
|
||||
messageOpts?: object
|
||||
templateKey?: string
|
||||
}
|
||||
|
||||
interface DatabaseNotification {
|
||||
_id: ObjectId
|
||||
user_id: ObjectId
|
||||
key: string
|
||||
messageOpts?: object
|
||||
templateKey?: string
|
||||
}
|
||||
|
||||
function convertInputNotificationToDatabaseNotification(
|
||||
inputNotification: InputNotification
|
||||
): DatabaseNotification {
|
||||
return {
|
||||
...inputNotification,
|
||||
_id: new ObjectId(),
|
||||
user_id: new ObjectId(inputNotification.user_id),
|
||||
}
|
||||
}
|
||||
|
||||
describe('Notifications Controller', () => {
|
||||
let controller: typeof NotificationsController,
|
||||
stubbedNotification: Array<InputNotification>
|
||||
beforeEach(async () => {
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: {
|
||||
inc: vi.fn(),
|
||||
mongodb: {
|
||||
monitor: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
controller = (await import(modulePath)).default
|
||||
|
||||
stubbedNotification = [
|
||||
{
|
||||
user_id: new ObjectId().toString(),
|
||||
key: notificationKey,
|
||||
messageOpts: { info: 'some info' },
|
||||
templateKey: 'template-key',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
describe('getUserNotifications', () => {
|
||||
it('should ask the notifications for the users notifications', async () => {
|
||||
const databaseNotifications = stubbedNotification.map(
|
||||
convertInputNotificationToDatabaseNotification
|
||||
)
|
||||
vi.mocked(Notifications.getUserNotifications).mockResolvedValue(
|
||||
databaseNotifications
|
||||
)
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
controller.getUserNotifications(req, {
|
||||
json: result => {
|
||||
expect(result).toBe(databaseNotifications)
|
||||
expect(Notifications.getUserNotifications).toHaveBeenCalledWith(
|
||||
userId
|
||||
)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addNotification', () => {
|
||||
it('should tell the notifications to add the notification for the user', async () => {
|
||||
vi.mocked(Notifications.addNotification).mockResolvedValue({
|
||||
acknowledged: true,
|
||||
upsertedId: new ObjectId(),
|
||||
matchedCount: 1,
|
||||
modifiedCount: 1,
|
||||
upsertedCount: 0,
|
||||
})
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
body: stubbedNotification[0],
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
controller.addNotification(req, {
|
||||
sendStatus: code => {
|
||||
expect(Notifications.addNotification).toHaveBeenCalledWith(
|
||||
userId,
|
||||
stubbedNotification[0]
|
||||
)
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNotificationId', () => {
|
||||
it('should tell the notifications to mark the notification Id as read', async () => {
|
||||
vi.mocked(Notifications.removeNotificationId).mockResolvedValue({
|
||||
acknowledged: true,
|
||||
upsertedId: null,
|
||||
upsertedCount: 0,
|
||||
matchedCount: 1,
|
||||
modifiedCount: 1,
|
||||
})
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
notification_id: notificationId,
|
||||
},
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
controller.removeNotificationId(req, {
|
||||
sendStatus: code => {
|
||||
expect(Notifications.removeNotificationId).toHaveBeenCalledWith(
|
||||
userId,
|
||||
notificationId
|
||||
)
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNotificationKey', () => {
|
||||
it('should tell the notifications to mark the notification Key as read', async () => {
|
||||
vi.mocked(Notifications.removeNotificationKey).mockResolvedValue({
|
||||
acknowledged: true,
|
||||
upsertedId: null,
|
||||
upsertedCount: 0,
|
||||
matchedCount: 1,
|
||||
modifiedCount: 1,
|
||||
})
|
||||
const req = {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
body: { key: notificationKey },
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
controller.removeNotificationKey(req, {
|
||||
sendStatus: code => {
|
||||
expect(Notifications.removeNotificationKey).toHaveBeenCalledWith(
|
||||
userId,
|
||||
notificationKey
|
||||
)
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeNotificationByKeyOnly', () => {
|
||||
it('should tell the notifications to mark the notification Key as read', async () => {
|
||||
vi.mocked(Notifications.removeNotificationByKeyOnly).mockResolvedValue({
|
||||
acknowledged: true,
|
||||
upsertedId: null,
|
||||
upsertedCount: 0,
|
||||
matchedCount: 1,
|
||||
modifiedCount: 1,
|
||||
})
|
||||
const req = {
|
||||
params: {
|
||||
key: notificationKey,
|
||||
},
|
||||
}
|
||||
await new Promise<void>(resolve =>
|
||||
controller.removeNotificationByKeyOnly(req, {
|
||||
sendStatus: code => {
|
||||
expect(
|
||||
Notifications.removeNotificationByKeyOnly
|
||||
).toHaveBeenCalledWith(notificationKey)
|
||||
|
||||
expect(code).toBe(200)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
8
services/notifications/vitest.config.acceptance.cjs
Normal file
8
services/notifications/vitest.config.acceptance.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
const { defineConfig } = require('vitest/config')
|
||||
|
||||
module.exports = defineConfig({
|
||||
test: {
|
||||
include: ['test/acceptance/js/**/*.test.{js,ts}'],
|
||||
isolate: false,
|
||||
},
|
||||
})
|
||||
@@ -2,7 +2,7 @@ const { defineConfig } = require('vitest/config')
|
||||
|
||||
module.exports = defineConfig({
|
||||
test: {
|
||||
include: ['test/unit/js/**/*.test.js'],
|
||||
include: ['test/unit/js/**/*.test.{js,ts}'],
|
||||
setupFiles: ['./test/setup.js'],
|
||||
isolate: false,
|
||||
},
|
||||
|
||||
17
services/project-history/.jenkinsIncludeFile
Normal file
17
services/project-history/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,17 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/overleaf-editor-core/**
|
||||
libraries/promise-utils/**
|
||||
libraries/redis-wrapper/**
|
||||
libraries/settings/**
|
||||
libraries/stream-utils/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/project-history/**
|
||||
@@ -12,8 +12,8 @@
|
||||
"test:acceptance:_run": "mocha --loader=esmock --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
|
||||
"test:unit:_run": "mocha --loader=esmock --recursive --reporter spec --exit $@ test/unit/js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import nock from 'nock'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
|
||||
import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
|
||||
const { ObjectId } = mongodb
|
||||
@@ -50,6 +51,12 @@ describe('LatestSnapshot', function () {
|
||||
.get(`/api/projects/${this.historyId}/latest/history`)
|
||||
.replyWithFile(200, fixture('chunks/0-3.json'))
|
||||
|
||||
const fixtureData = JSON.parse(
|
||||
readFileSync(fixture('chunks/0-3.json'), 'utf8')
|
||||
)
|
||||
const changes = fixtureData.chunk.history.changes
|
||||
const lastTimestamp = changes[changes.length - 1].timestamp
|
||||
|
||||
ProjectHistoryClient.getLatestSnapshot(this.projectId, (error, body) => {
|
||||
if (error) {
|
||||
throw error
|
||||
@@ -69,6 +76,7 @@ describe('LatestSnapshot', function () {
|
||||
operations: [{ textOperation: [26, '\n\nFour five six'] }],
|
||||
},
|
||||
},
|
||||
timestamp: lastTimestamp,
|
||||
},
|
||||
version: 3,
|
||||
})
|
||||
|
||||
13
services/real-time/.jenkinsIncludeFile
Normal file
13
services/real-time/.jenkinsIncludeFile
Normal file
@@ -0,0 +1,13 @@
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/o-error/**
|
||||
libraries/redis-wrapper/**
|
||||
libraries/settings/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/real-time/**
|
||||
@@ -11,8 +11,8 @@
|
||||
"test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
|
||||
"nodemon": "node --watch app.js",
|
||||
"lint": "eslint --max-warnings 0 --format unix .",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,Jenkinsfile}'",
|
||||
"format": "prettier --list-different $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"format:fix": "prettier --write $PWD/'**/{*.*js,*.ts,Jenkinsfile}'",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
libraries/**
|
||||
patches/**
|
||||
services/web/**
|
||||
# Autogenerated by build scripts. Do not edit.
|
||||
.eslint*
|
||||
.prettier*
|
||||
package.json
|
||||
libraries/access-token-encryptor/**
|
||||
libraries/eslint-plugin/**
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
libraries/metrics/**
|
||||
libraries/mongo-utils/**
|
||||
libraries/o-error/**
|
||||
libraries/object-persistor/**
|
||||
libraries/overleaf-editor-core/**
|
||||
libraries/promise-utils/**
|
||||
libraries/ranges-tracker/**
|
||||
libraries/redis-wrapper/**
|
||||
libraries/settings/**
|
||||
libraries/stream-utils/**
|
||||
libraries/validation-tools/**
|
||||
package-lock.json
|
||||
package.json
|
||||
patches/**
|
||||
services/web/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import UserInfoManager from '../User/UserInfoManager.js'
|
||||
import UserInfoController from '../User/UserInfoController.js'
|
||||
import ChatManager from './ChatManager.js'
|
||||
import ChatManager from './ChatManager.mjs'
|
||||
|
||||
async function sendMessage(req, res) {
|
||||
const { project_id: projectId } = req.params
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const UserInfoController = require('../User/UserInfoController')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const { callbackify } = require('@overleaf/promise-utils')
|
||||
import UserInfoController from '../User/UserInfoController.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { callbackify } from '@overleaf/promise-utils'
|
||||
|
||||
async function injectUserInfoIntoThreads(threads) {
|
||||
const userIds = new Set()
|
||||
@@ -38,7 +38,7 @@ async function injectUserInfoIntoThreads(threads) {
|
||||
return threads
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
injectUserInfoIntoThreads: callbackify(injectUserInfoIntoThreads),
|
||||
promises: {
|
||||
injectUserInfoIntoThreads,
|
||||
@@ -4,7 +4,7 @@ import { Cookie } from 'tough-cookie'
|
||||
import OError from '@overleaf/o-error'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import CompileManager from './CompileManager.js'
|
||||
import CompileManager from './CompileManager.mjs'
|
||||
import ClsiManager from './ClsiManager.js'
|
||||
import logger from '@overleaf/logger'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import Crypto from 'node:crypto'
|
||||
import Settings from '@overleaf/settings'
|
||||
import RedisWrapper from '../../infrastructure/RedisWrapper.js'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import ProjectRootDocManager from '../Project/ProjectRootDocManager.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import ClsiManager from './ClsiManager.js'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
import UserAnalyticsIdCache from '../Analytics/UserAnalyticsIdCache.js'
|
||||
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
|
||||
let CompileManager
|
||||
const Crypto = require('crypto')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const RedisWrapper = require('../../infrastructure/RedisWrapper')
|
||||
const rclient = RedisWrapper.client('clsi_recently_compiled')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const ClsiManager = require('./ClsiManager')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const { RateLimiter } = require('../../infrastructure/RateLimiter')
|
||||
const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache')
|
||||
const {
|
||||
callbackify,
|
||||
callbackifyMultiResult,
|
||||
} = require('@overleaf/promise-utils')
|
||||
|
||||
function instrumentWithTimer(fn, key) {
|
||||
return async (...args) => {
|
||||
@@ -196,7 +193,7 @@ async function deleteAuxFiles(projectId, userId, clsiserverid) {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = CompileManager = {
|
||||
export default CompileManager = {
|
||||
promises: {
|
||||
compile: instrumentedCompile,
|
||||
deleteAuxFiles,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isZodErrorLike, fromZodError } from 'zod-validation-error'
|
||||
import Errors from './Errors.js'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import SamlLogHandler from '../SamlLog/SamlLogHandler.js'
|
||||
import SamlLogHandler from '../SamlLog/SamlLogHandler.mjs'
|
||||
import HttpErrorHandler from './HttpErrorHandler.js'
|
||||
import { plainTextResponse } from '../../infrastructure/Response.js'
|
||||
import { expressifyErrorHandler } from '@overleaf/promise-utils'
|
||||
|
||||
@@ -7,6 +7,7 @@ const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
||||
const { File } = require('../../models/File')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const { promisifyAll } = require('@overleaf/promise-utils')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
|
||||
const FileStoreHandler = {
|
||||
RETRY_ATTEMPTS: 3,
|
||||
@@ -69,24 +70,47 @@ const FileStoreHandler = {
|
||||
)
|
||||
return callback(new Error('can not upload symlink'))
|
||||
}
|
||||
FileHashManager.computeHash(fsPath, function (err, hash) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
FileStoreHandler._uploadToHistory(
|
||||
historyId,
|
||||
hash,
|
||||
stat.size,
|
||||
fsPath,
|
||||
function (err) {
|
||||
const size = stat.size
|
||||
Modules.hooks.fire(
|
||||
'preUploadFile',
|
||||
{ projectId, historyId, fileArgs, fsPath, size },
|
||||
preUploadErr => {
|
||||
if (preUploadErr) {
|
||||
return callback(preUploadErr)
|
||||
}
|
||||
FileHashManager.computeHash(fsPath, function (err, hash) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
fileArgs = { ...fileArgs, hash }
|
||||
callback(err, new File(fileArgs), true)
|
||||
}
|
||||
)
|
||||
})
|
||||
FileStoreHandler._uploadToHistory(
|
||||
historyId,
|
||||
hash,
|
||||
stat.size,
|
||||
fsPath,
|
||||
function (err) {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
const fileRef = new File({ ...fileArgs, hash })
|
||||
Modules.hooks.fire(
|
||||
'postUploadFile',
|
||||
{
|
||||
projectId,
|
||||
fileRef,
|
||||
size,
|
||||
},
|
||||
postUploadErr => {
|
||||
if (postUploadErr) {
|
||||
return callback(postUploadErr)
|
||||
}
|
||||
callback(err, fileRef, true, size)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -94,7 +118,7 @@ const FileStoreHandler = {
|
||||
module.exports = FileStoreHandler
|
||||
module.exports.promises = promisifyAll(FileStoreHandler, {
|
||||
multiResult: {
|
||||
uploadFileFromDisk: ['fileRef', 'createdBlob'],
|
||||
uploadFileFromDiskWithHistoryId: ['fileRef', 'createdBlob'],
|
||||
uploadFileFromDisk: ['fileRef', 'createdBlob', 'size'],
|
||||
uploadFileFromDiskWithHistoryId: ['fileRef', 'createdBlob', 'size'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,14 +7,13 @@ import EditorController from '../Editor/EditorController.js'
|
||||
import Errors from '../Errors/Errors.js'
|
||||
import moment from 'moment'
|
||||
import { callbackifyAll } from '@overleaf/promise-utils'
|
||||
import { fetchJson } from '@overleaf/fetch-utils'
|
||||
import ProjectLocator from '../Project/ProjectLocator.js'
|
||||
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
|
||||
import ChatApiHandler from '../Chat/ChatApiHandler.js'
|
||||
import DocstoreManager from '../Docstore/DocstoreManager.js'
|
||||
import logger from '@overleaf/logger'
|
||||
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
|
||||
import ChatManager from '../Chat/ChatManager.js'
|
||||
import ChatManager from '../Chat/ChatManager.mjs'
|
||||
import OError from '@overleaf/o-error'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import ProjectEntityHandler from '../Project/ProjectEntityHandler.js'
|
||||
@@ -69,6 +68,13 @@ const RestoreManager = {
|
||||
)
|
||||
const snapshot = Snapshot.fromRaw(snapshotRaw)
|
||||
|
||||
const origin = options.origin ?? {
|
||||
kind: 'file-restore',
|
||||
path: pathname,
|
||||
version,
|
||||
timestamp: snapshot.getTimestamp()?.toISOString(),
|
||||
}
|
||||
|
||||
return await RestoreManager._revertSingleFile(
|
||||
userId,
|
||||
projectId,
|
||||
@@ -76,7 +82,7 @@ const RestoreManager = {
|
||||
pathname,
|
||||
threadIds,
|
||||
snapshot,
|
||||
options
|
||||
{ origin }
|
||||
)
|
||||
},
|
||||
|
||||
@@ -131,17 +137,9 @@ const RestoreManager = {
|
||||
})
|
||||
.catch(() => null)
|
||||
|
||||
const updates = await RestoreManager._getUpdatesFromHistory(
|
||||
projectId,
|
||||
version
|
||||
)
|
||||
const updateAtVersion = updates.find(update => update.toV === version)
|
||||
|
||||
const origin = options.origin || {
|
||||
kind: 'file-restore',
|
||||
path: pathname,
|
||||
version,
|
||||
timestamp: new Date(updateAtVersion.meta.end_ts).toISOString(),
|
||||
const snapshotFile = projectSnapshotAtVersion.getFile(pathname)
|
||||
if (!snapshotFile) {
|
||||
throw new OError('file not found in snapshot', { pathname })
|
||||
}
|
||||
|
||||
const importInfo = await FileSystemImportManager.promises.importFile(
|
||||
@@ -162,7 +160,7 @@ const RestoreManager = {
|
||||
projectId,
|
||||
file.element._id,
|
||||
file.type,
|
||||
origin,
|
||||
options.origin,
|
||||
userId
|
||||
)
|
||||
|
||||
@@ -177,25 +175,24 @@ const RestoreManager = {
|
||||
threadIds.delete(file.element._id.toString())
|
||||
}
|
||||
|
||||
const snapshotFile = projectSnapshotAtVersion.getFile(pathname)
|
||||
if (!snapshotFile) {
|
||||
throw new OError('file not found in snapshot', { pathname })
|
||||
}
|
||||
|
||||
// Look for metadata indicating a linked file.
|
||||
const fileMetadata = snapshotFile.getMetadata()
|
||||
const isFileMetadata = fileMetadata && 'provider' in fileMetadata
|
||||
|
||||
logger.debug({ fileMetadata }, 'metadata from history')
|
||||
|
||||
if (importInfo.type === 'file' || isFileMetadata) {
|
||||
if (
|
||||
!snapshotFile.isEditable() ||
|
||||
importInfo.type === 'file' ||
|
||||
isFileMetadata
|
||||
) {
|
||||
const newFile = await EditorController.promises.upsertFile(
|
||||
projectId,
|
||||
parentFolderId,
|
||||
basename,
|
||||
fsPath,
|
||||
fileMetadata,
|
||||
origin,
|
||||
options.origin,
|
||||
userId
|
||||
)
|
||||
|
||||
@@ -296,13 +293,17 @@ const RestoreManager = {
|
||||
newCommentThreadData
|
||||
)
|
||||
|
||||
const lines = snapshotFile
|
||||
.getContent({ filterTrackedDeletes: true })
|
||||
.split('\n')
|
||||
|
||||
const { _id } = await EditorController.promises.addDocWithRanges(
|
||||
projectId,
|
||||
parentFolderId,
|
||||
basename,
|
||||
importInfo.lines,
|
||||
lines,
|
||||
newRanges,
|
||||
origin,
|
||||
options.origin,
|
||||
userId
|
||||
)
|
||||
|
||||
@@ -362,31 +363,21 @@ const RestoreManager = {
|
||||
throw new OError('project does not have ranges support', { projectId })
|
||||
}
|
||||
|
||||
// Get project paths at version
|
||||
const pathsAtPastVersion = await RestoreManager._getProjectPathsAtVersion(
|
||||
projectId,
|
||||
version
|
||||
)
|
||||
|
||||
const updates = await RestoreManager._getUpdatesFromHistory(
|
||||
projectId,
|
||||
version
|
||||
)
|
||||
const updateAtVersion = updates.find(update => update.toV === version)
|
||||
|
||||
const origin = {
|
||||
kind: 'project-restore',
|
||||
version,
|
||||
timestamp: new Date(updateAtVersion.meta.end_ts).toISOString(),
|
||||
}
|
||||
const threadIds = await getCommentThreadIds(projectId)
|
||||
|
||||
const snapshotRaw = await HistoryManager.promises.getContentAtVersion(
|
||||
projectId,
|
||||
version
|
||||
)
|
||||
const snapshot = Snapshot.fromRaw(snapshotRaw)
|
||||
|
||||
const pathsAtPastVersion = snapshot.getFilePathnames()
|
||||
|
||||
const origin = {
|
||||
kind: 'project-restore',
|
||||
version,
|
||||
timestamp: snapshot.getTimestamp()?.toISOString(),
|
||||
}
|
||||
const threadIds = await getCommentThreadIds(projectId)
|
||||
|
||||
const reverted = []
|
||||
for (const pathname of pathsAtPastVersion) {
|
||||
const res = await RestoreManager._revertSingleFile(
|
||||
@@ -437,18 +428,6 @@ const RestoreManager = {
|
||||
}/project/${projectId}/version/${version}/${encodeURIComponent(pathname)}`
|
||||
return await FileWriter.promises.writeUrlToDisk(projectId, url)
|
||||
},
|
||||
|
||||
async _getUpdatesFromHistory(projectId, version) {
|
||||
const url = `${Settings.apis.project_history.url}/project/${projectId}/updates?before=${version}&min_count=1`
|
||||
const res = await fetchJson(url)
|
||||
return res.updates
|
||||
},
|
||||
|
||||
async _getProjectPathsAtVersion(projectId, version) {
|
||||
const url = `${Settings.apis.project_history.url}/project/${projectId}/paths/version/${version}`
|
||||
const res = await fetchJson(url)
|
||||
return res.paths
|
||||
},
|
||||
}
|
||||
|
||||
export default { ...callbackifyAll(RestoreManager), promises: RestoreManager }
|
||||
|
||||
@@ -163,20 +163,18 @@ function getUserAffiliations(userId, callback) {
|
||||
if (body?.length > 0) {
|
||||
const concurrencyLimit = 10
|
||||
await promiseMapWithLimit(concurrencyLimit, body, async affiliation => {
|
||||
if (!affiliation.institution.commonsAccount) {
|
||||
const group = (
|
||||
await Modules.promises.hooks.fire(
|
||||
'getGroupWithDomainCaptureByV1Id',
|
||||
affiliation.institution.id
|
||||
)
|
||||
)?.[0]
|
||||
const group = (
|
||||
await Modules.promises.hooks.fire(
|
||||
'getGroupWithDomainCaptureByV1Id',
|
||||
affiliation.institution.id
|
||||
)
|
||||
)?.[0]
|
||||
|
||||
if (group) {
|
||||
affiliation.group = {
|
||||
_id: group._id,
|
||||
managedUsersEnabled: Boolean(group.managedUsersEnabled),
|
||||
domainCaptureEnabled: Boolean(group.domainCaptureEnabled),
|
||||
}
|
||||
if (group) {
|
||||
affiliation.group = {
|
||||
_id: group._id,
|
||||
managedUsersEnabled: Boolean(group.managedUsersEnabled),
|
||||
domainCaptureEnabled: Boolean(group.domainCaptureEnabled),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,7 @@ import Settings from '@overleaf/settings'
|
||||
import _ from 'lodash'
|
||||
import AnalyticsManager from '../../../../app/src/Features/Analytics/AnalyticsManager.js'
|
||||
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
|
||||
import {
|
||||
CompileFailedError,
|
||||
UrlFetchFailedError,
|
||||
InvalidUrlError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
BadDataError,
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError,
|
||||
SourceFileNotFoundError,
|
||||
NotOriginalImporterError,
|
||||
FeatureNotAvailableError,
|
||||
RemoteServiceError,
|
||||
FileCannotRefreshError,
|
||||
} from './LinkedFilesErrors.js'
|
||||
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
|
||||
import {
|
||||
OutputFileFetchFailedError,
|
||||
FileTooLargeError,
|
||||
@@ -45,6 +31,22 @@ import ProjectOutputFileAgent from './ProjectOutputFileAgent.mjs'
|
||||
import ProjectFileAgent from './ProjectFileAgent.mjs'
|
||||
import UrlAgent from './UrlAgent.mjs'
|
||||
|
||||
const {
|
||||
CompileFailedError,
|
||||
UrlFetchFailedError,
|
||||
InvalidUrlError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
BadDataError,
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError,
|
||||
SourceFileNotFoundError,
|
||||
NotOriginalImporterError,
|
||||
FeatureNotAvailableError,
|
||||
RemoteServiceError,
|
||||
FileCannotRefreshError,
|
||||
} = LinkedFilesErrors
|
||||
|
||||
let LinkedFilesController
|
||||
|
||||
const createLinkedFileSchema = z.object({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { BackwardCompatibleError } = require('../Errors/Errors')
|
||||
import { BackwardCompatibleError } from '../Errors/Errors.js'
|
||||
|
||||
class UrlFetchFailedError extends BackwardCompatibleError {}
|
||||
|
||||
@@ -26,7 +26,7 @@ class RemoteServiceError extends BackwardCompatibleError {}
|
||||
|
||||
class FileCannotRefreshError extends BackwardCompatibleError {}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
CompileFailedError,
|
||||
UrlFetchFailedError,
|
||||
InvalidUrlError,
|
||||
@@ -3,13 +3,12 @@ import EditorController from '../Editor/EditorController.js'
|
||||
import ProjectLocator from '../Project/ProjectLocator.js'
|
||||
import { Project } from '../../models/Project.js'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import {
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError,
|
||||
BadDataError,
|
||||
} from './LinkedFilesErrors.js'
|
||||
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
|
||||
import { callbackifyAll } from '@overleaf/promise-utils'
|
||||
|
||||
const { ProjectNotFoundError, V1ProjectNotFoundError, BadDataError } =
|
||||
LinkedFilesErrors
|
||||
|
||||
const LinkedFilesHandler = {
|
||||
async getFileById(projectId, fileId) {
|
||||
const { element, path, folder } = await ProjectLocator.promises.findElement(
|
||||
|
||||
@@ -16,16 +16,16 @@ import DocstoreManager from '../Docstore/DocstoreManager.js'
|
||||
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
|
||||
import _ from 'lodash'
|
||||
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
|
||||
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
|
||||
import { promisify } from '@overleaf/promise-utils'
|
||||
import HistoryManager from '../History/HistoryManager.js'
|
||||
|
||||
import {
|
||||
const {
|
||||
BadDataError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
SourceFileNotFoundError,
|
||||
} from './LinkedFilesErrors.js'
|
||||
|
||||
import { promisify } from '@overleaf/promise-utils'
|
||||
import HistoryManager from '../History/HistoryManager.js'
|
||||
} = LinkedFilesErrors
|
||||
|
||||
let ProjectFileAgent
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
|
||||
import CompileManager from '../Compile/CompileManager.js'
|
||||
import CompileManager from '../Compile/CompileManager.mjs'
|
||||
import ClsiManager from '../Compile/ClsiManager.js'
|
||||
import ProjectFileAgent from './ProjectFileAgent.mjs'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
CompileFailedError,
|
||||
BadDataError,
|
||||
AccessDeniedError,
|
||||
} from './LinkedFilesErrors.js'
|
||||
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
|
||||
import { OutputFileFetchFailedError } from '../Errors/Errors.js'
|
||||
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
|
||||
import { promisify } from '@overleaf/promise-utils'
|
||||
|
||||
const { CompileFailedError, BadDataError, AccessDeniedError } =
|
||||
LinkedFilesErrors
|
||||
|
||||
function _prepare(projectId, linkedFileData, userId, callback) {
|
||||
_checkAuth(projectId, linkedFileData, userId, (err, allowed) => {
|
||||
if (err) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import logger from '@overleaf/logger'
|
||||
import urlValidator from 'valid-url'
|
||||
import { InvalidUrlError, UrlFetchFailedError } from './LinkedFilesErrors.js'
|
||||
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
|
||||
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
|
||||
import UrlHelper from '../Helpers/UrlHelper.js'
|
||||
import { fetchStream, RequestFailedError } from '@overleaf/fetch-utils'
|
||||
import { callbackify } from '@overleaf/promise-utils'
|
||||
import { FileTooLargeError } from '../Errors/Errors.js'
|
||||
|
||||
const { InvalidUrlError, UrlFetchFailedError } = LinkedFilesErrors
|
||||
|
||||
async function createLinkedFile(
|
||||
projectId,
|
||||
linkedFileData,
|
||||
|
||||
@@ -35,20 +35,20 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.js'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
|
||||
import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js'
|
||||
import FeaturesUpdater from '../Subscription/FeaturesUpdater.js'
|
||||
import SpellingHandler from '../Spelling/SpellingHandler.js'
|
||||
import SpellingHandler from '../Spelling/SpellingHandler.mjs'
|
||||
import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js'
|
||||
import InstitutionsFeatures from '../Institutions/InstitutionsFeatures.js'
|
||||
import InstitutionsGetter from '../Institutions/InstitutionsGetter.js'
|
||||
import ProjectAuditLogHandler from './ProjectAuditLogHandler.mjs'
|
||||
import PublicAccessLevels from '../Authorization/PublicAccessLevels.js'
|
||||
import TagsHandler from '../Tags/TagsHandler.js'
|
||||
import TutorialHandler from '../Tutorial/TutorialHandler.js'
|
||||
import TutorialHandler from '../Tutorial/TutorialHandler.mjs'
|
||||
import UserUpdater from '../User/UserUpdater.js'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import { z, zz, validateReq } from '../../infrastructure/Validation.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { isStandaloneAiAddOnPlanCode } from '../Subscription/AiHelper.js'
|
||||
import SubscriptionController from '../Subscription/SubscriptionController.js'
|
||||
import SubscriptionController from '../Subscription/SubscriptionController.mjs'
|
||||
import { formatCurrency } from '../../util/currency.js'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
@@ -167,6 +167,7 @@ function getAllowedImagesForUser(user) {
|
||||
return {
|
||||
...image,
|
||||
allowed: _imageAllowed(user, image),
|
||||
rolling: image.monthlyExperimental,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import NotificationsBuilder from '../Notifications/NotificationsBuilder.js'
|
||||
import GeoIpLookup from '../../infrastructure/GeoIpLookup.js'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
|
||||
import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js'
|
||||
import TutorialHandler from '../Tutorial/TutorialHandler.js'
|
||||
import TutorialHandler from '../Tutorial/TutorialHandler.mjs'
|
||||
import SubscriptionHelper from '../Subscription/SubscriptionHelper.js'
|
||||
import PermissionsManager from '../Authorization/PermissionsManager.js'
|
||||
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
|
||||
@@ -319,7 +319,9 @@ async function projectListPage(req, res, next) {
|
||||
institutionName:
|
||||
samlSession.linked.universityName ||
|
||||
samlSession.linked.providerName,
|
||||
templateKey: 'notification_institution_sso_linked',
|
||||
templateKey: samlSession.domainCaptureEnabled
|
||||
? 'notification_group_sso_linked'
|
||||
: 'notification_institution_sso_linked',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const { SamlLog } = require('../../models/SamlLog')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const logger = require('@overleaf/logger')
|
||||
const { err: errSerializer } = require('@overleaf/logger/serializers')
|
||||
const { callbackify } = require('util')
|
||||
const Settings = require('@overleaf/settings')
|
||||
import { SamlLog } from '../../models/SamlLog.js'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import logger from '@overleaf/logger'
|
||||
import loggerSerializers from '@overleaf/logger/serializers.js'
|
||||
import { callbackify } from 'node:util'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
const ALLOWED_PATHS = Settings.saml?.logAllowList || ['/saml/']
|
||||
|
||||
@@ -33,7 +33,7 @@ async function log(req, data, samlAssertion) {
|
||||
data.samlSession = saml
|
||||
|
||||
if (data.error instanceof Error) {
|
||||
const errSerialized = errSerializer(data.error)
|
||||
const errSerialized = loggerSerializers.err(data.error)
|
||||
if (data.error.tryAgain) {
|
||||
errSerialized.tryAgain = data.error.tryAgain
|
||||
}
|
||||
@@ -82,4 +82,4 @@ const SamlLogHandler = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = SamlLogHandler
|
||||
export default SamlLogHandler
|
||||
@@ -5,7 +5,7 @@ import Settings from '@overleaf/settings'
|
||||
import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.js'
|
||||
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js'
|
||||
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
|
||||
import SystemMessageManager from '../SystemMessages/SystemMessageManager.js'
|
||||
import SystemMessageManager from '../SystemMessages/SystemMessageManager.mjs'
|
||||
|
||||
const AdminController = {
|
||||
_sendDisconnectAllUsersMessage: delay => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const OError = require('@overleaf/o-error')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const { promisifyAll } = require('@overleaf/promise-utils')
|
||||
const LearnedWordsManager = require('./LearnedWordsManager')
|
||||
import OError from '@overleaf/o-error'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import { promisifyAll } from '@overleaf/promise-utils'
|
||||
import LearnedWordsManager from './LearnedWordsManager.js'
|
||||
|
||||
module.exports = {
|
||||
const SpellingHandler = {
|
||||
getUserDictionary(userId, callback) {
|
||||
const timer = new Metrics.Timer('spelling_get_dict')
|
||||
LearnedWordsManager.getLearnedWords(userId, (error, words) => {
|
||||
@@ -26,4 +26,4 @@ module.exports = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.promises = promisifyAll(module.exports)
|
||||
export default { ...SpellingHandler, promises: promisifyAll(SpellingHandler) }
|
||||
@@ -1,48 +1,48 @@
|
||||
// @ts-check
|
||||
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const SubscriptionHandler = require('./SubscriptionHandler')
|
||||
const SubscriptionHelper = require('./SubscriptionHelper')
|
||||
const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
|
||||
const LimitationsManager = require('./LimitationsManager')
|
||||
const RecurlyWrapper = require('./RecurlyWrapper')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
|
||||
const FeaturesUpdater = require('./FeaturesUpdater')
|
||||
const GroupPlansData = require('./GroupPlansData')
|
||||
const V1SubscriptionManager = require('./V1SubscriptionManager')
|
||||
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
||||
const RecurlyEventHandler = require('./RecurlyEventHandler')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const OError = require('@overleaf/o-error')
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import SubscriptionHandler from './SubscriptionHandler.js'
|
||||
import SubscriptionHelper from './SubscriptionHelper.js'
|
||||
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.js'
|
||||
import LimitationsManager from './LimitationsManager.js'
|
||||
import RecurlyWrapper from './RecurlyWrapper.js'
|
||||
import Settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
import GeoIpLookup from '../../infrastructure/GeoIpLookup.js'
|
||||
import FeaturesUpdater from './FeaturesUpdater.js'
|
||||
import GroupPlansData from './GroupPlansData.js'
|
||||
import V1SubscriptionManager from './V1SubscriptionManager.js'
|
||||
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
|
||||
import RecurlyEventHandler from './RecurlyEventHandler.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import OError from '@overleaf/o-error'
|
||||
import Errors from './Errors.js'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
|
||||
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import async from 'async'
|
||||
import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
|
||||
import RecurlyClient from './RecurlyClient.js'
|
||||
import {
|
||||
AI_ADD_ON_CODE,
|
||||
subscriptionChangeIsAiAssistUpgrade,
|
||||
} from './AiHelper.js'
|
||||
import PlansLocator from './PlansLocator.js'
|
||||
import { User } from '../../models/User.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import PermissionsManager from '../Authorization/PermissionsManager.js'
|
||||
import { sanitizeSessionUserForFrontEnd } from '../../infrastructure/FrontEndUser.js'
|
||||
import { z, validateReq } from '../../infrastructure/Validation.js'
|
||||
import { IndeterminateInvoiceError } from '../Errors/Errors.js'
|
||||
import SubscriptionLocator from './SubscriptionLocator.js'
|
||||
|
||||
const {
|
||||
DuplicateAddOnError,
|
||||
AddOnNotPresentError,
|
||||
PaymentActionRequiredError,
|
||||
PaymentFailedError,
|
||||
MissingBillingInfoError,
|
||||
} = require('./Errors')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
const AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const async = require('async')
|
||||
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
|
||||
const RecurlyClient = require('./RecurlyClient')
|
||||
const {
|
||||
AI_ADD_ON_CODE,
|
||||
subscriptionChangeIsAiAssistUpgrade,
|
||||
} = require('./AiHelper')
|
||||
const PlansLocator = require('./PlansLocator')
|
||||
const { User } = require('../../models/User')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const PermissionsManager = require('../Authorization/PermissionsManager')
|
||||
const {
|
||||
sanitizeSessionUserForFrontEnd,
|
||||
} = require('../../infrastructure/FrontEndUser')
|
||||
const { z, validateReq } = require('../../infrastructure/Validation')
|
||||
const { IndeterminateInvoiceError } = require('../Errors/Errors')
|
||||
const SubscriptionLocator = require('./SubscriptionLocator')
|
||||
} = Errors
|
||||
|
||||
const SUBSCRIPTION_PAUSED_REDIRECT_PATH =
|
||||
'/user/subscription?redirect-reason=subscription-paused'
|
||||
@@ -1101,7 +1101,7 @@ function makeChangePreview(
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
userSubscriptionPage: expressify(userSubscriptionPage),
|
||||
successfulSubscription: expressify(successfulSubscription),
|
||||
cancelSubscription,
|
||||
@@ -3,7 +3,7 @@ import _ from 'lodash'
|
||||
import OError from '@overleaf/o-error'
|
||||
import SubscriptionUpdater from './SubscriptionUpdater.js'
|
||||
import SubscriptionLocator from './SubscriptionLocator.js'
|
||||
import SubscriptionController from './SubscriptionController.js'
|
||||
import SubscriptionController from './SubscriptionController.mjs'
|
||||
import SubscriptionHelper from './SubscriptionHelper.js'
|
||||
import { Subscription } from '../../models/Subscription.js'
|
||||
import { User } from '../../models/User.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import AuthenticationController from '../Authentication/AuthenticationController.js'
|
||||
import PermissionsController from '../Authorization/PermissionsController.mjs'
|
||||
import SubscriptionController from './SubscriptionController.js'
|
||||
import SubscriptionController from './SubscriptionController.mjs'
|
||||
import SubscriptionGroupController from './SubscriptionGroupController.mjs'
|
||||
import TeamInvitesController from './TeamInvitesController.mjs'
|
||||
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Settings from '@overleaf/settings'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import SystemMessageManager from './SystemMessageManager.js'
|
||||
import SystemMessageManager from './SystemMessageManager.mjs'
|
||||
|
||||
const ProjectController = {
|
||||
getMessages(req, res, next) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const { SystemMessage } = require('../../models/SystemMessage')
|
||||
const {
|
||||
addRequiredCleanupHandlerBeforeDrainingConnections,
|
||||
} = require('../../infrastructure/GracefulShutdown')
|
||||
const { callbackifyAll } = require('@overleaf/promise-utils')
|
||||
const logger = require('@overleaf/logger')
|
||||
import { SystemMessage } from '../../models/SystemMessage.js'
|
||||
import { addRequiredCleanupHandlerBeforeDrainingConnections } from '../../infrastructure/GracefulShutdown.js'
|
||||
import { callbackifyAll } from '@overleaf/promise-utils'
|
||||
import logger from '@overleaf/logger'
|
||||
|
||||
const SystemMessageManager = {
|
||||
_cachedMessages: [],
|
||||
@@ -52,7 +50,7 @@ addRequiredCleanupHandlerBeforeDrainingConnections(
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
getMessages: SystemMessageManager.getMessages.bind(SystemMessageManager),
|
||||
...callbackifyAll(SystemMessageManager, { without: ['getMessages'] }),
|
||||
promises: SystemMessageManager,
|
||||
@@ -1,16 +1,16 @@
|
||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const TemplatesController = require('./TemplatesController')
|
||||
const TemplatesMiddleware = require('./TemplatesMiddleware')
|
||||
const { RateLimiter } = require('../../infrastructure/RateLimiter')
|
||||
const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware')
|
||||
const AnalyticsRegistrationSourceMiddleware = require('../Analytics/AnalyticsRegistrationSourceMiddleware')
|
||||
import AuthenticationController from '../Authentication/AuthenticationController.js'
|
||||
import TemplatesController from './TemplatesController.js'
|
||||
import TemplatesMiddleware from './TemplatesMiddleware.js'
|
||||
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
|
||||
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.js'
|
||||
|
||||
const rateLimiter = new RateLimiter('create-project-from-template', {
|
||||
points: 20,
|
||||
duration: 60,
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
rateLimiter,
|
||||
apply(app) {
|
||||
app.get(
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import TpdsUpdateHandler from './TpdsUpdateHandler.mjs'
|
||||
import UpdateMerger from './UpdateMerger.js'
|
||||
import UpdateMerger from './UpdateMerger.mjs'
|
||||
import Errors from '../Errors/Errors.js'
|
||||
import logger from '@overleaf/logger'
|
||||
import Path from 'node:path'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { callbackify } from 'node:util'
|
||||
import UpdateMerger from './UpdateMerger.js'
|
||||
import UpdateMerger from './UpdateMerger.mjs'
|
||||
import logger from '@overleaf/logger'
|
||||
import NotificationsBuilder from '../Notifications/NotificationsBuilder.js'
|
||||
import ProjectCreationHandler from '../Project/ProjectCreationHandler.js'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
const { callbackify } = require('util')
|
||||
const _ = require('lodash')
|
||||
const fsPromises = require('fs/promises')
|
||||
const fs = require('fs')
|
||||
const logger = require('@overleaf/logger')
|
||||
const EditorController = require('../Editor/EditorController')
|
||||
const FileTypeManager = require('../Uploads/FileTypeManager')
|
||||
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||
const crypto = require('crypto')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { pipeline } = require('stream/promises')
|
||||
import { callbackify } from 'node:util'
|
||||
import _ from 'lodash'
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import fs from 'node:fs'
|
||||
import logger from '@overleaf/logger'
|
||||
import EditorController from '../Editor/EditorController.js'
|
||||
import FileTypeManager from '../Uploads/FileTypeManager.js'
|
||||
import ProjectEntityHandler from '../Project/ProjectEntityHandler.js'
|
||||
import crypto from 'node:crypto'
|
||||
import Settings from '@overleaf/settings'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
|
||||
async function mergeUpdate(userId, projectId, path, updateRequest, source) {
|
||||
const fsPath = await writeUpdateToDisk(projectId, updateRequest)
|
||||
@@ -185,7 +185,7 @@ async function createFolder(projectId, path, userId) {
|
||||
return folder
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
mergeUpdate: callbackify(mergeUpdate),
|
||||
_mergeUpdate: callbackify(_mergeUpdate),
|
||||
deleteUpdate: callbackify(deleteUpdate),
|
||||
@@ -1,5 +1,5 @@
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import TutorialHandler from './TutorialHandler.js'
|
||||
import TutorialHandler from './TutorialHandler.mjs'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
const VALID_KEYS = [
|
||||
@@ -21,6 +21,7 @@ const VALID_KEYS = [
|
||||
'ide-redesign-new-survey-promo',
|
||||
'ide-redesign-beta-intro',
|
||||
'ide-redesign-labs-user-beta-promo',
|
||||
'rolling-compile-image-changed',
|
||||
]
|
||||
|
||||
async function completeTutorial(req, res, next) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const UserUpdater = require('../User/UserUpdater')
|
||||
import UserUpdater from '../User/UserUpdater.js'
|
||||
|
||||
const POSTPONE_DURATION_MS = 24 * 60 * 60 * 1000 // 1 day
|
||||
|
||||
@@ -59,4 +59,4 @@ function getInactiveTutorials(user, tutorialKey) {
|
||||
return inactiveTutorials
|
||||
}
|
||||
|
||||
module.exports = { setTutorialState, getInactiveTutorials }
|
||||
export default { setTutorialState, getInactiveTutorials }
|
||||
@@ -62,6 +62,7 @@ const db = {
|
||||
projectHistoryFailures: internalDb.collection('projectHistoryFailures'),
|
||||
projectHistoryGlobalBlobs: internalDb.collection('projectHistoryGlobalBlobs'),
|
||||
projectHistoryLabels: internalDb.collection('projectHistoryLabels'),
|
||||
projectHistorySizes: internalDb.collection('projectHistorySizes'),
|
||||
projectHistorySyncState: internalDb.collection('projectHistorySyncState'),
|
||||
projectInvites: internalDb.collection('projectInvites'),
|
||||
projects: internalDb.collection('projects'),
|
||||
|
||||
@@ -24,7 +24,7 @@ import UserEmailsController from './Features/User/UserEmailsController.js'
|
||||
import UserPagesController from './Features/User/UserPagesController.mjs'
|
||||
import TutorialController from './Features/Tutorial/TutorialController.mjs'
|
||||
import DocumentController from './Features/Documents/DocumentController.mjs'
|
||||
import CompileManager from './Features/Compile/CompileManager.js'
|
||||
import CompileManager from './Features/Compile/CompileManager.mjs'
|
||||
import CompileController from './Features/Compile/CompileController.mjs'
|
||||
import HealthCheckController from './Features/HealthCheck/HealthCheckController.mjs'
|
||||
import ProjectDownloadsController from './Features/Downloads/ProjectDownloadsController.mjs'
|
||||
@@ -52,7 +52,7 @@ import MetaController from './Features/Metadata/MetaController.mjs'
|
||||
import TokenAccessController from './Features/TokenAccess/TokenAccessController.mjs'
|
||||
import TokenAccessRouter from './Features/TokenAccess/TokenAccessRouter.mjs'
|
||||
import LinkedFilesRouter from './Features/LinkedFiles/LinkedFilesRouter.mjs'
|
||||
import TemplatesRouter from './Features/Templates/TemplatesRouter.js'
|
||||
import TemplatesRouter from './Features/Templates/TemplatesRouter.mjs'
|
||||
import UserMembershipRouter from './Features/UserMembership/UserMembershipRouter.mjs'
|
||||
import SystemMessageController from './Features/SystemMessages/SystemMessageController.mjs'
|
||||
import AnalyticsRegistrationSourceMiddleware from './Features/Analytics/AnalyticsRegistrationSourceMiddleware.js'
|
||||
|
||||
10
services/web/buildscript.txt
Normal file
10
services/web/buildscript.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
web
|
||||
--only-jenkins-include-file=True
|
||||
--dependencies=
|
||||
--docker-repos=us-east1-docker.pkg.dev/overleaf-ops/ol-docker
|
||||
--env-add=
|
||||
--env-pass-through=
|
||||
--esmock-loader=False
|
||||
--node-version=22.18.0
|
||||
--public-repo=False
|
||||
--script-version=4.7.0
|
||||
@@ -1006,6 +1006,7 @@ module.exports = {
|
||||
v1ImportDataScreen: [],
|
||||
snapshotUtils: [],
|
||||
usGovBanner: [],
|
||||
rollingBuildsUpdatedAlert: [],
|
||||
offlineModeToolbarButtons: [],
|
||||
settingsEntries: [],
|
||||
autoCompleteExtensions: [],
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"a_new_reference_was_added_from_provider": "",
|
||||
"a_new_reference_was_added_to_file": "",
|
||||
"a_new_reference_was_added_to_file_from_provider": "",
|
||||
"a_new_version_of_the_rolling_texlive_build_released": "",
|
||||
"about_to_archive_projects": "",
|
||||
"about_to_delete_cert": "",
|
||||
"about_to_delete_projects": "",
|
||||
@@ -50,6 +51,7 @@
|
||||
"access_edit_your_projects": "",
|
||||
"access_levels_changed": "",
|
||||
"account_billed_manually": "",
|
||||
"account_has_been_link_to_group_account": "",
|
||||
"account_has_been_link_to_institution_account": "",
|
||||
"account_has_past_due_invoice_change_plan_warning": "",
|
||||
"account_help": "",
|
||||
@@ -657,6 +659,7 @@
|
||||
"get_most_subscription_by_checking_overleaf": "",
|
||||
"get_most_subscription_by_checking_overleaf_ai_writefull": "",
|
||||
"get_real_time_track_changes": "",
|
||||
"get_regular_access_to_new_versions_of_tex_live": "",
|
||||
"git": "",
|
||||
"git_authentication_token": "",
|
||||
"git_authentication_token_create_modal_info_1": "",
|
||||
@@ -1615,6 +1618,7 @@
|
||||
"showing_x_results_of_total": "",
|
||||
"sign_up": "",
|
||||
"simple_search_mode": "",
|
||||
"since_this_project_is_set_to_the_rolling_build": "",
|
||||
"single_sign_on_sso": "",
|
||||
"size": "",
|
||||
"something_not_right": "",
|
||||
@@ -1779,11 +1783,11 @@
|
||||
"test": "",
|
||||
"test_configuration": "",
|
||||
"test_configuration_successful": "",
|
||||
"test_more_recent_versions_of_texlive": "",
|
||||
"tex_live_version": "",
|
||||
"texgpt": "",
|
||||
"thank_you": "",
|
||||
"thank_you_exclamation": "",
|
||||
"thank_you_for_joining_the_rolling_texlive": "",
|
||||
"thank_you_for_your_feedback": "",
|
||||
"thanks_for_confirming_your_email_address": "",
|
||||
"thanks_for_getting_in_touch": "",
|
||||
|
||||
@@ -2,9 +2,16 @@ import { useTranslation } from 'react-i18next'
|
||||
import { LostConnectionAlert } from './lost-connection-alert'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { debugging } from '@/utils/debugging'
|
||||
import { ElementType } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
|
||||
const rollingBuildsUpdatedAlert: Array<{
|
||||
import: { default: ElementType }
|
||||
path: string
|
||||
}> = importOverleafModules('rollingBuildsUpdatedAlert')
|
||||
|
||||
export function Alerts() {
|
||||
const { t } = useTranslation()
|
||||
@@ -23,6 +30,12 @@ export function Alerts() {
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
{rollingBuildsUpdatedAlert.map(
|
||||
({ import: { default: Component }, path }) => (
|
||||
<Component key={path} />
|
||||
)
|
||||
)}
|
||||
|
||||
{connectionState.forceDisconnected &&
|
||||
// hide "disconnected" banner when displaying out of sync modal
|
||||
connectionState.error !== 'out-of-sync' ? (
|
||||
|
||||
@@ -5,8 +5,8 @@ import { isSplitTestEnabled, getSplitTestVariant } from '@/utils/splitTestUtils'
|
||||
export const ignoringUserCutoffDate =
|
||||
new URLSearchParams(window.location.search).get('skip-new-user-check') ===
|
||||
'true'
|
||||
// TODO: change this when we have a launch date
|
||||
const NEW_USER_CUTOFF_DATE = new Date('2100-01-01')
|
||||
|
||||
const NEW_USER_CUTOFF_DATE = new Date(Date.UTC(2025, 8, 23, 13, 0, 0)) // 2pm British Summer Time on September 23, 2025
|
||||
|
||||
export const isNewUser = () => {
|
||||
if (ignoringUserCutoffDate) return true
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import LabsExperimentWidget from '../../shared/components/labs/labs-experiments-widget'
|
||||
import { isInExperiment } from '@/utils/labs-utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
export const TUTORIAL_KEY = 'rolling-compile-image-changed'
|
||||
|
||||
const MonthlyTexliveLabsWidget = ({
|
||||
labsProgram,
|
||||
@@ -15,6 +19,18 @@ const MonthlyTexliveLabsWidget = ({
|
||||
const { t } = useTranslation()
|
||||
const [optedIn, setOptedIn] = useState(isInExperiment('monthly-texlive'))
|
||||
|
||||
const optInWithCompletedTutorial = useCallback(
|
||||
async (shouldOptIn: boolean) => {
|
||||
try {
|
||||
await postJSON(`/tutorial/${TUTORIAL_KEY}/complete`)
|
||||
} catch (err) {
|
||||
debugConsole.error(err)
|
||||
}
|
||||
setOptedIn(shouldOptIn)
|
||||
},
|
||||
[setOptedIn]
|
||||
)
|
||||
|
||||
const monthlyTexLiveSplitTestEnabled = isSplitTestEnabled('monthly-texlive')
|
||||
if (!monthlyTexLiveSplitTestEnabled) {
|
||||
return null
|
||||
@@ -27,15 +43,31 @@ const MonthlyTexliveLabsWidget = ({
|
||||
className="rounded bg-primary-subtle"
|
||||
/>
|
||||
)
|
||||
|
||||
const optedInDescription = (
|
||||
<Trans
|
||||
i18nKey="thank_you_for_joining_the_rolling_texlive"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/learn/latex/Overleaf_and_TeX_Live#How_do_I_change_a_project’s_TeX_Live_version?"
|
||||
target="_blank"
|
||||
key="getting-started-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<LabsExperimentWidget
|
||||
description={t('test_more_recent_versions_of_texlive')}
|
||||
description={t('get_regular_access_to_new_versions_of_tex_live')}
|
||||
optedInDescription={optedInDescription}
|
||||
experimentName="monthly-texlive"
|
||||
logo={logo}
|
||||
labsEnabled={labsProgram}
|
||||
setErrorMessage={setErrorMessage}
|
||||
optedIn={optedIn}
|
||||
setOptedIn={setOptedIn}
|
||||
setOptedIn={optInWithCompletedTutorial}
|
||||
title={t('rolling_texlive_build')}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTutorial } from '@/shared/hooks/promotions/use-tutorial'
|
||||
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { useProjectSettingsContext } from '../editor-left-menu/context/project-settings-context'
|
||||
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const TUTORIAL_KEY = 'rolling-compile-image-changed'
|
||||
const rollingImages = getMeta('ol-imageNames')
|
||||
.filter(img => img.rolling)
|
||||
.map(img => img.imageName)
|
||||
|
||||
const RollingCompileImageChangedAlert = () => {
|
||||
const { completeTutorial } = useTutorial(TUTORIAL_KEY)
|
||||
|
||||
const { inactiveTutorials } = useEditorContext()
|
||||
const { imageName } = useProjectSettingsContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
completeTutorial({ event: 'promo-click', action: 'complete' })
|
||||
}, [completeTutorial])
|
||||
|
||||
const onRollingBuild = imageName && rollingImages.includes(imageName)
|
||||
if (inactiveTutorials.includes(TUTORIAL_KEY) || !onRollingBuild) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<OLNotification
|
||||
className="mt-5"
|
||||
isDismissible
|
||||
onDismiss={onClose}
|
||||
content={t('since_this_project_is_set_to_the_rolling_build')}
|
||||
type="info"
|
||||
title={t('a_new_version_of_the_rolling_texlive_build_released')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RollingCompileImageChangedAlert
|
||||
@@ -88,6 +88,21 @@ function Institution() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{templateKey === 'notification_group_sso_linked' && (
|
||||
<Notification
|
||||
type="info"
|
||||
onDismiss={() => id && handleDismiss(id)}
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="account_has_been_link_to_group_account"
|
||||
components={{ b: <b /> }}
|
||||
values={{ appName, email, institutionName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{templateKey === 'notification_institution_sso_non_canonical' && (
|
||||
<Notification
|
||||
type="warning"
|
||||
|
||||
@@ -13,10 +13,12 @@ const en = {
|
||||
cancel: 'Cancel',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
'do-not-know': 'Don’t know',
|
||||
equation: 'equation',
|
||||
table: 'table',
|
||||
or: 'or',
|
||||
close: 'Close',
|
||||
submit: 'Submit',
|
||||
'add-unlimited-ai': 'Add unlimited AI',
|
||||
'accept-and-continue': 'Accept and continue',
|
||||
'copy-code': 'Copy code',
|
||||
@@ -249,6 +251,21 @@ const en = {
|
||||
'create-modal.disclaimer':
|
||||
'AI can make mistakes. Review the code before applying it.',
|
||||
'create-modal.your-result': 'Your __name__ suggestion:',
|
||||
'fake-door-test.text-to-speech.try': 'Try Text to Speech',
|
||||
'fake-door-test.text-to-speech.title':
|
||||
'Sorry, text to speech isn’t available yet',
|
||||
'fake-door-test.text-to-speech.introduction_p1':
|
||||
'We are considering adding text to speech to language suggestions in Overleaf and are gathering feedback.',
|
||||
'fake-door-test.text-to-speech.introduction_p2':
|
||||
'Is text to speech something that would help you with your writing in Overleaf?',
|
||||
'fake-door-test.text-to-speech.more-information-if-yes':
|
||||
'How would text to speech help you with your writing?',
|
||||
'fake-door-test.text-to-speech.more-information-if-no':
|
||||
'What would help you with your writing in Overleaf?',
|
||||
'fake-door-test.text-to-speech.closing.title':
|
||||
'Thank you for your feedback!',
|
||||
'fake-door-test.text-to-speech.closing.body':
|
||||
'We’ll use this to assess whether we add text to speech functionality to language suggestions in Overleaf.',
|
||||
'language-model.using-this-feature':
|
||||
'Using this feature means your text is sent to OpenAI’s servers and may be kept there for up to 30 days. It is not used to train OpenAI’s models. Writefull does not store or train on your texts.',
|
||||
'language-model.learn-more': 'Learn more',
|
||||
@@ -386,10 +403,12 @@ const es = {
|
||||
cancel: 'Cancelar',
|
||||
yes: 'Sí',
|
||||
no: 'No',
|
||||
'do-not-know': 'No lo sé',
|
||||
equation: 'equación',
|
||||
table: 'tabla',
|
||||
or: 'o',
|
||||
close: 'Cerrar',
|
||||
submit: 'Enviar',
|
||||
'add-unlimited-ai': 'Añadir IA ilimitada',
|
||||
'accept-and-continue': 'Aceptar y continuar',
|
||||
'copy-code': 'Copiar código',
|
||||
@@ -640,6 +659,21 @@ const es = {
|
||||
'create-modal.disclaimer':
|
||||
'AI can make mistakes. Review the code before apply it.',
|
||||
'create-modal.your-result': 'Your __name__ suggestion:',
|
||||
'fake-door-test.text-to-speech.try': 'Probar texto a voz',
|
||||
'fake-door-test.text-to-speech.title':
|
||||
'Lo sentimos, el texto a voz aún no está disponible',
|
||||
'fake-door-test.text-to-speech.introduction_p1':
|
||||
'Estamos considerando agregar texto a voz a las sugerencias de idioma en Overleaf y estamos recopilando comentarios.',
|
||||
'fake-door-test.text-to-speech.introduction_p2':
|
||||
'¿Es el texto a voz algo que te ayudaría con tu escritura en Overleaf?',
|
||||
'fake-door-test.text-to-speech.more-information-if-yes':
|
||||
'¿Cómo te ayudaría el texto a voz con tu escritura?',
|
||||
'fake-door-test.text-to-speech.more-information-if-no':
|
||||
'¿Qué te ayudaría con tu escritura en Overleaf?',
|
||||
'fake-door-test.text-to-speech.closing.title':
|
||||
'¡Gracias por tus comentarios!',
|
||||
'fake-door-test.text-to-speech.closing.body':
|
||||
'Los utilizaremos para evaluar si agregamos la funcionalidad de texto a voz a las sugerencias de idioma en Overleaf.',
|
||||
'language-model.using-this-feature':
|
||||
'Usar esta función significa que su texto se envía a los servidores de OpenAI y puede mantenerse allí hasta por 30 días. No se utiliza para entrenar los modelos de OpenAI. Writefull no almacena ni entrena con sus textos.',
|
||||
'language-model.learn-more': 'Aprende más',
|
||||
|
||||
@@ -9,7 +9,8 @@ import getMeta from '@/utils/meta'
|
||||
type IntegrationLinkingWidgetProps = {
|
||||
logo: ReactNode
|
||||
title: string
|
||||
description: string
|
||||
description: string | ReactNode
|
||||
optedInDescription?: string | ReactNode
|
||||
helpPath?: string
|
||||
labsEnabled?: boolean
|
||||
experimentName: string
|
||||
@@ -22,6 +23,7 @@ export function LabsExperimentWidget({
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
optedInDescription,
|
||||
helpPath,
|
||||
labsEnabled,
|
||||
experimentName,
|
||||
@@ -69,7 +71,7 @@ export function LabsExperimentWidget({
|
||||
{optedIn && <OLBadge bg="info">{t('enabled')}</OLBadge>}
|
||||
</div>
|
||||
<p className="small">
|
||||
{description}{' '}
|
||||
{optedIn && optedInDescription ? optedInDescription : description}{' '}
|
||||
{helpPath && (
|
||||
<a href={helpPath} target="_blank" rel="noreferrer">
|
||||
{t('learn_more')}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user