Compare commits

...
5 Commits
Author SHA1 Message Date
Wyatt Johnson 1b73a5fcd7 [CORL-976] Story Closed Fix (#2903)
* fix: fixed issue where `isStoryClosed` can return null

* chore: version bump
2020-03-23 18:02:45 +00:00
Wyatt Johnson 1503b735e5 fix: set SameSite: None for cookies (#2890) 2020-03-19 19:16:50 +00:00
Wyatt Johnson 6db72db837 [CORL-963] Story sorting fix (#2887)
* fix: fixed sorting issues with queries

* review: naming touchups
2020-03-19 18:59:34 +00:00
Wyatt Johnson 18b274a6a9 [CORL-972] Version Linter (#2898)
* fix: added version linting

* chore: bump 6.0.2
2020-03-19 18:44:56 +00:00
Wyatt Johnson 78cfb4cac8 fix: added more checks to migration (#2894) 2020-03-18 20:53:16 +00:00
9 changed files with 437 additions and 95 deletions
+6
View File
@@ -69,6 +69,9 @@ jobs:
command: |
npm run doctoc
git diff --exit-code
- run:
name: Lint Versions
command: npx @coralproject/package-version-lint
# unit_tests will run the unit tests.
unit_tests:
@@ -141,6 +144,9 @@ jobs:
<<: *job_defaults
steps:
- checkout
- run:
name: Verify release version
command: npx @coralproject/package-version-lint --expect ${CIRCLE_TAG/#v}
- setup_remote_docker
- deploy:
name: Deploy the code
+255 -42
View File
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "5.5.1",
"version": "6.0.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -8680,20 +8680,95 @@
"dev": true
},
"body-parser": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
"integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.0.0",
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.1",
"http-errors": "~1.6.2",
"iconv-lite": "0.4.19",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.5.1",
"raw-body": "2.3.2",
"type-is": "~1.6.15"
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"requires": {
"mime-db": "1.43.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
}
}
},
"bonjour": {
@@ -10373,7 +10448,8 @@
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
"dev": true
},
"content-security-policy-builder": {
"version": "2.0.0",
@@ -15674,46 +15750,182 @@
"integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
},
"express": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
"integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.5",
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.18.2",
"content-disposition": "0.5.2",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.3",
"qs": "6.5.1",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.1",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"requires": {
"mime-db": "1.43.0"
}
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
}
}
},
@@ -16084,6 +16296,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"dev": true,
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
@@ -20265,9 +20478,9 @@
"integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA=="
},
"ipaddr.js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
"integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"is-absolute": {
"version": "1.0.0",
@@ -26560,7 +26773,7 @@
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
}
@@ -29819,12 +30032,12 @@
}
},
"proxy-addr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
"integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.6.0"
"ipaddr.js": "1.9.1"
}
},
"proxy-agent": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "6.0.0",
"version": "6.0.3",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
@@ -83,7 +83,7 @@
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
"env-rewrite": "^1.0.2",
"express": "^4.16.3",
"express": "^4.17.1",
"express-enforces-ssl": "^1.1.0",
"express-static-gzip": "^0.3.2",
"fs-extra": "^6.0.1",
@@ -155,6 +155,11 @@ const generateCookieOptions = (
path: "/api",
httpOnly: true,
secure: req.secure,
// Chrome will ignore `SameSite: None` when not used in a secure context
// anyways, so don't bother setting `None` when we're not secure. The only
// time we aren't behind HTTPS is when we're testing/in development where the
// the setting for `SameSite: Lax` would be OK.
sameSite: req.secure ? "None" : "Lax",
expires: expiresIn,
});
+79 -4
View File
@@ -1,8 +1,10 @@
import DataLoader from "dataloader";
import { defaultTo } from "lodash";
import { DateTime } from "luxon";
import GraphContext from "coral-server/graph/context";
import { Connection } from "coral-server/models/helpers";
import { CloseCommenting } from "coral-server/models/settings";
import {
retrieveActiveStories,
retrieveManyStories,
@@ -26,16 +28,89 @@ import {
import { createManyBatchLoadFn } from "./util";
const statusFilter = (
status?: GQLSTORY_STATUS
closeCommenting: CloseCommenting,
status?: GQLSTORY_STATUS,
now = new Date()
): StoryConnectionInput["filter"] => {
switch (status) {
case GQLSTORY_STATUS.OPEN:
if (closeCommenting.auto) {
// Automatic story closing has been enabled. Stories will be considered
// open if they have a ${closedAt} date in the future, if they've been
// forced open (where ${closedAt} is set to false), or they haven't been
// closed and their ${createdAt} date is after ${now - close.timeout}.
// Calculate the cutoff time for createdAt.
const consideredClosedAt = DateTime.fromJSDate(now)
.plus({
seconds: -closeCommenting.timeout,
})
.toJSDate();
return {
$or: [
// The story will be open if the close date is in the future...
{
closedAt: { $gt: now },
},
// Or the story has been forced open by setting closedAt to false...
{
closedAt: false,
},
// Or the closed at date isn't set and the createdAt date is after
// the cutoff date.
{
closedAt: null,
createdAt: { $gt: consideredClosedAt },
},
],
};
}
// Automatic story closing is not enabled. Stories will be considered open
// if they do not have a ${closedAt}, it has been forced open (where
// ${closedAt} is set to false), or the ${closedAt} date is in the future.
return {
closedAt: { $in: [null, false] },
$or: [
// A story is open if the closedAt date is in the future...
{ closedAt: { $gt: now } },
// Or the closedAt date is not set.
{ closedAt: { $in: [null, false] } },
],
};
case GQLSTORY_STATUS.CLOSED:
if (closeCommenting.auto) {
// Automatic story closing has been enabled. Stories will be considered
// closed if they have a ${closedAt} date before the current date or
// they do not have a ${closedAt} date set and the ${createdAt} date is
// before ${now - close.timeout} (implying that it's close window is
// up).
// Calculate the cutoff time for createdAt.
const consideredClosedAt = DateTime.fromJSDate(now)
.plus({
seconds: -closeCommenting.timeout,
})
.toJSDate();
return {
$or: [
// The story will be closed if the closedAt date is in the past...
{ closedAt: { $lte: now } },
// Or the closedAt date isn't set and the createdAt date is before
// the cutoff date.
{
closedAt: null,
createdAt: { $lte: consideredClosedAt },
},
],
};
}
// Automatic story closing is not enabled. Stories will be considered
// closed if the ${closedAt} date is before ${now}.
return {
closedAt: { $lte: new Date() },
closedAt: { $lte: now },
};
default:
return {};
@@ -126,7 +201,7 @@ export default (ctx: GraphContext) => ({
// Merge the site filter into the connection filter.
...siteFilter(siteID),
// Merge the status filter into the connection filter.
...statusFilter(status),
...statusFilter(ctx.tenant.closeCommenting, status, ctx.now),
// Merge the query filters into the query.
...queryFilter(query),
},
+7 -10
View File
@@ -1,29 +1,26 @@
import { defaultsDeep } from "lodash";
import { decodeActionCounts } from "coral-server/models/action/comment";
import * as story from "coral-server/models/story";
import {
GQLSTORY_STATUS,
GQLStoryTypeResolver,
GQLTAG,
} from "coral-server/graph/schema/__generated__/types";
import { decodeActionCounts } from "coral-server/models/action/comment";
import * as story from "coral-server/models/story";
import GraphContext from "../context";
import { CommentCountsInput } from "./CommentCounts";
import { storyModerationInputResolver } from "./ModerationQueues";
const isStoryClosed = (s: story.Story, ctx: GraphContext) => {
const closedAt = story.getStoryClosedAt(ctx.tenant, s) || null;
return !!closedAt && new Date() >= closedAt;
};
export const Story: GQLStoryTypeResolver<story.Story> = {
comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input),
featuredComments: (s, input, ctx) =>
ctx.loaders.Comments.taggedForStory(s.id, GQLTAG.FEATURED, input),
status: (s, input, ctx) =>
isStoryClosed(s, ctx) ? GQLSTORY_STATUS.CLOSED : GQLSTORY_STATUS.OPEN,
isClosed: (s, input, ctx) => isStoryClosed(s, ctx),
story.isStoryClosed(ctx.tenant, s, ctx.now)
? GQLSTORY_STATUS.CLOSED
: GQLSTORY_STATUS.OPEN,
isClosed: (s, input, ctx) => story.isStoryClosed(ctx.tenant, s, ctx.now),
closedAt: (s, input, ctx) => story.getStoryClosedAt(ctx.tenant, s) || null,
commentActionCounts: s => decodeActionCounts(s.commentCounts.action),
commentCounts: (s): CommentCountsInput => s,
+12 -3
View File
@@ -26,10 +26,19 @@ export function getStoryTitle(story: Pick<Story, "metadata" | "url">) {
: story.url;
}
export function isStoryClosed(
tenant: Pick<Tenant, "closeCommenting">,
story: Pick<Story, "closedAt" | "createdAt">,
now = new Date()
) {
const closedAt = getStoryClosedAt(tenant, story);
return !!closedAt && closedAt <= now;
}
export function getStoryClosedAt(
tenant: Pick<Tenant, "closeCommenting">,
story: Pick<Story, "closedAt" | "createdAt">
): Story["closedAt"] {
): Date | null {
// Try to get the closedAt time from the story.
if (story.closedAt) {
return story.closedAt;
@@ -37,7 +46,7 @@ export function getStoryClosedAt(
// Check to see if the story has been forced open again.
if (story.closedAt === false) {
return false;
return null;
}
// If the story hasn't already been closed, then check to see if the Tenant
@@ -50,5 +59,5 @@ export function getStoryClosedAt(
.toJSDate();
}
return;
return null;
}
@@ -1,5 +1,5 @@
import { StoryClosedError } from "coral-server/errors";
import { getStoryClosedAt } from "coral-server/models/story";
import { isStoryClosed } from "coral-server/models/story";
import {
IntermediatePhaseResult,
ModerationPhaseContext,
@@ -14,8 +14,7 @@ export const storyClosed = ({
ModerationPhaseContext,
"story" | "tenant" | "now"
>): IntermediatePhaseResult | void => {
const closedAt = getStoryClosedAt(tenant, story);
if (closedAt && closedAt <= now) {
if (isStoryClosed(tenant, story, now)) {
throw new StoryClosedError();
}
};
@@ -16,6 +16,7 @@ import {
import {
createSite,
getURLOrigins,
retrieveTenantSites,
Site,
updateSiteCounts,
} from "coral-server/models/site";
@@ -24,68 +25,99 @@ import Migration from "coral-server/services/migrate/migration";
import collections from "coral-server/services/mongodb/collections";
import { MigrationError } from "../error";
import { createIndexesFactory } from "../indexing";
interface OldTenant extends Tenant {
allowedDomains: string[];
}
async function findOrCreateSite(
mongo: Db,
tenant: Readonly<OldTenant>
): Promise<Site> {
// Get all the sites attached to this Tenant (if there are any).
const sites = await retrieveTenantSites(mongo, tenant.id);
if (sites && sites.length > 0) {
// There was at least one site! If there is exactly one, then return it,
// otherwise there is more than one site.
if (sites.length === 1) {
return sites[0];
}
// There were more than 1 site! We can't handle this case.
throw new Error("more than one site for this tenant is available");
}
// Convert a tenant's domains into origins that we will re-use on the site.
const allowedOrigins = getURLOrigins([
...tenant.allowedDomains,
tenant.domain,
]);
return createSite(mongo, {
tenantID: tenant.id,
name: tenant.organization.name,
allowedOrigins,
});
}
export default class extends Migration {
private async createSite(mongo: Db, tenant: Readonly<OldTenant>) {
const {
organization: { name },
domain,
allowedDomains,
} = tenant;
private async findOrCreateSite(mongo: Db, tenant: Readonly<OldTenant>) {
// Try to find the site.
const site = await findOrCreateSite(mongo, tenant);
// Convert a tenant's domains into origins that we will re-use on the site.
const allowedOrigins = getURLOrigins([...allowedDomains, domain]);
// Create the new site.
const site = await createSite(mongo, {
name,
tenantID: tenant.id,
allowedOrigins,
});
this.logger.info({ site }, "created site");
this.log(tenant.id).info("starting stories migration");
// Add the siteID to all the stories.
let result = await collections
.stories(mongo)
.updateMany({ tenantID: tenant.id }, { $set: { siteID: site.id } });
.updateMany(
{ tenantID: tenant.id, siteID: null },
{ $set: { siteID: site.id } }
);
this.log(tenant.id).info(
{
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
},
"added siteID to stories"
"finished stories migration"
);
this.log(tenant.id).info("starting comments migration");
// Add the siteID to all comments.
result = await collections
.comments(mongo)
.updateMany({ tenantID: tenant.id }, { $set: { siteID: site.id } });
.updateMany(
{ tenantID: tenant.id, siteID: null },
{ $set: { siteID: site.id } }
);
this.log(tenant.id).info(
{
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
},
"added siteID to comments"
"finished comments migration"
);
this.log(tenant.id).info("starting commentActions migration");
// Add the siteID to all commentActions.
result = await collections
.commentActions(mongo)
.updateMany({ tenantID: tenant.id }, { $set: { siteID: site.id } });
.updateMany(
{ tenantID: tenant.id, siteID: null },
{ $set: { siteID: site.id } }
);
this.log(tenant.id).info(
{
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
},
"added siteID to commentActions"
"finished commentActions migration"
);
return site;
@@ -216,15 +248,8 @@ export default class extends Migration {
]);
}
// Try to find any site to see if this migration is needed.
let site = await collections.sites(mongo).findOne({ tenantID });
if (site) {
this.log(tenantID).info({ site }, "site has already been created");
return;
}
// Create the site.
site = await this.createSite(mongo, tenant);
const site = await this.findOrCreateSite(mongo, tenant);
// Update the story action counts on the stories.
await this.updateStoryActionCommentCounts(mongo, tenant);
@@ -232,4 +257,17 @@ export default class extends Migration {
// Update the site comment counts from the stories.
await this.updateCommentCounts(mongo, tenant, site);
}
public async indexes(mongo: Db) {
// Create the indexes factory.
const index = createIndexesFactory(mongo);
// Add indexes for { tenantID, siteID }.
await index.comments({ tenantID: 1, siteID: 1 }, { background: true });
await index.stories({ tenantID: 1, siteID: 1 }, { background: true });
await index.commentActions(
{ tenantID: 1, siteID: 1 },
{ background: true }
);
}
}