mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8368eb78d | ||
|
|
e88bac6e0f | ||
|
|
583ab0ff74 | ||
|
|
fb7ad76b8b | ||
|
|
f7d1543aa3 | ||
|
|
950a35310b | ||
|
|
9af7ae0357 | ||
|
|
073eea2847 | ||
|
|
c699e044fd | ||
|
|
4ef7abc132 | ||
|
|
9e486f3a9d | ||
|
|
fbcd390220 | ||
|
|
f0330ed21c | ||
|
|
75a944eba8 | ||
|
|
6850dd1a53 | ||
|
|
98ef60a48e | ||
|
|
8323344d7d | ||
|
|
fbecc363cf | ||
|
|
df925f5a1a | ||
|
|
30073b88f1 | ||
|
|
27ecbd1bdd | ||
|
|
4477c0981f | ||
|
|
2c9eae9264 | ||
|
|
812a4ee704 | ||
|
|
d3caf350ac | ||
|
|
864e3f1a1f | ||
|
|
61d16fbe08 | ||
|
|
000e26e3e1 | ||
|
|
71f0200557 | ||
|
|
d79edcd412 | ||
|
|
485b9d036f | ||
|
|
041ba95a5b | ||
|
|
d0d047f641 | ||
|
|
b057131d34 | ||
|
|
ba572fbf48 | ||
|
|
e3c1f8c231 | ||
|
|
07ddb94327 | ||
|
|
0addb938ae | ||
|
|
c0d01e55e1 | ||
|
|
3ac301142e | ||
|
|
7072aa013b | ||
|
|
091be25f36 | ||
|
|
41a3639d31 | ||
|
|
f96146bfd9 | ||
|
|
2eec87aa80 | ||
|
|
aabd40d53a | ||
|
|
9bce69a7cd | ||
|
|
6329e9f618 | ||
|
|
4f2d97cd82 | ||
|
|
6a00d3d249 | ||
|
|
d3040804a2 | ||
|
|
04ae844a1a |
@@ -128,7 +128,7 @@ class UserDetailContainer extends React.Component {
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
...CoralAdmin_UserDetail_CommentConnection
|
||||
}
|
||||
}
|
||||
${commentConnectionFragment}
|
||||
|
||||
@@ -38,7 +38,7 @@ function applyToCommentsOrigin(root, callback) {
|
||||
function findAndInsertComment(parent, comment) {
|
||||
const isAsset = parent.__typename === 'Asset';
|
||||
const [connectionField, countField, action] = isAsset
|
||||
? ['comments', 'commentCount', '$unshift']
|
||||
? ['comments', 'totalCommentCount', '$unshift']
|
||||
: ['replies', 'replyCount', '$push'];
|
||||
|
||||
if (
|
||||
@@ -67,19 +67,12 @@ function findAndInsertComment(parent, comment) {
|
||||
}
|
||||
|
||||
export function insertCommentIntoEmbedQuery(root, comment) {
|
||||
|
||||
// Increase total comment count by one.
|
||||
root = update(root, {
|
||||
asset: {
|
||||
totalCommentCount: {$apply: (c) => c + 1},
|
||||
},
|
||||
});
|
||||
return applyToCommentsOrigin(root, (origin) => findAndInsertComment(origin, comment));
|
||||
}
|
||||
|
||||
function findAndRemoveComment(parent, id) {
|
||||
const [connectionField, countField] = parent.__typename === 'Asset'
|
||||
? ['comments', 'commentCount']
|
||||
? ['comments', 'totalCommentCount']
|
||||
: ['replies', 'replyCount'];
|
||||
|
||||
const connection = parent[connectionField];
|
||||
@@ -104,13 +97,6 @@ function findAndRemoveComment(parent, id) {
|
||||
}
|
||||
|
||||
export function removeCommentFromEmbedQuery(root, id) {
|
||||
|
||||
// Decrease total comment by one.
|
||||
root = update(root, {
|
||||
asset: {
|
||||
totalCommentCount: {$apply: (c) => c - 1},
|
||||
},
|
||||
});
|
||||
return applyToCommentsOrigin(root, (origin) => findAndRemoveComment(origin, id));
|
||||
}
|
||||
|
||||
@@ -142,17 +128,21 @@ export function findCommentWithId(nodes, id) {
|
||||
}
|
||||
|
||||
export function findCommentInEmbedQuery(root, callbackOrId) {
|
||||
return findCommentInAsset(root.asset, callbackOrId);
|
||||
}
|
||||
|
||||
export function findCommentInAsset(asset, callbackOrId) {
|
||||
let callback = callbackOrId;
|
||||
if (typeof callbackOrId === 'string') {
|
||||
callback = (node) => node.id === callbackOrId;
|
||||
}
|
||||
if (root.asset.comment) {
|
||||
return findComment([getTopLevelParent(root.asset.comment)], callback);
|
||||
if (asset.comment) {
|
||||
return findComment([getTopLevelParent(asset.comment)], callback);
|
||||
}
|
||||
if (!root.asset.comments) {
|
||||
if (!asset.comments) {
|
||||
return false;
|
||||
}
|
||||
return findComment(root.asset.comments.nodes, callback);
|
||||
return findComment(asset.comments.nodes, callback);
|
||||
}
|
||||
|
||||
function findAndInsertFetchedComments(parent, comments, parent_id) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {Spinner} from 'coral-ui';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import {
|
||||
findCommentInEmbedQuery,
|
||||
findCommentInAsset,
|
||||
insertCommentIntoEmbedQuery,
|
||||
removeCommentFromEmbedQuery,
|
||||
insertFetchedCommentsIntoEmbedQuery,
|
||||
@@ -108,7 +109,7 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
|
||||
loadNewReplies = (parent_id) => {
|
||||
const comment = findCommentInEmbedQuery(this.props.root, parent_id);
|
||||
const comment = findCommentInAsset(this.props.asset, parent_id);
|
||||
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
@@ -323,7 +324,6 @@ const fragments = {
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
commentCount @skip(if: $hasComment)
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
@@ -344,7 +344,6 @@ const fragments = {
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
refetching: state.embed.refetching,
|
||||
commentCountCache: state.stream.commentCountCache,
|
||||
activeReplyBox: state.stream.activeReplyBox,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
|
||||
@@ -70,10 +70,9 @@ export class Talk {
|
||||
// Extract the asset url.
|
||||
if (opts.asset_url) {
|
||||
query.asset_url = opts.asset_url;
|
||||
} else if (!opts.asset_id) {
|
||||
} else {
|
||||
|
||||
// The asset url was not provided and the asset id was also not provided,
|
||||
// we need to infer the asset url from details on the page.
|
||||
// The asset url was not provided so we need to infer the asset url from // details on the page.
|
||||
|
||||
try {
|
||||
query.asset_url = document.querySelector('link[rel="canonical"]').href;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import graphql from '@coralproject/graphql-anywhere-optimized';
|
||||
import {createTypeGetter} from 'graphql-ast-tools';
|
||||
import introspectionData from './introspection.json';
|
||||
|
||||
// Use typeGetter to get more optimized documents.
|
||||
const typeGetter = createTypeGetter(introspectionData);
|
||||
|
||||
// Use global fragment cache for transformed fragments.
|
||||
const fragmentMap = {};
|
||||
|
||||
export default (...args) => {
|
||||
while (args.length < 7) {
|
||||
args.push(undefined);
|
||||
}
|
||||
|
||||
const transformOptions = {
|
||||
typeGetter,
|
||||
fragmentMap,
|
||||
};
|
||||
|
||||
args[6] = transformOptions;
|
||||
return graphql(...args);
|
||||
};
|
||||
@@ -1,293 +0,0 @@
|
||||
import {
|
||||
getMainDefinition,
|
||||
getFragmentDefinitions,
|
||||
createFragmentMap,
|
||||
shouldInclude,
|
||||
getOperationDefinition,
|
||||
} from 'apollo-utilities';
|
||||
|
||||
function getDirectivesID(directives) {
|
||||
let id = '';
|
||||
directives.forEach((directive) => {
|
||||
id += `@${directive.name.value}(`;
|
||||
let first = true;
|
||||
directive.arguments.forEach((arg) => {
|
||||
if (!first) {
|
||||
id += ',';
|
||||
}
|
||||
first = false;
|
||||
const value = arg.value.kind === 'Variable'
|
||||
? `$${arg.value.name.value}`
|
||||
: arg.value.value;
|
||||
id += `${arg.name.value}:${value}`;
|
||||
});
|
||||
id += ')';
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
// If two definitions have the same id, they can be merged.
|
||||
function getDefinitionID(definition) {
|
||||
|
||||
// Only merge when directives are exactly the same.
|
||||
const trailing = definition.directives.length
|
||||
? `_${getDirectivesID(definition.directives)}`
|
||||
: '';
|
||||
|
||||
switch (definition.kind) {
|
||||
case 'FragmentSpread':
|
||||
return `FragmentSpread_${definition.name.value}`;
|
||||
case 'Field':
|
||||
return `Field_${definition.alias ? definition.alias.value : definition.name.value}${trailing}`;
|
||||
case 'InlineFragment':
|
||||
return `InlineFragment_${definition.typeCondition.name.value}${trailing}`;
|
||||
default:
|
||||
throw new Error(`unknown definition kind ${definition.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge selections of 2 definitions.
|
||||
*/
|
||||
export function mergeDefinitions(a, b) {
|
||||
const name = getDefinitionID(a);
|
||||
|
||||
if (!!a.selectionSet !== !!b.selectionSet) {
|
||||
throw Error(`incompatible field definition for ${name}`);
|
||||
}
|
||||
|
||||
if (!a.selectionSet) {
|
||||
return b;
|
||||
}
|
||||
|
||||
const selectionSet = mergeSelectionSets(a.selectionSet, b.selectionSet);
|
||||
|
||||
return {
|
||||
...b,
|
||||
selectionSet,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge selectionSets
|
||||
*/
|
||||
export function mergeSelectionSets(a, b) {
|
||||
const selectionsMap = [...a.selections, ...b.selections].reduce((o, sel) => {
|
||||
const selName = getDefinitionID(sel);
|
||||
if (!(selName in o)) {
|
||||
o[selName] = sel;
|
||||
return o;
|
||||
}
|
||||
o[selName] = mergeDefinitions(o[selName], sel);
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
const selections = Object.keys(selectionsMap).map((key) => selectionsMap[key]);
|
||||
|
||||
return {
|
||||
...b,
|
||||
selections,
|
||||
};
|
||||
}
|
||||
|
||||
function getFragmentOrDie(name, execContext) {
|
||||
const {
|
||||
rawFragmentMap,
|
||||
fragmentMap,
|
||||
} = execContext;
|
||||
|
||||
if (!(name in fragmentMap)) {
|
||||
const fragment = rawFragmentMap[name];
|
||||
|
||||
if (!fragment) {
|
||||
throw new Error(`fragment ${fragment.name.value} does not exist`);
|
||||
}
|
||||
|
||||
const typeCondition = fragment.typeCondition.name.value;
|
||||
const transformed = transformDefinition(fragment, execContext, `type.${typeCondition}`, typeCondition);
|
||||
fragmentMap[name] = transformed;
|
||||
}
|
||||
|
||||
return fragmentMap[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selections with resolved named fragments and directives.
|
||||
*/
|
||||
function getTransformedSelections(definition, path, gqlType, execContext) {
|
||||
const {
|
||||
variables,
|
||||
} = execContext;
|
||||
|
||||
const selectionsMap = definition.selectionSet.selections.reduce((o, sel) => {
|
||||
if (variables && !shouldInclude(sel, variables)) {
|
||||
|
||||
// Skip this entirely
|
||||
return o;
|
||||
}
|
||||
if (sel.kind !== 'FragmentSpread') {
|
||||
const transformed = transformDefinition(sel, execContext, path, gqlType);
|
||||
const name = getDefinitionID(sel);
|
||||
|
||||
// Merge existing value.
|
||||
if (name in o) {
|
||||
o[name] = mergeDefinitions(o[name], transformed);
|
||||
return o;
|
||||
}
|
||||
|
||||
o[name] = transformed;
|
||||
return o;
|
||||
}
|
||||
|
||||
const fragment = getFragmentOrDie(sel.name.value, execContext);
|
||||
const typeCondition = fragment.typeCondition.name.value;
|
||||
|
||||
// Turn NamedFragment into an InlineFragment.
|
||||
if (gqlType !== typeCondition || fragment.directives.length) {
|
||||
const node = {
|
||||
...fragment,
|
||||
kind: 'InlineFragment',
|
||||
};
|
||||
const name = getDefinitionID(node);
|
||||
|
||||
// Merge existing value.
|
||||
if (name in o) {
|
||||
o[name] = mergeDefinitions(o[name], node);
|
||||
return o;
|
||||
}
|
||||
|
||||
o[name] = node;
|
||||
return o;
|
||||
}
|
||||
|
||||
// Merge NamedFragment directly into selections.
|
||||
const fragmentSelections = fragment.selectionSet.selections;
|
||||
fragmentSelections.forEach((s) => {
|
||||
|
||||
if (variables && !shouldInclude(s, variables)) {
|
||||
|
||||
// Skip this entirely
|
||||
return;
|
||||
}
|
||||
|
||||
const selName = getDefinitionID(s);
|
||||
if (!(selName in o)) {
|
||||
o[selName] = s;
|
||||
return;
|
||||
}
|
||||
|
||||
o[selName] = mergeDefinitions(o[selName], s);
|
||||
});
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
const selections = Object.keys(selectionsMap).map((key) => selectionsMap[key]);
|
||||
return selections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve named fragments and directives in a definition.
|
||||
*/
|
||||
function transformDefinition(definition, execContext, path = '', type = null) {
|
||||
if (!definition.selectionSet) {
|
||||
return definition;
|
||||
}
|
||||
|
||||
const {typeGetter} = execContext;
|
||||
|
||||
if (definition.kind === 'Field') {
|
||||
const fieldName = definition.name.value;
|
||||
path = `${path}.${fieldName}`;
|
||||
|
||||
if (typeGetter) {
|
||||
type = typeGetter(path);
|
||||
}
|
||||
}
|
||||
|
||||
// InlineFragments
|
||||
else if(!type && typeGetter) {
|
||||
type = typeGetter(path);
|
||||
}
|
||||
|
||||
return {
|
||||
...definition,
|
||||
selectionSet: {
|
||||
...definition.selectionSet,
|
||||
selections: getTransformedSelections(definition, path, type, execContext),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function reduceDocument(document, options = {}) {
|
||||
const mainDefinition = getMainDefinition(document);
|
||||
const fragments = getFragmentDefinitions(document);
|
||||
const operationDefinition = getOperationDefinition(document);
|
||||
const path = operationDefinition
|
||||
? operationDefinition.operation
|
||||
: `type.${mainDefinition.typeCondition.name.value}`;
|
||||
|
||||
const execContext = {
|
||||
rawFragmentMap: createFragmentMap(fragments),
|
||||
fragmentMap: options.fragmentMap || {},
|
||||
variables: options.variables,
|
||||
typeGetter: options.typeGetter || (() => null),
|
||||
};
|
||||
|
||||
return {
|
||||
kind: 'Document',
|
||||
definitions: [transformDefinition(mainDefinition, execContext, path)],
|
||||
};
|
||||
}
|
||||
|
||||
function getObjectType(fieldType) {
|
||||
if (['NON_NULL', 'LIST'].indexOf(fieldType.kind) > -1) {
|
||||
return getObjectType(fieldType.ofType);
|
||||
}
|
||||
return fieldType.name;
|
||||
}
|
||||
|
||||
function getFieldType(parentType, fieldName) {
|
||||
const field = parentType.fields.find((f) => f.name === fieldName);
|
||||
return getObjectType(field.type);
|
||||
}
|
||||
|
||||
export function createTypeGetter(introspectionData) {
|
||||
const types = {};
|
||||
introspectionData.__schema.types.forEach((type) => types[type.name] = type);
|
||||
|
||||
const result = {
|
||||
'query': introspectionData.__schema.queryType.name,
|
||||
'mutation': introspectionData.__schema.mutationType.name,
|
||||
'subscription': introspectionData.__schema.subscriptionType.name,
|
||||
};
|
||||
|
||||
return (path) => {
|
||||
if (result[path]) {
|
||||
return result[path];
|
||||
}
|
||||
let currentPath = '';
|
||||
const parts = path.split('.');
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
// Handle special path e.g. 'type.ROOT_QUERY.fieldName'
|
||||
if (part === 'type') {
|
||||
const type = parts[i + 1];
|
||||
const nextPath = `type.${type}`;
|
||||
result[nextPath] = type;
|
||||
currentPath = nextPath;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextPath = currentPath ? `${currentPath}.${part}` : part;
|
||||
if (nextPath in result) {
|
||||
currentPath = nextPath;
|
||||
continue;
|
||||
}
|
||||
result[nextPath] = getFieldType(types[result[currentPath]], part);
|
||||
currentPath = nextPath;
|
||||
}
|
||||
return result[path];
|
||||
};
|
||||
}
|
||||
@@ -108,6 +108,12 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
this.subscriptionQueue = [];
|
||||
}, 1000);
|
||||
|
||||
handleOnLoaded() {
|
||||
|
||||
// Trigger subscription queue processing after query has loaded.
|
||||
setTimeout(() => this.processSubscriptionQueue(), 1000);
|
||||
}
|
||||
|
||||
subscribeToMoreThrottled = ({document, variables, updateQuery}) => {
|
||||
|
||||
// We need to add the typenames and resolve fragments.
|
||||
@@ -122,8 +128,12 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
if (data) {
|
||||
this.subscriptionQueue.push([updateQuery, data]);
|
||||
|
||||
// Triggers the throttled subscription queue processor.
|
||||
this.processSubscriptionQueue();
|
||||
// Only trigger handler when query has been loaded.
|
||||
if (!this.data.loading) {
|
||||
|
||||
// Triggers the throttled subscription queue processor.
|
||||
this.processSubscriptionQueue();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,6 +155,11 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => {
|
||||
|
||||
// If data was previously set, we update it in a immutable way.
|
||||
if (this.data) {
|
||||
|
||||
if (this.data.loading && !data.loading) {
|
||||
this.handleOnLoaded();
|
||||
}
|
||||
|
||||
if (this.data.networkStatus !== data.networkStatus ||
|
||||
this.data.loading !== data.loading ||
|
||||
this.data.error !== data.error ||
|
||||
|
||||
@@ -124,10 +124,6 @@ export async function createContext({
|
||||
const plugins = createPluginsService(pluginsConfig);
|
||||
const graphql = createGraphQLService(
|
||||
createGraphQLRegistry(plugins.getSlotFragments.bind(plugins)),
|
||||
{
|
||||
introspectionData,
|
||||
optimize: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
);
|
||||
if (!notification) {
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import reduceDocument, {createTypeGetter} from '../graphql/reduceDocument';
|
||||
import {addTypenameToDocument} from 'apollo-client/queries/queryTransform';
|
||||
|
||||
/**
|
||||
@@ -6,18 +5,7 @@ import {addTypenameToDocument} from 'apollo-client/queries/queryTransform';
|
||||
* @param {string} basename base path of the url
|
||||
* @return {Object} histor service
|
||||
*/
|
||||
export function createGraphQLService(registry, {
|
||||
introspectionData,
|
||||
optimize = false,
|
||||
}) {
|
||||
const reduceOptions = {
|
||||
typeGetter: optimize && introspectionData ? createTypeGetter(introspectionData) : null,
|
||||
|
||||
// Use shared fragment map.
|
||||
// Attention: Fragment names must be unique otherwise weird things will happen.
|
||||
fragmentMap: {},
|
||||
};
|
||||
|
||||
export function createGraphQLService(registry) {
|
||||
return {
|
||||
registry,
|
||||
resolveDocument(documentOrCallback, props, context) {
|
||||
@@ -26,10 +14,6 @@ export function createGraphQLService(registry, {
|
||||
: documentOrCallback;
|
||||
document = registry.resolveFragments(document);
|
||||
|
||||
if (optimize) {
|
||||
document = reduceDocument(document, reduceOptions);
|
||||
}
|
||||
|
||||
// We also add typenames to the document which apollo would usually do,
|
||||
// but we also use the network interface in subscriptions directly
|
||||
// which require the resolved typenames.
|
||||
|
||||
@@ -24,6 +24,10 @@ const CONFIG = {
|
||||
// indexes.
|
||||
CREATE_MONGO_INDEXES: process.env.DISABLE_CREATE_MONGO_INDEXES !== 'TRUE',
|
||||
|
||||
// SETTINGS_CACHE_TIME is the time that we'll cache the settings in redis before
|
||||
// fetching again.
|
||||
SETTINGS_CACHE_TIME: ms(process.env.TALK_SETTINGS_CACHE_TIME || '1hr'),
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -26,7 +26,7 @@ to persist data. The following versions are supported:
|
||||
|
||||
An optional dependency for Talk is
|
||||
[Docker](https://www.docker.com/community-edition#/download){:target="_blank"}.
|
||||
It is used during [development](#development) to setup the database and can be
|
||||
It is used during [development](#development) to set up the database and can be
|
||||
used to [install via Docker](#installation-from-docker). We have tested Talk
|
||||
and this documentation with versions {{ site.versions.docker }}.
|
||||
|
||||
@@ -182,4 +182,4 @@ machine.
|
||||
|
||||
At this point you've successfully installed, configured, and ran your very own
|
||||
instance of Talk! Continue through this documentation on this site to learn more
|
||||
on how to configure, develop with, and contribute to Talk!
|
||||
on how to configure, develop with, and contribute to Talk!
|
||||
|
||||
+105
-59
@@ -1,31 +1,14 @@
|
||||
const DataLoader = require('dataloader');
|
||||
const url = require('url');
|
||||
const {URL} = require('url');
|
||||
const {singleJoinBy, SingletonResolver} = require('./util');
|
||||
|
||||
const errors = require('../../errors');
|
||||
const scraper = require('../../services/scraper');
|
||||
const util = require('./util');
|
||||
|
||||
const AssetModel = require('../../models/asset');
|
||||
const AssetsService = require('../../services/assets');
|
||||
|
||||
/**
|
||||
* Retrieves assets by an array of ids.
|
||||
* @param {Object} context the context of the request
|
||||
* @param {Array} ids array of ids to lookup
|
||||
*/
|
||||
const genAssetsByID = (context, ids) => AssetModel.find({
|
||||
const genAssetsByID = ({connectors: {models: {Asset}}}, ids) => Asset.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
}).then(util.singleJoinBy(ids, 'id'));
|
||||
}).then(singleJoinBy(ids, 'id'));
|
||||
|
||||
/**
|
||||
* [getAssetsByQuery description]
|
||||
* @param {Object} context the context of the request
|
||||
* @param {Object} query the query
|
||||
* @return {Promise} resolves the assets
|
||||
*/
|
||||
const getAssetsByQuery = async (context, query) => {
|
||||
const getAssetsByQuery = async ({connectors: {services: {Assets}}}, query) => {
|
||||
|
||||
// If we are requesting based on a limit, ask for one more than we want.
|
||||
const limit = query.limit;
|
||||
@@ -33,7 +16,7 @@ const getAssetsByQuery = async (context, query) => {
|
||||
query.limit += 1;
|
||||
}
|
||||
|
||||
const nodes = await AssetsService.search(query);
|
||||
const nodes = await Assets.search(query);
|
||||
|
||||
// The hasNextPage is always handled the same (ask for one more than we need,
|
||||
// if there is one more, than there is more).
|
||||
@@ -54,58 +37,121 @@ const getAssetsByQuery = async (context, query) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint find or creates an asset at the given url when it is loaded.
|
||||
* @param {Object} context the context of the request
|
||||
* @param {String} asset_url the url passed in from the query
|
||||
* @returns {Promise} resolves to the asset
|
||||
*/
|
||||
const findOrCreateAssetByURL = async (context, asset_url) => {
|
||||
const findOrCreateAssetByURL = async (ctx, url) => {
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
let parsed_asset_url = url.parse(asset_url);
|
||||
if (!parsed_asset_url.protocol) {
|
||||
throw errors.ErrInvalidAssetURL;
|
||||
// Pull our connectors out of the context.
|
||||
const {
|
||||
loaders: {
|
||||
Assets,
|
||||
Settings,
|
||||
},
|
||||
connectors: {
|
||||
models: {
|
||||
Asset,
|
||||
},
|
||||
services: {
|
||||
DomainList,
|
||||
Scraper,
|
||||
},
|
||||
errors: {
|
||||
ErrInvalidAssetURL,
|
||||
},
|
||||
},
|
||||
} = ctx;
|
||||
|
||||
// Try to validate that the url is valid. If the URL constructor throws an
|
||||
// error, throw our internal ErrInvalidAssetURL instead. This will validate
|
||||
// that the url contains a valid scheme.
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (err) {
|
||||
throw ErrInvalidAssetURL;
|
||||
}
|
||||
|
||||
let asset = await AssetsService.findOrCreateByUrl(asset_url);
|
||||
// Try the easy lookup first.
|
||||
let asset = await Assets.findByUrl(url);
|
||||
if (asset) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
// If the asset wasn't scraped before, scrape it! Otherwise just return
|
||||
// the asset.
|
||||
// Seems the asset wasn't here yet.. We should do some validation.
|
||||
|
||||
// Check for whitelisting + get the settings at the same time.
|
||||
const [
|
||||
whitelisted,
|
||||
settings,
|
||||
] = await Promise.all([
|
||||
DomainList.urlCheck(url),
|
||||
Settings.load('autoCloseStream closedTimeout'),
|
||||
]);
|
||||
|
||||
// If the domain wasn't whitelisted, then we shouldn't create this asset!
|
||||
if (!whitelisted) {
|
||||
throw ErrInvalidAssetURL;
|
||||
}
|
||||
|
||||
// Construct the update operator that we'll use to create the asset.
|
||||
const update = {
|
||||
$setOnInsert: {
|
||||
url,
|
||||
},
|
||||
};
|
||||
|
||||
// If the auto-close stream is enabled, close the stream after the designated
|
||||
// timeout.
|
||||
if (settings.autoCloseStream) {
|
||||
update.$setOnInsert.closedAt = new Date(Date.now() + settings.closedTimeout * 1000);
|
||||
}
|
||||
|
||||
// We're using the findOneAndUpdate here instead of a insert to protect
|
||||
// against race conditions.
|
||||
asset = await Asset.findOneAndUpdate({
|
||||
url,
|
||||
}, update, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true,
|
||||
|
||||
// Ensure that we validate the input that we do have.
|
||||
runValidators: true,
|
||||
});
|
||||
|
||||
// If this is a new asset, then we need to scrape it!
|
||||
if (!asset.scraped) {
|
||||
await scraper.create(asset);
|
||||
|
||||
// Create the Scraper job.
|
||||
await Scraper.create(asset);
|
||||
}
|
||||
|
||||
return asset;
|
||||
};
|
||||
|
||||
const findByUrl = async (context, asset_url) => {
|
||||
const findByUrl = async ({connectors: {errors, services: {Assets}}}, asset_url) => {
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
let parsed_asset_url = url.parse(asset_url);
|
||||
if (!parsed_asset_url.protocol) {
|
||||
// Try to validate that the url is valid. If the URL constructor throws an
|
||||
// error, throw our internal ErrInvalidAssetURL instead. This will validate
|
||||
// that the url contains a valid scheme.
|
||||
try {
|
||||
new URL(asset_url);
|
||||
} catch (err) {
|
||||
throw errors.ErrInvalidAssetURL;
|
||||
}
|
||||
|
||||
return AssetsService.findByUrl(asset_url);
|
||||
return Assets.findByUrl(asset_url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
|
||||
module.exports = (context) => ({
|
||||
module.exports = (ctx) => ({
|
||||
Assets: {
|
||||
|
||||
// TODO: decide whether we want to move these to mutators or not, as in fact
|
||||
// this operation create a new asset if one isn't found.
|
||||
getByURL: (url) => findOrCreateAssetByURL(context, url),
|
||||
|
||||
findByUrl: (url) => findByUrl(context, url),
|
||||
getByQuery: (query) => getAssetsByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
getAll: new util.SingletonResolver(() => AssetModel.find({}))
|
||||
getByURL: (url) => findOrCreateAssetByURL(ctx, url),
|
||||
findByUrl: (url) => findByUrl(ctx, url),
|
||||
getByQuery: (query) => getAssetsByQuery(ctx, query),
|
||||
getByID: new DataLoader((ids) => genAssetsByID(ctx, ids)),
|
||||
getAll: new SingletonResolver(() => ctx.connectors.models.Asset.find({}))
|
||||
}
|
||||
});
|
||||
|
||||
+17
-19
@@ -82,54 +82,52 @@ const getParentCountsByAssetID = (context, asset_ids) => {
|
||||
|
||||
/**
|
||||
* Retrieves the count of comments based on the passed in query.
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} ctx graph context
|
||||
* @param {Object} query query to execute against the comments collection
|
||||
* to compute the counts
|
||||
* @return {Promise} resolves to the counts of the comments from the
|
||||
* query
|
||||
*/
|
||||
const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, author_id, tags, action_type}) => {
|
||||
let query = CommentModel.find();
|
||||
const getCommentCountByQuery = (ctx, options) => {
|
||||
const {statuses, asset_id, parent_id, author_id, tags, action_type} = options;
|
||||
|
||||
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special privileges.
|
||||
if (
|
||||
(!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
|
||||
(context.user == null || !context.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
|
||||
(ctx.user == null || !ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (statuses && statuses.length > 0) {
|
||||
query = query.where({status: {$in: statuses}});
|
||||
}
|
||||
|
||||
if (ids) {
|
||||
query = query.where({id: {$in: ids}});
|
||||
}
|
||||
const query = CommentModel.find();
|
||||
|
||||
if (asset_id != null) {
|
||||
query = query.where({asset_id});
|
||||
query.merge({asset_id});
|
||||
}
|
||||
|
||||
if (parent_id !== undefined) {
|
||||
query = query.where({parent_id});
|
||||
query.merge({parent_id});
|
||||
}
|
||||
|
||||
if (author_id) {
|
||||
query = query.where({author_id});
|
||||
query.merge({author_id});
|
||||
}
|
||||
|
||||
if (context.user != null && context.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
query = query.where({
|
||||
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
query.merge({
|
||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||
$gt: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
query = query.find({
|
||||
if (statuses && statuses.length > 0) {
|
||||
query.merge({status: {$in: statuses}});
|
||||
}
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
query.merge({
|
||||
'tags.tag.name': {
|
||||
$in: tags,
|
||||
},
|
||||
@@ -289,7 +287,7 @@ const getCommentsByQuery = async (ctx, {ids, statuses, asset_id, parent_id, auth
|
||||
let comments = CommentModel.find();
|
||||
|
||||
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special priviledges.
|
||||
// special privileges.
|
||||
if (
|
||||
(!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
|
||||
(ctx.user == null || !ctx.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
const SettingsService = require('../../services/settings');
|
||||
const {SingletonResolver} = require('./util');
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
module.exports = () => ({
|
||||
Settings: new SingletonResolver(() => SettingsService.retrieve())
|
||||
});
|
||||
module.exports = () => {
|
||||
const loader = new DataLoader((selections) => Promise.all(selections.map((fields) => {
|
||||
return SettingsService.retrieve(fields);
|
||||
})));
|
||||
|
||||
return {
|
||||
Settings: {
|
||||
load: (fields = false) => loader.load(fields),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -178,7 +178,7 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
|
||||
// just added a new comment, hence the counts should be updated. We should
|
||||
// perform these increments in the event that we do have a new comment that
|
||||
// is approved or without a comment.
|
||||
if (status === 'NONE' || status === 'APPROVED') {
|
||||
if (status === 'NONE' || status === 'ACCEPTED') {
|
||||
if (parent_id === null) {
|
||||
Comments.parentCountByAssetID.incr(asset_id);
|
||||
}
|
||||
|
||||
+20
-8
@@ -123,20 +123,23 @@ CommentSchema.index({
|
||||
background: true,
|
||||
});
|
||||
|
||||
// Add an index that is optimized for sorting based on the action count data.
|
||||
CommentSchema.index({
|
||||
'created_at': 1,
|
||||
'action_counts.flag': 1,
|
||||
}, {
|
||||
background: true,
|
||||
});
|
||||
|
||||
// Create a sparse index to search across.
|
||||
CommentSchema.index({
|
||||
'created_at': 1,
|
||||
'action_counts.flag': 1,
|
||||
'status': 1,
|
||||
}, {
|
||||
background: true,
|
||||
sparse: true,
|
||||
});
|
||||
|
||||
// Create a sparse index to search across.
|
||||
CommentSchema.index({
|
||||
'action_counts.flag': 1,
|
||||
'status': 1,
|
||||
}, {
|
||||
background: true,
|
||||
sparse: true,
|
||||
});
|
||||
|
||||
// Add an index that is optimized for finding flagged comments.
|
||||
@@ -166,6 +169,15 @@ CommentSchema.index({
|
||||
background: true,
|
||||
});
|
||||
|
||||
// Optimize for tag searches/counts.
|
||||
CommentSchema.index({
|
||||
'tags.tag.name': 1,
|
||||
'status': 1,
|
||||
}, {
|
||||
background: true,
|
||||
sparse: true,
|
||||
});
|
||||
|
||||
// Add an index that is optimized for sorting based on the created_at timestamp
|
||||
// but also good at locating comments that have a specific asset id.
|
||||
CommentSchema.index({
|
||||
|
||||
+2
-2
@@ -9,13 +9,13 @@ module.exports = {
|
||||
globals_path: './test/e2e/globals',
|
||||
selenium: {
|
||||
start_process: true,
|
||||
server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.6.0-server.jar',
|
||||
server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.7.1-server.jar',
|
||||
log_path: './test/e2e/',
|
||||
host: '127.0.0.1',
|
||||
port: 6666,
|
||||
cli_args: {
|
||||
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.33-x64-chromedriver',
|
||||
'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.19.0-x64-geckodriver',
|
||||
'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.19.1-x64-geckodriver',
|
||||
}
|
||||
},
|
||||
test_settings: {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.3",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -61,7 +61,6 @@
|
||||
"@coralproject/graphql-anywhere-optimized": "^0.1.0",
|
||||
"accepts": "^1.3.4",
|
||||
"apollo-client": "^1.9.1",
|
||||
"apollo-utilities": "^1.0.3",
|
||||
"app-module-path": "^2.2.0",
|
||||
"autoprefixer": "^6.5.2",
|
||||
"babel-cli": "6.26.0",
|
||||
@@ -107,6 +106,7 @@
|
||||
"fs-extra": "^4.0.1",
|
||||
"gql-merge": "^0.0.4",
|
||||
"graphql": "^0.9.1",
|
||||
"graphql-ast-tools": "0.2.3",
|
||||
"graphql-docs": "0.2.0",
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-redis-subscriptions": "1.3.0",
|
||||
|
||||
+2
-2
@@ -110,9 +110,9 @@ router.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
||||
// Interactive graphiql interface.
|
||||
router.use('/api/v1/graph/iql', (req, res) => {
|
||||
router.use('/api/v1/graph/iql', staticTemplate, (req, res) => {
|
||||
res.render('graphiql', {
|
||||
endpointURL: `${req.app.locals.BASE_URL}api/v1/graph/ql`
|
||||
endpointURL: 'api/v1/graph/ql'
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+4
-1
@@ -8,7 +8,10 @@ E2E_MAX_RETRIES=${E2E_MAX_RETRIES:-1}
|
||||
|
||||
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
|
||||
# IE 64bit has issues with receiving keyboard input. Let's wait for them to fix it.
|
||||
BROWSERS="chrome,firefox,edge" #ie safari
|
||||
|
||||
# FIXME: disabled firefox,edge until fixing pass is done
|
||||
# BROWSERS="chrome,firefox,edge" #ie safari
|
||||
BROWSERS="chrome"
|
||||
|
||||
if [[ "${CIRCLE_BRANCH}" == "master" && -n "$BROWSERSTACK_KEY" ]]; then
|
||||
echo Testing on browserstack
|
||||
|
||||
+88
-16
@@ -61,30 +61,36 @@ cache.init = async () => {
|
||||
|
||||
// This is designed to increment a key and add an expiry iff the key already
|
||||
// exists.
|
||||
const INCR_SCRIPT = `
|
||||
if redis.call('GET', KEYS[1]) ~= false then
|
||||
redis.call('INCR', KEYS[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
`;
|
||||
|
||||
cache.client.defineCommand('increx', {
|
||||
numberOfKeys: 1,
|
||||
lua: INCR_SCRIPT,
|
||||
lua: `
|
||||
if redis.call('GET', KEYS[1]) ~= false then
|
||||
redis.call('INCR', KEYS[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
`,
|
||||
});
|
||||
|
||||
// This is designed to decrement a key and add an expiry iff the key already
|
||||
// exists.
|
||||
const DECR_SCRIPT = `
|
||||
if redis.call('GET', KEYS[1]) ~= false then
|
||||
redis.call('DECR', KEYS[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
`;
|
||||
|
||||
cache.client.defineCommand('decrex', {
|
||||
numberOfKeys: 1,
|
||||
lua: DECR_SCRIPT,
|
||||
lua: `
|
||||
if redis.call('GET', KEYS[1]) ~= false then
|
||||
redis.call('DECR', KEYS[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
`,
|
||||
});
|
||||
|
||||
cache.client.defineCommand('hincrbyex', {
|
||||
numberOfKeys: 2,
|
||||
lua: `
|
||||
if redis.call('HGET', KEYS[1], KEYS[2]) ~= false then
|
||||
redis.call('HINCRBY', KEYS[1], KEYS[2], ARGV[1])
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[2])
|
||||
end
|
||||
`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -270,3 +276,69 @@ cache.set = async (key, value, expiry, kf = keyfunc) => {
|
||||
|
||||
return cache.client.set(kf(key), reply, 'EX', expiry);
|
||||
};
|
||||
|
||||
/**
|
||||
* h is the hash form of the cache.
|
||||
*/
|
||||
cache.h = {};
|
||||
|
||||
cache.h.get = async (key, field = '__default__') => {
|
||||
|
||||
// Get the current value from redis.
|
||||
const reply = await cache.client.hget(keyfunc(key), field);
|
||||
|
||||
if (typeof reply !== 'undefined' && reply !== null) {
|
||||
return JSON.parse(reply);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
cache.h.set = async (key, field = '__default__', value, expiry = 60) => {
|
||||
|
||||
// Serialize the value as JSON.
|
||||
let reply = JSON.stringify(value);
|
||||
|
||||
return cache.client
|
||||
.pipeline()
|
||||
.hset(keyfunc(key), field, reply)
|
||||
.expire(keyfunc(key), expiry)
|
||||
.exec();
|
||||
};
|
||||
|
||||
cache.h.invalidate = async (key, field = null) => {
|
||||
if (field === null) {
|
||||
return cache.invalidate(key);
|
||||
}
|
||||
|
||||
debug(`invalidate: ${keyfunc(key)} ${field}`);
|
||||
|
||||
return cache.client.hdel(keyfunc(key), field);
|
||||
};
|
||||
|
||||
cache.h.wrap = async (key, field, expiry, work) => {
|
||||
let value = await cache.h.get(key, field);
|
||||
if (value !== null) {
|
||||
debug('wrap: hit', keyfunc(key));
|
||||
return value;
|
||||
}
|
||||
|
||||
debug('wrap: miss', keyfunc(key));
|
||||
|
||||
value = await work();
|
||||
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
await cache.h.set(key, field, value, expiry);
|
||||
debug('wrap: set complete');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
cache.h.incr = async (key, field = '__default__', expiry) => cache.client.hincrbyex(keyfunc(key), field, 1, expiry);
|
||||
|
||||
cache.h.decr = async (key, field = '__default__', expiry) => cache.client.hincrbyex(keyfunc(key), field, -1, expiry);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
const cache = require('./cache');
|
||||
const debug = require('debug')('talk:services:hcache');
|
||||
|
||||
const kf = (key) => `hcache:${key}`;
|
||||
|
||||
const hcache = module.exports = {};
|
||||
|
||||
hcache.get = async (key, field = '__default__') => {
|
||||
|
||||
// Get the current value from redis.
|
||||
const reply = await cache.client.hget(kf(key), field);
|
||||
|
||||
if (typeof reply !== 'undefined' && reply !== null) {
|
||||
return JSON.parse(reply);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
hcache.set = async (key, field = '__default__', value, expiry = 60) => {
|
||||
|
||||
// Serialize the value as JSON.
|
||||
let reply = JSON.stringify(value);
|
||||
|
||||
return cache.client
|
||||
.pipeline()
|
||||
.hset(kf(key), field, reply)
|
||||
.expire(kf(key), expiry)
|
||||
.exec();
|
||||
};
|
||||
|
||||
hcache.del = async (key, field = null) => {
|
||||
if (field === null) {
|
||||
return cache.client.del(kf(key));
|
||||
}
|
||||
|
||||
return cache.client.hdel(kf(key), field);
|
||||
};
|
||||
|
||||
hcache.wrap = async (key, field, expiry, work) => {
|
||||
let value = await hcache.get(key, field);
|
||||
if (value !== null) {
|
||||
debug('wrap: hit', kf(key));
|
||||
return value;
|
||||
}
|
||||
|
||||
debug('wrap: miss', kf(key));
|
||||
|
||||
value = await work();
|
||||
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
await hcache.set(key, field, value, expiry);
|
||||
debug('wrap: set complete');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
};
|
||||
+15
-4
@@ -9,7 +9,8 @@ const kue = require('kue');
|
||||
// singleton Queue instance. So you can configure and use only a single Queue
|
||||
// object within your node.js process.
|
||||
let queue = null;
|
||||
const getQueue = () => {
|
||||
let isManaging = false;
|
||||
const getQueue = ({managed = false} = {}) => {
|
||||
if (queue) {
|
||||
return queue;
|
||||
}
|
||||
@@ -21,8 +22,16 @@ const getQueue = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for stuck jobs to manage.
|
||||
queue.watchStuckJobs(1000);
|
||||
// If this is a managed queue, and we aren't managing yet, then start the
|
||||
// management.
|
||||
if (managed && !isManaging) {
|
||||
|
||||
// Watch for stuck jobs to manage.
|
||||
queue.watchStuckJobs(60000);
|
||||
|
||||
// Mark that we've now started management routines.
|
||||
isManaging = true;
|
||||
}
|
||||
|
||||
return queue;
|
||||
};
|
||||
@@ -67,7 +76,9 @@ class Task {
|
||||
* Process jobs for the queue.
|
||||
*/
|
||||
process(callback) {
|
||||
return getQueue().process(this.name, callback);
|
||||
|
||||
// Get the queue in managed mode.
|
||||
return getQueue({managed: true}).process(this.name, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const SettingModel = require('../models/setting');
|
||||
const hcache = require('./hcache');
|
||||
const cache = require('./cache');
|
||||
const errors = require('../errors');
|
||||
const {dotize} = require('./utils');
|
||||
const {SETTINGS_CACHE_TIME} = require('../config');
|
||||
|
||||
/**
|
||||
* The selector used to uniquely identify the settings document.
|
||||
@@ -35,7 +36,7 @@ module.exports = class SettingsService {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// When in production, wrap the settings retrieval with a cache.
|
||||
const settings = await hcache.wrap('settings', fields, 60, () => retrieve(fields));
|
||||
const settings = await cache.h.wrap('settings', fields, SETTINGS_CACHE_TIME / 1000, () => retrieve(fields));
|
||||
|
||||
return new SettingModel(settings);
|
||||
}
|
||||
@@ -58,7 +59,7 @@ module.exports = class SettingsService {
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
await hcache.del('settings');
|
||||
await cache.h.invalidate('settings');
|
||||
}
|
||||
|
||||
return updatedSettings;
|
||||
|
||||
@@ -25,8 +25,28 @@ class SortedWindowHandler {
|
||||
*/
|
||||
windowHandles(callback) {
|
||||
this.client.windowHandles((result) => {
|
||||
this.handles = this.handles.filter((handle) => result.value.includes(handle));
|
||||
const remaining = result.value.filter((handle) => !this.handles.includes(handle));
|
||||
if (Array.isArray(result.value)) {
|
||||
this.handles = this.handles.filter((handle) => {
|
||||
for (let i = 0; i < result.value.length; i++) {
|
||||
if (result.value[i] === handle) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
this.handles = [];
|
||||
}
|
||||
const remaining = result.value.filter((handle) => {
|
||||
for (let i = 0; i < this.handles.length; i++) {
|
||||
if (this.handles[i] === handle) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
if (remaining.length === 1) {
|
||||
this.handles.push(remaining[0]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '<%= endpointURL %>');
|
||||
var fetchURL = locationQuery(otherParams, '<%= BASE_URL %><%= endpointURL %>');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ const config = {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'graphql-anywhere': '@coralproject/graphql-anywhere-optimized',
|
||||
'graphql-anywhere': path.resolve(__dirname, 'client/coral-framework/graphql/anywhere'),
|
||||
'plugin-api': path.resolve(__dirname, 'plugin-api/'),
|
||||
plugins: path.resolve(__dirname, 'plugins/'),
|
||||
pluginsConfig: pluginsPath
|
||||
|
||||
Reference in New Issue
Block a user