diff --git a/.gitignore b/.gitignore index fc9aa6bf..b3295956 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ pm_to_blib release-archives/ target/ .cpcache/ +.classpath +.factorypath # Elastic Beanstalk Files .ebextensions/app-env.config @@ -53,5 +55,11 @@ data/ # Source code linter cache /.clj-kondo/.cache/ +# IDE project/module settings +.vscode +.project +.settings + # VS Code calva extension configs .calva +.lsp diff --git a/Makefile b/Makefile index c19f4697..32e3912b 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ ifeq ($(PROJ_NAME), wormbase-names) else ifeq ($(PROJ_NAME), wormbase-names-test) WB_DB_URI ?= "datomic:ddb://${AWS_DEFAULT_REGION}/WSNames-test-14/wormbase" GOOGLE_REDIRECT_URI ?= "https://test-names.wormbase.org" - APP_PROFILE ?= "prod" + APP_PROFILE ?= "test" else WB_DB_URI ?= "datomic:ddb-local://localhost:8000/WBNames_local/wormbase" # Ensure GOOGLE_REDIRECT_URI is defined appropriately as an env variable or CLI argument @@ -170,7 +170,7 @@ docker-tag: ENV.VERSION_TAG \ @docker tag ${ECR_REPO_NAME}:${VERSION_TAG} ${ECR_REPO_URI} .PHONY: eb-def-app-env -eb-def-app-env: google-oauth2-secrets ENV.VERSION_TAG \ +eb-def-app-env: google-oauth2-secrets caltech-api-secrets ENV.VERSION_TAG \ $(call print-help,eb-def-app-env \ [WB_DB_URI=] [GOOGLE_REDIRECT_URI=],\ Define the ElasticBeanStalk app-environment config file.) @@ -185,6 +185,9 @@ endif sed -i -r 's~(API_GOOGLE_OAUTH_CLIENT_ID:\s+)".*"~\1"'"${GOOGLE_OAUTH_CLIENT_ID}"'"~' .ebextensions/${EB_APP_ENV_FILE} sed -i -r 's~(API_GOOGLE_OAUTH_CLIENT_SECRET:\s+)".*"~\1"'"${GOOGLE_OAUTH_CLIENT_SECRET}"'"~' .ebextensions/${EB_APP_ENV_FILE} sed -i -r 's~(GOOGLE_REDIRECT_URI:\s+)".*"~\1"'"${GOOGLE_REDIRECT_URI}"'"~' .ebextensions/${EB_APP_ENV_FILE} + sed -i -r 's~(CALTECH_API_URL:\s+)".*"~\1"'"${CALTECH_API_URL}"'"~' .ebextensions/${EB_APP_ENV_FILE} + sed -i -r 's~(CALTECH_API_USER:\s+)".*"~\1"'"${CALTECH_API_USER}"'"~' .ebextensions/${EB_APP_ENV_FILE} + sed -i -r 's~(CALTECH_API_PASSWORD:\s+)".*"~\1"'"${CALTECH_API_PASSWORD}"'"~' .ebextensions/${EB_APP_ENV_FILE} sed -i -r 's~(WB_NAMES_RELEASE: ).+~\1'${VERSION_TAG}'~' .ebextensions/${EB_APP_ENV_FILE} .PHONY: eb-create @@ -306,24 +309,30 @@ release: ENV.VERSION_TAG deploy-ecr \ Release the applicaton.) .PHONY: run-tests -run-tests: google-oauth2-secrets \ +run-tests: google-oauth2-secrets caltech-api-secrets \ $(call print-help,run-tests,\ Run all tests.) - @ export API_GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} && \ - export API_GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} && \ - export GOOGLE_REDIRECT_URI=${LOCAL_GOOGLE_REDIRECT_URI} && \ + @ export API_GOOGLE_OAUTH_CLIENT_ID="${GOOGLE_OAUTH_CLIENT_ID}" && \ + export API_GOOGLE_OAUTH_CLIENT_SECRET="${GOOGLE_OAUTH_CLIENT_SECRET}" && \ + export CALTECH_API_URL="${CALTECH_API_URL}" && \ + export CALTECH_API_USER="${CALTECH_API_USER}" && \ + export CALTECH_API_PASSWORD="${CALTECH_API_PASSWORD}" && \ + export GOOGLE_REDIRECT_URI="${LOCAL_GOOGLE_REDIRECT_URI}" && \ clojure -A:datomic-pro:logging:webassets:dev:test:run-tests .PHONY: run-dev-webserver run-dev-webserver: PORT := 4010 -run-dev-webserver: google-oauth2-secrets \ +run-dev-webserver: google-oauth2-secrets caltech-api-secrets \ $(call print-help,run-dev-webserver PORT= WB_DB_URI= \ GOOGLE_REDIRECT_URI=,\ Run a local development webserver.) @ export WB_DB_URI=${WB_DB_URI} && export PORT=${PORT} && \ - export GOOGLE_REDIRECT_URI=${GOOGLE_REDIRECT_URI} && \ - export API_GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} && \ - export API_GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} && \ + export GOOGLE_REDIRECT_URI="${GOOGLE_REDIRECT_URI}" && \ + export API_GOOGLE_OAUTH_CLIENT_ID="${GOOGLE_OAUTH_CLIENT_ID}" && \ + export API_GOOGLE_OAUTH_CLIENT_SECRET="${GOOGLE_OAUTH_CLIENT_SECRET}" && \ + export CALTECH_API_URL="${CALTECH_API_URL}" && \ + export CALTECH_API_USER="${CALTECH_API_USER}" && \ + export CALTECH_API_PASSWORD="${CALTECH_API_PASSWORD}" && \ clj -A:logging:datomic-pro:webassets:dev -m wormbase.names.service .PHONY: run-dev-ui @@ -356,11 +365,46 @@ ENV.GOOGLE_OAUTH_CLIENT_SECRET: source-secrets \ $(call check_defined, GOOGLE_OAUTH_CLIENT_SECRET, Check the defined APP_PROFILE value\ and ensure the AWS_PROFILE variable is appropriately defined) +.PHONY: ENV.CALTECH_API_URL +ENV.CALTECH_API_URL: \ + $(call print-help,ENV.CALTECH_API_URL,\ + Retrieve the CALTECH_API_URL env variable for make targets from aws ssm if undefined.) + $(eval ACTION_MSG := $(if ${CALTECH_API_URL},"Using predefined CALTECH_API_URL.","Retrieving CALTECH_API_URL from AWS SSM (APP_PROFILE '${APP_PROFILE}').")) + @echo ${ACTION_MSG} + $(if ${CALTECH_API_URL},,$(eval CALTECH_API_URL := $(shell ${AWS_CLI_BASE} ssm get-parameter --name "/name-service/${APP_PROFILE}/caltech-api-config/url" --query "Parameter.Value" --output text --with-decryption))) + $(call check_defined, CALTECH_API_URL, Check the defined APP_PROFILE value\ + and ensure the AWS_PROFILE variable is appropriately defined) + +.PHONY: ENV.CALTECH_API_USER +ENV.CALTECH_API_USER: \ + $(call print-help,ENV.CALTECH_API_USER,\ + Retrieve the CALTECH_API_USER env variable for make targets from aws ssm if undefined.) + $(eval ACTION_MSG := $(if ${CALTECH_API_USER},"Using predefined CALTECH_API_USER.","Retrieving CALTECH_API_USER from AWS SSM (APP_PROFILE '${APP_PROFILE}').")) + @echo ${ACTION_MSG} + $(if ${CALTECH_API_USER},,$(eval CALTECH_API_USER := $(shell ${AWS_CLI_BASE} ssm get-parameter --name "/name-service/${APP_PROFILE}/caltech-api-config/username" --query "Parameter.Value" --output text --with-decryption))) + $(call check_defined, CALTECH_API_USER, Check the defined APP_PROFILE value\ + and ensure the AWS_PROFILE variable is appropriately defined) + +.PHONY: ENV.CALTECH_API_PASSWORD +ENV.CALTECH_API_PASSWORD: \ + $(call print-help,ENV.CALTECH_API_PASSWORD,\ + Retrieve the CALTECH_API_PASSWORD env variable for make targets from aws ssm if undefined.) + $(eval ACTION_MSG := $(if ${CALTECH_API_PASSWORD},"Using predefined CALTECH_API_PASSWORD.","Retrieving CALTECH_API_PASSWORD from AWS SSM (APP_PROFILE '${APP_PROFILE}').")) + @echo ${ACTION_MSG} + $(if ${CALTECH_API_PASSWORD},,$(eval CALTECH_API_PASSWORD := $(shell ${AWS_CLI_BASE} ssm get-parameter --name "/name-service/${APP_PROFILE}/caltech-api-config/password" --query "Parameter.Value" --output text --with-decryption))) + $(call check_defined, CALTECH_API_PASSWORD, Check the defined APP_PROFILE value\ + and ensure the AWS_PROFILE variable is appropriately defined) + .PHONY: google-oauth2-secrets google-oauth2-secrets: ENV.GOOGLE_OAUTH_CLIENT_ID ENV.GOOGLE_OAUTH_CLIENT_SECRET \ $(call print-help,google-oauth2-secrets,\ Store the Google oauth2 client details as env variables.) +.PHONY: caltech-api-secrets +caltech-api-secrets: ENV.CALTECH_API_URL ENV.CALTECH_API_USER ENV.CALTECH_API_PASSWORD \ + $(call print-help,caltech-api-secrets,\ + Store the Caltech API details as env variables.) + ${STORE_SECRETS_FILE}: google-oauth2-secrets @install -m 600 /dev/null ${STORE_SECRETS_FILE} @echo "GOOGLE_OAUTH_CLIENT_ID:=${GOOGLE_OAUTH_CLIENT_ID}" >> ${STORE_SECRETS_FILE} diff --git a/client/package-lock.json b/client/package-lock.json index 5d0edae4..aa48a2b4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13750,15 +13750,6 @@ } } }, - "react-document-title": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/react-document-title/-/react-document-title-2.0.3.tgz", - "integrity": "sha1-u/kioNcUEvyUgkXkKDskEt9w8rk=", - "requires": { - "prop-types": "^15.5.6", - "react-side-effect": "^1.0.2" - } - }, "react-dom": { "version": "16.8.5", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.5.tgz", @@ -13904,14 +13895,6 @@ } } }, - "react-side-effect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz", - "integrity": "sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==", - "requires": { - "shallowequal": "^1.0.1" - } - }, "react-storage-hooks": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/react-storage-hooks/-/react-storage-hooks-3.0.1.tgz", @@ -15072,11 +15055,6 @@ } } }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/client/package.json b/client/package.json index f26e13bd..a7a13335 100644 --- a/client/package.json +++ b/client/package.json @@ -25,7 +25,6 @@ "prop-types": "15.7.2", "query-string": "5", "react": "16.8.5", - "react-document-title": "2.0.3", "react-dom": "16.8.5", "react-router-dom": "5.3.4", "react-storage-hooks": "3.0.1", diff --git a/client/src/components/elements/DocumentTitle.js b/client/src/components/elements/DocumentTitle.js deleted file mode 100644 index b115b2b6..00000000 --- a/client/src/components/elements/DocumentTitle.js +++ /dev/null @@ -1,3 +0,0 @@ -import DocumentTitle from 'react-document-title'; - -export default DocumentTitle; diff --git a/client/src/components/elements/NotFound.js b/client/src/components/elements/NotFound.js index e3c0201f..12d00836 100644 --- a/client/src/components/elements/NotFound.js +++ b/client/src/components/elements/NotFound.js @@ -1,20 +1,23 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { Typography, withStyles } from '@material-ui/core'; -class NotFound extends Component { - render() { - const { classes } = this.props; - return ( -
- - Not Found - - {this.props.children} -
- ); - } -} +import { useTitle } from '../../hooks/useTitle'; + +const NotFound = (props) => { + const { classes } = props; + + useTitle('Not found'); + + return ( +
+ + Not Found + + {props.children} +
+ ); +}; NotFound.propTypes = { children: PropTypes.element, diff --git a/client/src/components/elements/Page/index.js b/client/src/components/elements/Page/index.js index e9790886..efb841bc 100644 --- a/client/src/components/elements/Page/index.js +++ b/client/src/components/elements/Page/index.js @@ -2,11 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core'; -function page({ classes, ...others }) { - return
; -} -page.propTypes = { +import { useTitle } from '../../../hooks/useTitle'; + +const Page = (props) => { + useTitle(props.title); + return
; +}; +Page.propTypes = { classes: PropTypes.object.isRequired, + title: PropTypes.string, }; function pageMain({ classes, ...others }) { @@ -30,7 +34,7 @@ pageRight.propTypes = { classes: PropTypes.object.isRequired, }; -export const Page = withStyles((theme) => ({ +export default withStyles((theme) => ({ root: { display: 'flex', margin: theme.spacing(4), @@ -38,7 +42,7 @@ export const Page = withStyles((theme) => ({ flexDirection: 'column', }, }, -}))(page); +}))(Page); export const PageLeft = withStyles((theme) => ({ left: { @@ -70,5 +74,3 @@ export const PageMain = withStyles((theme) => ({ // }, // }, // }))(pageRight); - -export default Page; diff --git a/client/src/components/elements/TextArea.js b/client/src/components/elements/TextArea.js index c6e286e8..7f2bcf49 100644 --- a/client/src/components/elements/TextArea.js +++ b/client/src/components/elements/TextArea.js @@ -2,5 +2,5 @@ import React from 'react'; import TextField from './TextField'; export default function TextArea(props) { - return ; + return ; } diff --git a/client/src/components/elements/index.js b/client/src/components/elements/index.js index 2221f04d..bc11b518 100644 --- a/client/src/components/elements/index.js +++ b/client/src/components/elements/index.js @@ -6,7 +6,7 @@ import { import { MuiThemeProvider, - createMuiTheme, + createTheme, withStyles, Dialog as MuiDialog, withMobileDialog, @@ -44,13 +44,12 @@ export { default as AutocompleteSuggestion } from './AutocompleteSuggestion'; export { default as BaseForm } from './BaseForm'; export { default as BiotypeSelect } from './BiotypeSelect'; export { default as Button } from './Button'; -export { default as DocumentTitle } from './DocumentTitle'; export { default as EntityTypeSelect } from './EntityTypeSelect'; export { default as ErrorBoundary } from './ErrorBoundary'; export { default as SimpleAjax } from './SimpleAjax'; export { default as NoData } from './NoData'; export { default as NotFound } from './NotFound'; -export { Page, PageLeft, PageMain } from './Page'; +export { default as Page, PageLeft, PageMain } from './Page'; export * from './ProgressButton'; export { default as ProgressButton } from './ProgressButton'; export { default as SimpleListPagination } from './SimpleListPagination'; @@ -85,7 +84,7 @@ export const Dialog = withMobileDialog()( // }; // All the following keys are optional. // We try our best to provide a great default value. -export const theme = createMuiTheme({ +export const theme = createTheme({ palette: { primary: { main: primaryColor[700], @@ -104,4 +103,4 @@ export const theme = createMuiTheme({ }, }); -export { MuiThemeProvider, createMuiTheme, withStyles }; +export { MuiThemeProvider, createTheme, withStyles }; diff --git a/client/src/containers/Authenticate/Profile.js b/client/src/containers/Authenticate/Profile.js index 61928e0f..6b3a9e08 100644 --- a/client/src/containers/Authenticate/Profile.js +++ b/client/src/containers/Authenticate/Profile.js @@ -2,7 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '../../components/elements'; +import { useTitle } from '../../hooks/useTitle'; + const Profile = (props) => { + useTitle('Your profile'); return (

{props.name}

diff --git a/client/src/containers/Authenticate/ProfileButton.js b/client/src/containers/Authenticate/ProfileButton.js index 3ce0d92b..5c5bcba1 100644 --- a/client/src/containers/Authenticate/ProfileButton.js +++ b/client/src/containers/Authenticate/ProfileButton.js @@ -4,12 +4,13 @@ import { Button, withStyles } from '../../components/elements'; import { Link } from 'react-router-dom'; import PersonIcon from '@material-ui/icons/Person'; +const my_profile_link = React.forwardRef(function(props, ref) { + return ; +}); + const ProfileButton = (props) => { return props.name ? ( - diff --git a/client/src/containers/Entity/EntityCreate.js b/client/src/containers/Entity/EntityCreate.js index 5430fb59..9e061d67 100644 --- a/client/src/containers/Entity/EntityCreate.js +++ b/client/src/containers/Entity/EntityCreate.js @@ -8,7 +8,6 @@ import EntityEditNew from './EntityEditNew'; import EntityForm from './EntityForm'; import { Button, - DocumentTitle, ErrorBoundary, Page, PageLeft, @@ -43,47 +42,45 @@ class EntityCreate extends Component { const ReasonField = withFieldData(TextArea, 'why'); return ( - - - -
- -
-
- - - Add {entityType} - - - {renderForm ? ( - - {renderForm(formContext)} - {dirtinessContext(({ dirty }) => - true ? ( - - ) : null - )} - - ) : null} -
- - - Create - -
-
-
-
+ + +
+ +
+
+ + + Add {entityType} + + + {renderForm ? ( + + {renderForm(formContext)} + {dirtinessContext(({ dirty }) => + true ? ( + + ) : null + )} + + ) : null} +
+ + + Create + +
+
+
); }} diff --git a/client/src/containers/Entity/EntityDirectory.js b/client/src/containers/Entity/EntityDirectory.js index c4a8f02d..a132b83a 100644 --- a/client/src/containers/Entity/EntityDirectory.js +++ b/client/src/containers/Entity/EntityDirectory.js @@ -6,7 +6,6 @@ import Typography from '@material-ui/core/Typography'; import { capitalize } from '../../utils/format'; import { Button, - DocumentTitle, ErrorBoundary, Page, PageMain, @@ -20,43 +19,42 @@ const EntityDirectory = (props) => { entityType, renderHistory = () => , //Coming soon..., } = props; + const new_entity_link = React.forwardRef((props, ref) => ( + + )); return ( - - - -
-
- - {/* -
OR
-
- -
- */} -
+ + +
+
+ + {/* +
OR
+
+ +
+ */}
-
- {/* tables and charts */} - { -
- - Recent activities - - {renderHistory()} -
- } -
- - - +
+
+ {/* tables and charts */} + { +
+ + Recent activities + + {renderHistory()} +
+ } +
+
+
); }; diff --git a/client/src/containers/Entity/EntityDirectoryButton.js b/client/src/containers/Entity/EntityDirectoryButton.js index 2fec3522..04c3aa7f 100644 --- a/client/src/containers/Entity/EntityDirectoryButton.js +++ b/client/src/containers/Entity/EntityDirectoryButton.js @@ -5,11 +5,12 @@ import { Button } from '@material-ui/core'; export default function EntityDirectoryButton(props) { const { entityType } = props; + const entity_directory_link = React.forwardRef(function(props, ref) { + return ; + }); + return ( - ); diff --git a/client/src/containers/Entity/EntityEditNew.js b/client/src/containers/Entity/EntityEditNew.js index d3008668..99618805 100644 --- a/client/src/containers/Entity/EntityEditNew.js +++ b/client/src/containers/Entity/EntityEditNew.js @@ -76,30 +76,112 @@ class EntityEditNew extends Component { } ) .then((response) => { - return response.json(); - }) - .then((response) => { - if (!response.created) { + this.setState({ + error: null, + }); + + if (!response.ok) { + console.log( + 'Error response received on entity creation request.' + ); + + let error_message = `API error. Status ${response.status} (${ + response.statusText + }) received, but no message.`; + this.setState({ - error: response, - status: 'COMPLETE', + error: { message: error_message }, }); + } + + return response.json(); + }) + .then((response_body) => { + if (this.state.error || !response_body.created) { + console.log( + 'state.error found or response_body.created not found.' + ); + console.log('state.error: ', this.state.error); + console.log('response_body.created: ', response_body.created); + + let new_state = this.state; + new_state['status'] = 'COMPLETE'; + + if (response_body.message) { + new_state['error'] = { message: response_body.message }; + } else { + console.log('Error response received but no message.'); + console.log('Full response body:', response_body); + } + + this.setState(new_state); } else { - this.setState( - { - data: response.created, - error: null, - status: 'COMPLETE', - }, - () => { + let redirect_delay = 0; + + let new_state = this.state; + + new_state['data'] = response_body.created; + new_state['status'] = 'COMPLETE'; + + if ('caltech-sync' in response_body) { + let caltech_result = response_body['caltech-sync']; + let message = `Entity ${ + response_body.created.id + } created successfully.`; + if ( + [200, 201, 202].includes( + caltech_result['http-response-status-code'] + ) + ) { + redirect_delay += 2000; + message += ' Caltech sync successful.'; + } else { + redirect_delay += 3000; + message += ` Error returned by Caltech sync API (HTTP Code ${ + caltech_result['http-response-status-code'] + }).`; + } + + if ('caltech-message' in caltech_result) { + redirect_delay += 2000; + message += ` Message returned: "${ + caltech_result['caltech-message'] + }".`; + } + + message += ' Redirecting to entity page...'; + + console.log('Caltech sync display message:', message); + + new_state['error'] = { message: message }; + } else { + console.log( + 'Entity successfully created, but no caltech-sync property found.' + ); + new_state['error'] = null; + } + + this.setState(new_state, () => { + setTimeout(() => { this.props.history.push( - `/${entityType}/id/${response.created.id}` + `/${entityType}/id/${response_body.created.id}` ); - } - ); + }, redirect_delay); + }); } }) - .catch((e) => console.log('error', e)); + .catch((e) => { + console.log('Caught error:', e); + this.setState({ + error: { + message: + 'Uncaught error: ' + + e.toString() + + '. See console for more details.', + }, + status: 'COMPLETE', + }); + }); } ); }; diff --git a/client/src/containers/Entity/EntityNotFound.js b/client/src/containers/Entity/EntityNotFound.js index 75cf0d4c..fc9d86eb 100644 --- a/client/src/containers/Entity/EntityNotFound.js +++ b/client/src/containers/Entity/EntityNotFound.js @@ -2,31 +2,32 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { withStyles, Button, Typography } from '@material-ui/core'; -import { DocumentTitle, NotFound } from '../../components/elements'; +import { NotFound } from '../../components/elements'; import EntityDirectoryButton from './EntityDirectoryButton'; function EntityNotFound(props) { const { classes = {}, wbId, entityType } = props; + + const new_entity_link = React.forwardRef(function(props, ref) { + return ; + }); + return ( - - - - {wbId} does not exist - -
- - -
-
-
+ + + {wbId} does not exist + +
+ + +
+
); } diff --git a/client/src/containers/Entity/EntityProfile.js b/client/src/containers/Entity/EntityProfile.js index c723f1a8..51414f32 100644 --- a/client/src/containers/Entity/EntityProfile.js +++ b/client/src/containers/Entity/EntityProfile.js @@ -9,7 +9,6 @@ import { import { Button, - DocumentTitle, ErrorBoundary, Humanize, Page, @@ -132,103 +131,101 @@ class EntityProfile extends Component { const ReasonField = withFieldData(TextArea, 'why'); return ( - - - -
- - - {renderOperations && - renderOperations({ + + +
+ + + {renderOperations && + renderOperations({ + ...renderProps, + getOperationProps, + getDialogProps, + })} + {renderOperationTip + ? renderOperationTip && + renderOperationTip({ ...renderProps, - getOperationProps, - getDialogProps, - })} - {renderOperationTip - ? renderOperationTip && - renderOperationTip({ - ...renderProps, - Wrapper: ({ children }) => ( - -
Tip:
- {children} -
- ), - }) - : null} + Wrapper: ({ children }) => ( + +
Tip:
+ {children} +
+ ), + }) + : null} +
+
+ + + {entityType} {wbId}{' '} + + + Update + +
- - - - {entityType} {wbId}{' '} - - - Update - -
+ {renderChanges ? ( +
+ + {renderChanges(renderProps)} +
- )} -
- - Change history - - {renderChanges ? ( -
- - {renderChanges(renderProps)} - -
- ) : null} -
- + ) : null} +
+ - + {message}} onClose={onMessageClose} - transitionDuration={0} - > - {message}} - onClose={onMessageClose} - /> - - - + /> + + ); }} @@ -241,8 +238,8 @@ EntityProfile.propTypes = { entityType: PropTypes.string.isRequired, wbId: PropTypes.string.isRequired, apiPrefix: PropTypes.string, - withFieldData: PropTypes.func.isRequired, - dirtinessContext: PropTypes.func.isRequired, + withFieldData: PropTypes.func, + dirtinessContext: PropTypes.func, errorMessage: PropTypes.string, message: PropTypes.string, messageVariant: PropTypes.oneOf(['info', 'warning']), diff --git a/client/src/containers/Entity/useEntityTypes.js b/client/src/containers/Entity/useEntityTypes.js index 33a13483..c965afd6 100644 --- a/client/src/containers/Entity/useEntityTypes.js +++ b/client/src/containers/Entity/useEntityTypes.js @@ -1,9 +1,6 @@ import React, { createContext, useCallback, useMemo, useContext } from 'react'; import PropTypes from 'prop-types'; -import { - createMuiTheme, - theme as defaultTheme, -} from '../../components/elements'; +import { createTheme, theme as defaultTheme } from '../../components/elements'; import { green as geneColor, purple as variationColor, @@ -34,7 +31,7 @@ const processEntityTypeConfig = ({ color, ...entityTypeConfig }) => { ? `/api/entity/${entityType}` : `/api/${entityType}`, theme: color - ? createMuiTheme({ + ? createTheme({ palette: { ...defaultTheme.palette, secondary: { diff --git a/client/src/containers/Header/NavBar.js b/client/src/containers/Header/NavBar.js index 903017b8..010709ae 100644 --- a/client/src/containers/Header/NavBar.js +++ b/client/src/containers/Header/NavBar.js @@ -27,20 +27,25 @@ const NavBar = (props) => { centered={true} className={props.classes.root} > - {allTabs.map(({ entityType, displayName, path }) => ( - } - /> - ))} + {allTabs.map(({ entityType, displayName, path }) => { + const entity_dir_link = React.forwardRef((props, ref) => ( + + )); + return ( + + ); + })} ); }; diff --git a/client/src/containers/Header/index.js b/client/src/containers/Header/index.js index 17736190..a4ccbb3f 100644 --- a/client/src/containers/Header/index.js +++ b/client/src/containers/Header/index.js @@ -19,6 +19,21 @@ const Header = (props) => { const { classes } = props; const { getEntityType } = useEntityTypes(); + const entity_type_searchbox = React.forwardRef((props, ref) => { + const entityType = props.match.params.entityType; + + return getEntityType(entityType) ? ( + + ) : null; + }); + return (
@@ -34,22 +49,7 @@ const Header = (props) => {
{props.isAuthenticated ? ( - { - const entityType = match.params.entityType; - - return getEntityType(entityType) ? ( - - ) : null; - }} - /> + ) : null} {props.children} diff --git a/client/src/containers/Home/index.js b/client/src/containers/Home/index.js index 115942f1..8c8299c5 100644 --- a/client/src/containers/Home/index.js +++ b/client/src/containers/Home/index.js @@ -17,31 +17,38 @@ function Home({ classes }) {
- {entityTypesAll.map(({ entityType, path, theme, displayName }) => ( - -
- - + +
+
+ - -
-
- -
-
- ))} +
+ + ); + })}
diff --git a/client/src/containers/Main/index.js b/client/src/containers/Main/index.js index b059694c..6fbf0709 100644 --- a/client/src/containers/Main/index.js +++ b/client/src/containers/Main/index.js @@ -7,7 +7,6 @@ import { MuiThemeProvider, CircularProgress, Page, - DocumentTitle, ErrorBoundary, NotFound, } from '../../components/elements'; @@ -63,15 +62,13 @@ function Main({ classes }) { ( - - - <> - -
- - -
-
+ + <> + +
+ + +
)} /> { {({ pageItems, navigation }) => isOpen ? ( - {pageItems.map((suggestion, index) => ( - ( + {pageItems.map((suggestion, index) => { + const search_result_link = React.forwardRef( + (props, ref) => ( { @@ -189,16 +187,25 @@ const SearchBox = (props) => { `/${entityType}/id/${suggestion.id}` ); }} + ref={ref} > - {children} + {props.children} - )} - index={index} - highlightedIndex={highlightedIndex} - selectedItem={selectedItem} - itemProps={getItemProps({ item: suggestion })} - /> - ))} + ) + ); + + return ( + + ); + })} {navigation} ) : null diff --git a/client/src/hooks/useTitle.js b/client/src/hooks/useTitle.js new file mode 100644 index 00000000..fbe28e22 --- /dev/null +++ b/client/src/hooks/useTitle.js @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +export function useTitle(title) { + useEffect(() => { + if (title) { + const prevTitle = document.title; + document.title = title; + return () => { + document.title = prevTitle; + }; + } else { + return undefined; + } + }); +} diff --git a/deps.edn b/deps.edn index 441a567d..70894829 100644 --- a/deps.edn +++ b/deps.edn @@ -7,8 +7,10 @@ :deps {org.clojure/clojure {:mvn/version "1.10.1"} org.clojure/data.csv {:mvn/version "1.0.1"} + org.clojure/data.json {:mvn/version "2.5.0"} org.clojure/tools.cli {:mvn/version "1.0.194"} org.clojure/tools.logging {:mvn/version "1.2.4"} + org.clojure/tools.reader {:mvn/version "1.3.6"} clojure.java-time/clojure.java-time {:mvn/version "1.4.2"} aero/aero {:mvn/version "1.1.6"} bk/ring-gzip {:mvn/version "0.3.0"} @@ -22,6 +24,7 @@ :exclusions [frankiesardo/linked]} metosin/muuntaja {:mvn/version "0.6.8"} metosin/ring-http-response {:mvn/version "0.9.3"} + clj-http/clj-http {:mvn/version "3.12.3"} ikitommi/linked {:mvn/version "1.3.1-alpha1"} mount/mount {:mvn/version "0.1.17"} phrase/phrase {:mvn/version "0.3-alpha4"} diff --git a/ebextensions-templates/app-env.config b/ebextensions-templates/app-env.config index ee80477e..fe63f72c 100644 --- a/ebextensions-templates/app-env.config +++ b/ebextensions-templates/app-env.config @@ -5,4 +5,7 @@ option_settings: GOOGLE_REDIRECT_URI: "" API_GOOGLE_OAUTH_CLIENT_ID: "" API_GOOGLE_OAUTH_CLIENT_SECRET: "" - _JAVA_OPTIONS: "-Xmx4g" + CALTECH_API_URL: "" + CALTECH_API_USER: "" + CALTECH_API_PASSWORD: "" + _JAVA_OPTIONS: "_Xmx4g" diff --git a/resources/config.edn b/resources/config.edn index 2676b06d..ce437670 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -2,17 +2,6 @@ :datomic {:internal-namespaces ["db" "db.alter" "db.install" "db.excise" "db.sys" "conformity" "fressian"]} - :event-broadcast - {:gmail #include "secrets/wb-ns-gmail.edn" - :aws-sqs - {:queue-name "org-wormbase-names-tx_messages" - :attributes - {:VisibilityTimeout 30 ;; sec - :MaximumMessageSize 65536 ;; bytes - :MessageRetentionPeriod 1209600 ;; sec - :ReceiveMessageWaitTimeSeconds 10}} - :aws-s3 - {:bucket-name "wormbase"}} :oauth2 {:google {:launch-uri "/oauth2/google" diff --git a/src/wormbase/names/entity.clj b/src/wormbase/names/entity.clj index 5bfba7a6..58cc5880 100644 --- a/src/wormbase/names/entity.clj +++ b/src/wormbase/names/entity.clj @@ -1,8 +1,11 @@ (ns wormbase.names.entity - (:require [clojure.spec.alpha :as s] + (:require [clojure.data.json :as json] + [clojure.spec.alpha :as s] [clojure.string :as str] + [clj-http.client :as client] [compojure.api.sweet :as sweet] [datomic.api :as d] + [environ.core :refer [env]] [magnetcoop.stork :as stork] [ring.util.http-response :refer [bad-request! conflict conflict! created not-found not-found! ok]] @@ -105,10 +108,27 @@ {} data)) -(defn creator +(defn new-entity-replay-caltech + "Replay entity creation to Caltech API endpoint" + [entity-type entity-id entity-name] + (if (some #{entity-type} '("variation" "strain")) + (let [api_url (env :caltech-api-url) + user (env :caltech-api-user) + pass (env :caltech-api-password) + body (json/write-str {"datatype" entity-type + "objId" entity-id + "objName" entity-name})] + (client/post api_url {:basic-auth [user pass] + :accept "text/plain" + :content-type :json + :body body + :throw-exceptions false})) + nil)) + +(defn new-entity-handler-creator "Return an endpoint handler for new entity creation." [uiident conform-spec-fn event get-info-fn & [validate]] - (fn handle-new [request] + (fn [request] (let [{payload :body-params db :db conn :conn} request ent-ns (namespace uiident) live-status-attr (keyword ent-ns "status") @@ -133,16 +153,31 @@ tx-res @(d/transact-async conn tx-data) dba (:db-after tx-res)] (when dba - (let [new-id (wdb/extract-id tx-res uiident) - emap (get-info-fn dba [uiident new-id]) - emap* (reduce-kv (fn [m k v] - (if (qualified-keyword? v) - (assoc m k (name v)) - (assoc m k v))) - {} - emap) - result {:created (wnu/unqualify-keys emap* ent-ns)}] - (created (str "/" ent-ns "/") result))))))) + (let [unqualified-emap (as-> (wdb/extract-id tx-res uiident) $ + (get-info-fn dba [uiident $]) + (reduce-kv (fn [m k v] + (if (qualified-keyword? v) + (assoc m k (name v)) + (assoc m k v))) + {} + $) + (wnu/unqualify-keys $ ent-ns)) + caltech-response (new-entity-replay-caltech ent-ns (:id unqualified-emap) (:name unqualified-emap)) + caltech-sync (if caltech-response + (merge {:http-response-status-code (:status caltech-response)} + (if (seq (:body caltech-response)) + (try + (let [raw-response-body (:body caltech-response) + response-body (json/read-str raw-response-body) + caltech-message (get response-body "message")] + {:caltech-message caltech-message}) + (catch Exception _ + {:http-response-body (:body caltech-response)})) + nil)) + nil) + response-body (-> {:created unqualified-emap} + (cond-> (seq caltech-sync) (assoc :caltech-sync caltech-sync)))] + (created (str "/" ent-ns "/") response-body))))))) (defn merge-into-ent-data [data ent-data db uiident] @@ -409,7 +444,7 @@ {entity-type :entity-type id-template :id-template generic? :generic - name-required? :name-required} data + name-required? :name-required?} data id-attr (keyword entity-type "id") prov (wnp/assoc-provenance request payload :event/new-entity-type)] (when-not entity-type @@ -517,8 +552,8 @@ (list-entity-schemas request))} :post {:summary "Add a new simple entity type to the system." - :responses (wnu/response-map created {:schema ::wse/schema-created}) - :parameters {:body-params {:data ::wse/new-schema + :responses (wnu/response-map created {:schema ::wse/new-type-response}) + :parameters {:body-params {:data ::wse/new-type :prov ::wsp/provenance}} :handler (fn register-entity-schema [request] (handle-register-entity-schema request))}}))) @@ -554,19 +589,19 @@ :parameters {:body-params {:data ::wse/new :prov ::wsp/provenance}} :responses (-> wnu/default-responses - (assoc created {:schema {:created ::wse/created}}) + (assoc created {:schema ::wse/created-response}) (wnu/response-map)) - :handler (fn new-entity [request] + :handler (fn [request] (let [id-ident (keyword entity-type "id") event-ident (keyword "event" (str "new-" entity-type)) conformer (partial wnu/conform-data ::wse/new) summary-fn (partial pull-ent-summary entity-type) - new-entity (creator id-ident - conformer - event-ident - summary-fn - validate-names)] - (new-entity request)))}}) + create-entity (new-entity-handler-creator id-ident + conformer + event-ident + summary-fn + validate-names)] + (create-entity request)))}}) (sweet/context "/:identifier" [] :tags ["entity"] :middleware [entity-enabled-checker] diff --git a/src/wormbase/names/event_broadcast.clj b/src/wormbase/names/event_broadcast.clj deleted file mode 100644 index 89e70b1a..00000000 --- a/src/wormbase/names/event_broadcast.clj +++ /dev/null @@ -1,94 +0,0 @@ -(ns wormbase.names.event-broadcast - "Relay messages for consumption by parties interested (primarily ACeDB clients). - - DEPREACTED: This module is not currently used. - The intent was for \"events\" perfomed via the Web UI to be queued in - some external storage for the purpose of relaying in another database (GeneACe). - This idea has been superceded by the GeneACe curator querying a rest endpoint periodically. - - The module here is perserved \"just in case\" we want to use it, - but the event broadcast facility has been switched off (No events will be relayed to any storage). - " - (:require - [clojure.tools.logging :as log] - [datomic.api :as d] - [wormbase.names.util :as wnu] - [wormbase.names.event-broadcast.proto :as wneb])) - -(defn read-changes [{:keys [db-after tx-data]}] - (d/q '[:find ?aname ?val - :in $ [[_ ?a ?val]] - :where - [?a :db/ident ?aname]] - db-after - tx-data)) - -(defn process-report - "Process a transaction report using `event-brodcaster`. - - `include-agents` can be used to filter which events can be sent by - the agent described in `:provenance/how`. It should be function - accepting a single argument, and should return a boolean. - - `tx-report-queue` should be an instance of the blocking queue returned by - `datomic.api/tx-report-queue`. - - `report` should be the element from the `tx-report-queue` queue to be processed." - [event-broadcaster include-agents tx-report-queue report] - (let [db-after (:db-after report) - changes (->> report - read-changes - (into {}) - (wnu/resolve-refs db-after)) - tx-k->db-id (->> (:tx-data report) - (map (fn [datom] - (list (d/ident db-after (.a datom)) (.e datom)))) - (map (partial apply array-map)) - (into {}))] - ;; debugging: - ;; (ownu/datom-table db-after (:tx-data report)) - (when (include-agents (:provenance/how changes)) - (log/info "Sending event message to event brodcaster.") - (wneb/send-message event-broadcaster - (assoc changes - :tx-id - (format "0x%016x" (:db/txInstant tx-k->db-id)))) - (while (not (wneb/message-persisted? event-broadcaster changes)) - (log/info "Message not persisted in storage yet, backing-off for 5 seconds") - ;; Perhaps terminate this loop and abort if unable to get a result? - (Thread/sleep 5000)) - (.remove tx-report-queue report)))) - -(defn monitor-tx-changes - "Monitor datomic transactions for events that are desired for later consumption - by clients wishing to process the same update(s) in ACeDB. - - `send-changes-fn` should be a functio accepting a map of changes to be sent. - e.g: via AWS SQS, or possibly email." - [tx-report-queue event-broadcaster & {:keys [include-agents] - :or {include-agents #{:agent/web}}}] - (let [peek-report #(.peek tx-report-queue)] - (loop [report (peek-report)] - (cond - (nil? report) (Thread/sleep 5000) - report (process-report event-broadcaster - include-agents - tx-report-queue - report)) - (recur (peek-report))))) - -(defn start-queue-monitor [conn event-broadcaster] - (let [tx-report-queue (d/tx-report-queue conn)] - (future (monitor-tx-changes tx-report-queue event-broadcaster)))) - -;; DISABLED. -;; (mount/defstate change-queue-monitor -;; :start (fn [] -;; (log/info "Starting change queue monitor") -;; (start-queue-monitor -;; wdb/conn -;; (-> {} wneb-s3/map->TxEventBroadcaster wneb/configure))) -;; :stop (fn [] -;; (log/info "Stopping change queue monitor") -;; (future-cancel change-queue-monitor) -;; (log/info "Change queue monitor stopped"))) diff --git a/src/wormbase/names/event_broadcast/aws_s3.clj b/src/wormbase/names/event_broadcast/aws_s3.clj deleted file mode 100644 index 190623be..00000000 --- a/src/wormbase/names/event_broadcast/aws_s3.clj +++ /dev/null @@ -1,34 +0,0 @@ -(ns wormbase.names.event-broadcast.aws-s3 - (:require - [clojure.string :as str] - [amazonica.aws.s3 :as s3] - [wormbase.names.event-broadcast.proto :as evb-proto]) - (:import - (java.io ByteArrayInputStream))) - -(defn derive-bucket-key [event] - (let [parts ["name-server" - "events" - (name (:provenance/what event)) - (str (:tx-id event) ".edn")]] - (str/join "/" parts))) - -(defn- bucket-name [tx-event-brodcaster] - (get-in tx-event-brodcaster [:config :bucket-name])) - -(defrecord TxEventBroadcaster [config] - evb-proto/TxEventBroadcaster - (message-persisted? [this changes] - (some? (s3/get-object-metadata - {:bucket-name (bucket-name this) - :key (derive-bucket-key changes)}))) - (send-message [this changes] - (let [data (.getBytes (pr-str changes) "UTF-8") - input-stream (ByteArrayInputStream. data)] - (s3/put-object :bucket-name (get-in this [:config :bucket-name]) - :key (derive-bucket-key changes) - :input-stream input-stream - :metadata {:content-length (count data)} - :return-values "ALL_OLD")))) - -(def make (partial evb-proto/make map->TxEventBroadcaster :aws-s3)) diff --git a/src/wormbase/names/event_broadcast/aws_sqs.clj b/src/wormbase/names/event_broadcast/aws_sqs.clj deleted file mode 100644 index ade4c03b..00000000 --- a/src/wormbase/names/event_broadcast/aws_sqs.clj +++ /dev/null @@ -1,23 +0,0 @@ -(ns wormbase.names.event-broadcast.aws-sqs - (:require - [amazonica.aws.sqs :as sqs] - [wormbase.names.event-broadcast.proto :as evb-proto])) - -(defn- sqs-queue [config] - (if-let [aq (-> config :queue-name sqs/find-queue)] - aq - (apply sqs/create-queue (-> config vec flatten)))) - -(defrecord TxEventBroadcaster [config] - evb-proto/TxEventBroadcaster - (configure [this] - (assoc-in this [:config :queue] (sqs-queue))) - (message-persisted? [this message-id] - (let [msgs (:messages (sqs/receive-message - :queue-url (get-in this [:config :queue]) - :delete false))] - (not-empty (filter #(= (:message-id %) message-id) msgs)))) - (send-message [this changes] - (sqs/send-message (get-in this [:config :queue]) changes))) - -(def make (partial evb-proto/make map->TxEventBroadcaster :aws-sqs)) diff --git a/src/wormbase/names/event_broadcast/gmail.clj b/src/wormbase/names/event_broadcast/gmail.clj deleted file mode 100644 index 94b2d11f..00000000 --- a/src/wormbase/names/event_broadcast/gmail.clj +++ /dev/null @@ -1,21 +0,0 @@ -(ns wormbase.names.event-broadcast.gmail - (:require - [postal.core :as postal] - [wormbase.names.event-broadcast.proto :as evb-proto])) - -(defn derive-message-subject [event] - (str (:event/type event))) - -(defrecord TxEventBroadcaster [config] - evb-proto/TxEventBroadcaster - (send-message [this changes] - (let [conn (:config this) - subject (derive-message-subject changes)] - (postal/send-message conn - {:from (:user conn) - :to (:group-list conn) - :subject subject})))) - - -(def make (partial evb-proto/make map->TxEventBroadcaster :gmail)) - diff --git a/src/wormbase/names/event_broadcast/proto.clj b/src/wormbase/names/event_broadcast/proto.clj deleted file mode 100644 index 4bc7d5ce..00000000 --- a/src/wormbase/names/event_broadcast/proto.clj +++ /dev/null @@ -1,13 +0,0 @@ -(ns wormbase.names.event-broadcast.proto - (:require [wormbase.util :as wu])) - - -(defprotocol TxEventBroadcaster - (message-persisted? [this message-id]) - (send-message [this changes]) - (configure [this])) - - -(defn make [ctor config-kw] - (let [config (get-in (wu/read-app-config) [:event-broadcast config-kw])] - (ctor {:config config}))) diff --git a/src/wormbase/names/gene.clj b/src/wormbase/names/gene.clj index d242b146..6899fdd9 100644 --- a/src/wormbase/names/gene.clj +++ b/src/wormbase/names/gene.clj @@ -404,12 +404,12 @@ :responses (wnu/response-map created {:schema {:created ::wsg/created}} bad-request {:schema ::wsv/error-response}) :handler (fn handle-new [request] - (let [new-gene (wne/creator :gene/id - (partial wnu/conform-data-drop-label ::wsg/new) - :event/new-gene - #(get-gene-info %1 %2 :fmt-output true) - validate-names)] - (new-gene request)))}}))) + (let [new-gene-handler (wne/new-entity-handler-creator :gene/id + (partial wnu/conform-data-drop-label ::wsg/new) + :event/new-gene + #(get-gene-info %1 %2 :fmt-output true) + validate-names)] + (new-gene-handler request)))}}))) (def item-resources (sweet/context "/gene/:identifier" [] diff --git a/src/wormbase/specs/entity.clj b/src/wormbase/specs/entity.clj index f202aec6..cda273dc 100644 --- a/src/wormbase/specs/entity.clj +++ b/src/wormbase/specs/entity.clj @@ -11,6 +11,20 @@ :dynamic true} *max-requestable-un-named* 1000000) +(s/def ::caltech-message (stc/spec {:spec string? + :swagger/example "Added 2023-11-21 22:06:46 WBVar03000001 tempVariation to obo_name_variation and obo_data_variation." + :description "A message received from a caltech API call."})) + +(s/def ::http-response-status-code (stc/spec {:spec (s/and int? + #(<= 100 %) + #(< % 600)) + :swagger/example 404 + :description "Any valid HTTP status code"})) + +(s/def ::http-response-body (stc/spec {:spec string? + :swagger/example "\n\n404 Not Found\n\n

Not Found

\n

The requested URL was not found on this server.

\n\n" + :description "A (complete) HTTP response body."})) + (s/def ::id-template (stc/spec {:spec (s/and string? #(str/starts-with? % "WB") @@ -29,13 +43,18 @@ :swagger/example "variation" :description "The name of the entity type."})) -(s/def ::new-schema (stc/spec {:spec (s/keys ::req-un [::id-template - ::entity-type - ::name-required?]) - :description "Parameters required to install a new entity schema." +(s/def ::message string?) + +(s/def ::new-type-response (stc/spec {:spec (s/keys :req-un [::message]) + :description "The response map returned by a successful entity-type creation operation."})) + +(s/def ::new-type (stc/spec {:spec (s/keys :req-un [::id-template + ::entity-type + ::name-required?]) + :description "Parameters required to install a new entity schema (aka entity type)." :swagger/example {:id-template "WBThing%d" :entity-type "thing" - :name-required true}})) + :name-required? true}})) (s/def ::named? (stc/spec {:spec sts/boolean? :swagger/example "true" @@ -137,9 +156,14 @@ (s/def ::created (stc/spec {:spec (s/keys :req-un [::id]) :description "A mapping describing a newly created entity."})) -(s/def ::message string?) +(s/def ::caltech-sync (stc/spec {:spec (s/keys :req-un [::http-response-status-code] + :opt-un [::caltech-message ::http-response-body]) + :description (str "A mapping describing the response received from " + "sending a new object to Caltech APIs (over HTTP(S)).")})) -(s/def ::schema-created (stc/spec (s/keys :req-un [::message]))) +(s/def ::created-response (stc/spec {:spec (s/keys :req-un [::created] + :opt-un [::caltech-sync]) + :description "The response map returned by a successful entity creation operation."})) (s/def ::names (stc/spec {:spec (s/coll-of (s/keys :req-un [::name]) :min-count 1) :description "A collection of entity names."})) diff --git a/test/integration/generic_entity_schema_test.clj b/test/integration/generic_entity_schema_test.clj index ca5a493b..bb0d3315 100644 --- a/test/integration/generic_entity_schema_test.clj +++ b/test/integration/generic_entity_schema_test.clj @@ -42,7 +42,7 @@ (let [data {:entity-type "strain" :id-template "WBStrain%0d" :generic true - :name-required true} + :name-required? true} response (new-entity-type {:data data :prov nil})] (t/is (ru-hp/created? response)) (let [db-after (d/db conn)