strapi-plugin-meilisearch 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/admin/src/Hooks/useAlert.js +17 -0
  2. package/admin/src/Hooks/useCollection.js +69 -56
  3. package/admin/src/Hooks/useCredential.js +24 -20
  4. package/admin/src/components/Initializer/index.js +27 -0
  5. package/admin/src/constants.js +34 -0
  6. package/admin/src/containers/Collection/CollectionColumn.js +34 -26
  7. package/admin/src/containers/Collection/CollectionTable.js +3 -5
  8. package/admin/src/containers/Collection/CollectionTableHeader.js +12 -6
  9. package/admin/src/containers/HomePage/index.js +8 -5
  10. package/admin/src/containers/PluginTabs/index.js +12 -6
  11. package/admin/src/containers/Settings/Credentials.js +22 -20
  12. package/admin/src/index.js +4 -2
  13. package/package.json +10 -10
  14. package/server/__tests__/configuration-validation.test.js +22 -22
  15. package/server/__tests__/configuration.test.js +1 -1
  16. package/server/__tests__/content-types.test.js +5 -5
  17. package/server/__tests__/meilisearch.test.js +4 -4
  18. package/server/bootstrap.js +45 -0
  19. package/server/configuration-validation.js +18 -18
  20. package/server/constants.js +13 -0
  21. package/server/controllers/reload.js +1 -2
  22. package/server/policies/isAdmin.js +1 -1
  23. package/server/routes/index.js +37 -7
  24. package/server/services/content-types/content-types.js +3 -3
  25. package/server/services/lifecycle/lifecycle.js +5 -5
  26. package/server/services/meilisearch/config.js +6 -6
  27. package/server/services/meilisearch/connector.js +24 -27
  28. package/server/services/store/credential.js +4 -7
  29. package/admin/src/containers/Collection/utils/getTrad.js +0 -5
@@ -14,8 +14,11 @@ export function useAlert() {
14
14
  message = 'Something occured in Meilisearch',
15
15
  link,
16
16
  blockTransition = true,
17
+ title,
17
18
  }) => {
18
19
  toggleNotification({
20
+ // optional
21
+ title,
19
22
  // required
20
23
  // type: 'info|success|warning',
21
24
  type,
@@ -32,8 +35,22 @@ export function useAlert() {
32
35
  onClose: () => localStorage.setItem('STRAPI_UPDATE_NOTIF', true),
33
36
  })
34
37
  }
38
+
39
+ const checkForbiddenError = ({ response }) => {
40
+ const status = response?.payload?.error?.status
41
+ if (status && status === 403) {
42
+ handleNotification({
43
+ title: 'Forbidden',
44
+ type: 'warning',
45
+ message: 'You do not have permission to do this action',
46
+ blockTransition: false,
47
+ })
48
+ }
49
+ }
50
+
35
51
  return {
36
52
  handleNotification,
53
+ checkForbiddenError,
37
54
  }
38
55
  }
39
56
 
@@ -16,7 +16,7 @@ export function useCollection() {
16
16
  const [reloadNeeded, setReloadNeeded] = useState(false)
17
17
  const [realTimeReports, setRealTimeReports] = useState(false)
18
18
 
19
- const { handleNotification } = useAlert()
19
+ const { handleNotification, checkForbiddenError } = useAlert()
20
20
 
21
21
  const refetchCollection = () =>
22
22
  setRefetchIndex(prevRefetchIndex => !prevRefetchIndex)
@@ -41,7 +41,7 @@ export function useCollection() {
41
41
  return collection
42
42
  })
43
43
  const reload = collections.find(
44
- col => col.reloadNeeded === 'Reload needed'
44
+ col => col.reloadNeeded === 'Reload needed',
45
45
  )
46
46
 
47
47
  const isIndexing = collections.find(col => col.isIndexing === true)
@@ -57,71 +57,84 @@ export function useCollection() {
57
57
  }
58
58
 
59
59
  const deleteCollection = async ({ contentType }) => {
60
- const { error } = await request(
61
- `/${pluginId}/content-type/${contentType}`,
62
- {
63
- method: 'DELETE',
60
+ try {
61
+ const { error } = await request(
62
+ `/${pluginId}/content-type/${contentType}`,
63
+ {
64
+ method: 'DELETE',
65
+ },
66
+ )
67
+ if (error) {
68
+ handleNotification({
69
+ type: 'warning',
70
+ message: error.message,
71
+ link: error.link,
72
+ })
73
+ } else {
74
+ refetchCollection()
75
+ handleNotification({
76
+ type: 'success',
77
+ message: 'Request to delete content-type is successful',
78
+ blockTransition: false,
79
+ })
64
80
  }
65
- )
66
- if (error) {
67
- handleNotification({
68
- type: 'warning',
69
- message: error.message,
70
- link: error.link,
71
- })
72
- } else {
73
- refetchCollection()
74
- handleNotification({
75
- type: 'success',
76
- message: 'Request to delete content-type is successful',
77
- blockTransition: false,
78
- })
81
+ } catch (error) {
82
+ checkForbiddenError(error)
79
83
  }
80
84
  }
81
85
 
82
86
  const addCollection = async ({ contentType }) => {
83
- const { error } = await request(`/${pluginId}/content-type`, {
84
- method: 'POST',
85
- body: {
86
- contentType,
87
- },
88
- })
89
- if (error) {
90
- handleNotification({
91
- type: 'warning',
92
- message: error.message,
93
- link: error.link,
94
- })
95
- } else {
96
- refetchCollection()
97
- handleNotification({
98
- type: 'success',
99
- message: 'Request to add a content-type is successful',
100
- blockTransition: false,
87
+ try {
88
+ const { error } = await request(`/${pluginId}/content-type`, {
89
+ method: 'POST',
90
+ body: {
91
+ contentType,
92
+ },
101
93
  })
94
+ if (error) {
95
+ handleNotification({
96
+ type: 'warning',
97
+ message: error.message,
98
+ link: error.link,
99
+ })
100
+ } else {
101
+ refetchCollection()
102
+ handleNotification({
103
+ type: 'success',
104
+ message: 'Request to add a content-type is successful',
105
+ blockTransition: false,
106
+ })
107
+ }
108
+ } catch (error) {
109
+ checkForbiddenError(error)
102
110
  }
103
111
  }
104
112
 
105
113
  const updateCollection = async ({ contentType }) => {
106
- const { error } = await request(`/${pluginId}/content-type`, {
107
- method: 'PUT',
108
- body: {
109
- contentType,
110
- },
111
- })
112
- if (error) {
113
- handleNotification({
114
- type: 'warning',
115
- message: error.message,
116
- link: error.link,
117
- })
118
- } else {
119
- refetchCollection()
120
- handleNotification({
121
- type: 'success',
122
- message: 'Request to update content-type is successful',
123
- blockTransition: false,
114
+ try {
115
+ const { error } = await request(`/${pluginId}/content-type`, {
116
+ method: 'PUT',
117
+ body: {
118
+ contentType,
119
+ },
124
120
  })
121
+
122
+ if (error) {
123
+ handleNotification({
124
+ type: 'warning',
125
+ message: error.message,
126
+ link: error.link,
127
+ })
128
+ } else {
129
+ refetchCollection()
130
+ handleNotification({
131
+ type: 'success',
132
+ message: 'Request to update content-type is successful',
133
+ blockTransition: false,
134
+ })
135
+ }
136
+ } catch (error) {
137
+ checkForbiddenError(error)
125
138
  }
126
139
  }
127
140
 
@@ -13,32 +13,36 @@ export function useCredential() {
13
13
  const [refetchIndex, setRefetchIndex] = useState(true)
14
14
  const [host, setHost] = useState('')
15
15
  const [apiKey, setApiKey] = useState('')
16
- const { handleNotification } = useAlert()
16
+ const { handleNotification, checkForbiddenError } = useAlert()
17
17
 
18
18
  const refetchCredentials = () =>
19
19
  setRefetchIndex(prevRefetchIndex => !prevRefetchIndex)
20
20
 
21
21
  const updateCredentials = async () => {
22
- const { error } = await request(`/${pluginId}/credential`, {
23
- method: 'POST',
24
- body: {
25
- apiKey: apiKey,
26
- host: host,
27
- },
28
- })
29
- if (error) {
30
- handleNotification({
31
- type: 'warning',
32
- message: error.message,
33
- link: error.link,
34
- })
35
- } else {
36
- refetchCredentials()
37
- handleNotification({
38
- type: 'success',
39
- message: 'Credentials sucessfully updated!',
40
- blockTransition: false,
22
+ try {
23
+ const { error } = await request(`/${pluginId}/credential`, {
24
+ method: 'POST',
25
+ body: {
26
+ apiKey: apiKey,
27
+ host: host,
28
+ },
41
29
  })
30
+ if (error) {
31
+ handleNotification({
32
+ type: 'warning',
33
+ message: error.message,
34
+ link: error.link,
35
+ })
36
+ } else {
37
+ refetchCredentials()
38
+ handleNotification({
39
+ type: 'success',
40
+ message: 'Credentials sucessfully updated!',
41
+ blockTransition: false,
42
+ })
43
+ }
44
+ } catch (error) {
45
+ checkForbiddenError(error)
42
46
  }
43
47
  }
44
48
 
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Initializer
4
+ *
5
+ */
6
+
7
+ import PropTypes from 'prop-types'
8
+ import { useEffect, useRef } from 'react'
9
+
10
+ import pluginId from '../../pluginId'
11
+
12
+ const Initializer = ({ setPlugin }) => {
13
+ const ref = useRef()
14
+ ref.current = setPlugin
15
+
16
+ useEffect(() => {
17
+ ref.current(pluginId)
18
+ }, [])
19
+
20
+ return null
21
+ }
22
+
23
+ Initializer.propTypes = {
24
+ setPlugin: PropTypes.func.isRequired,
25
+ }
26
+
27
+ export default Initializer
@@ -0,0 +1,34 @@
1
+ export const PERMISSIONS = {
2
+ // This permission regards the main component (App) and is used to tell
3
+ // If the plugin link should be displayed in the menu
4
+ // And also if the plugin is accessible. This use case is found when a user types the url of the
5
+ // plugin directly in the browser
6
+ main: [
7
+ { action: 'plugin::meilisearch.read', subject: null },
8
+ { action: 'plugin::meilisearch.collections.create', subject: null },
9
+ { action: 'plugin::meilisearch.collections.update', subject: null },
10
+ { action: 'plugin::meilisearch.collections.delete', subject: null },
11
+ { action: 'plugin::meilisearch.settings.edit', subject: null },
12
+ ],
13
+ collections: [
14
+ { action: 'plugin::meilisearch.read', subject: null },
15
+ { action: 'plugin::meilisearch.collections.create', subject: null },
16
+ { action: 'plugin::meilisearch.collections.update', subject: null },
17
+ { action: 'plugin::meilisearch.collections.delete', subject: null },
18
+ ],
19
+ settings: [
20
+ { action: 'plugin::meilisearch.read', subject: null },
21
+ { action: 'plugin::meilisearch.settings.edit', subject: null },
22
+ ],
23
+ read: [{ action: 'plugin::meilisearch.read', subject: null }],
24
+ create: [{ action: 'plugin::meilisearch.collections.create', subject: null }],
25
+ update: [{ action: 'plugin::meilisearch.collections.update', subject: null }],
26
+ delete: [{ action: 'plugin::meilisearch.collections.delete', subject: null }],
27
+ settingsEdit: [
28
+ { action: 'plugin::meilisearch.settings.edit', subject: null },
29
+ ],
30
+ createAndDelete: [
31
+ { action: 'plugin::meilisearch.collections.create', subject: null },
32
+ { action: 'plugin::meilisearch.collections.delete', subject: null },
33
+ ],
34
+ }
@@ -8,6 +8,8 @@ import {
8
8
  Tr,
9
9
  Typography,
10
10
  } from '@strapi/design-system'
11
+ import { CheckPermissions } from '@strapi/helper-plugin'
12
+ import { PERMISSIONS } from '../../constants'
11
13
 
12
14
  const CollectionColumn = ({
13
15
  entry,
@@ -17,17 +19,19 @@ const CollectionColumn = ({
17
19
  }) => {
18
20
  return (
19
21
  <Tr key={entry.contentType}>
20
- <Td>
21
- <BaseCheckbox
22
- aria-label={`Select ${entry.collection}`}
23
- onValueChange={() => {
24
- if (entry.indexed)
25
- deleteCollection({ contentType: entry.contentType })
26
- else addCollection({ contentType: entry.contentType })
27
- }}
28
- value={entry.indexed}
29
- />
30
- </Td>
22
+ <CheckPermissions permissions={PERMISSIONS.createAndDelete}>
23
+ <Td>
24
+ <BaseCheckbox
25
+ aria-label={`Select ${entry.collection}`}
26
+ onValueChange={() => {
27
+ if (entry.indexed)
28
+ deleteCollection({ contentType: entry.contentType })
29
+ else addCollection({ contentType: entry.contentType })
30
+ }}
31
+ value={entry.indexed}
32
+ />
33
+ </Td>
34
+ </CheckPermissions>
31
35
  {/* // Name */}
32
36
  <Td>
33
37
  <Typography textColor="neutral800">{entry.collection}</Typography>
@@ -58,21 +62,25 @@ const CollectionColumn = ({
58
62
  <Td>
59
63
  <Typography textColor="neutral800">{entry.reloadNeeded}</Typography>
60
64
  </Td>
61
- <Td>
62
- <Flex>
63
- <Box paddingLeft={1}>
64
- <Button
65
- onClick={() =>
66
- updateCollection({ contentType: entry.contentType })
67
- }
68
- size="S"
69
- variant="secondary"
70
- >
71
- Update
72
- </Button>
73
- </Box>
74
- </Flex>
75
- </Td>
65
+ <CheckPermissions permissions={PERMISSIONS.update}>
66
+ <Td>
67
+ <Flex>
68
+ <Box paddingLeft={1}>
69
+ {entry.indexed && (
70
+ <Button
71
+ onClick={() =>
72
+ updateCollection({ contentType: entry.contentType })
73
+ }
74
+ size="S"
75
+ variant="secondary"
76
+ >
77
+ Update
78
+ </Button>
79
+ )}
80
+ </Box>
81
+ </Flex>
82
+ </Td>
83
+ </CheckPermissions>
76
84
  </Tr>
77
85
  )
78
86
  }
@@ -15,10 +15,8 @@ const Collection = () => {
15
15
  reloadNeeded,
16
16
  refetchCollection,
17
17
  } = useCollection()
18
- const {
19
- lockAppWithAutoreload,
20
- unlockAppWithAutoreload,
21
- } = useAutoReloadOverlayBlocker()
18
+ const { lockAppWithAutoreload, unlockAppWithAutoreload } =
19
+ useAutoReloadOverlayBlocker()
22
20
  const [reload, setReload] = useState(false)
23
21
 
24
22
  const ROW_COUNT = 6
@@ -35,7 +33,7 @@ const Collection = () => {
35
33
  {
36
34
  method: 'GET',
37
35
  },
38
- true
36
+ true,
39
37
  )
40
38
  setReload(false)
41
39
  } catch (err) {
@@ -6,14 +6,18 @@ import {
6
6
  Typography,
7
7
  VisuallyHidden,
8
8
  } from '@strapi/design-system'
9
+ import { CheckPermissions } from '@strapi/helper-plugin'
10
+ import { PERMISSIONS } from '../../constants'
9
11
 
10
12
  const CollectionTableHeader = () => {
11
13
  return (
12
14
  <Thead>
13
15
  <Tr>
14
- <Th>
15
- <VisuallyHidden>INDEX</VisuallyHidden>
16
- </Th>
16
+ <CheckPermissions permissions={PERMISSIONS.createAndDelete}>
17
+ <Th>
18
+ <VisuallyHidden>INDEX</VisuallyHidden>
19
+ </Th>
20
+ </CheckPermissions>
17
21
  <Th>
18
22
  <Typography variant="sigma">NAME</Typography>
19
23
  </Th>
@@ -32,9 +36,11 @@ const CollectionTableHeader = () => {
32
36
  <Th>
33
37
  <Typography variant="sigma">HOOKS</Typography>
34
38
  </Th>
35
- <Th>
36
- <VisuallyHidden>Actions</VisuallyHidden>
37
- </Th>
39
+ <CheckPermissions permissions={PERMISSIONS.update}>
40
+ <Th>
41
+ <VisuallyHidden>Actions</VisuallyHidden>
42
+ </Th>
43
+ </CheckPermissions>
38
44
  </Tr>
39
45
  </Thead>
40
46
  )
@@ -3,17 +3,20 @@
3
3
  * HomePage
4
4
  *
5
5
  */
6
-
6
+ import { CheckPagePermissions } from '@strapi/helper-plugin'
7
7
  import React, { memo } from 'react'
8
8
  import PluginTabs from '../PluginTabs'
9
9
  import PluginHeader from '../PluginHeader'
10
+ import { PERMISSIONS } from '../../constants'
10
11
 
11
12
  const HomePage = () => {
12
13
  return (
13
- <div>
14
- <PluginHeader />
15
- <PluginTabs />
16
- </div>
14
+ <CheckPagePermissions permissions={PERMISSIONS.main}>
15
+ <div>
16
+ <PluginHeader />
17
+ <PluginTabs />
18
+ </div>
19
+ </CheckPagePermissions>
17
20
  )
18
21
  }
19
22
 
@@ -9,6 +9,8 @@ import {
9
9
  } from '@strapi/design-system'
10
10
  import { CollectionTable } from '../Collection'
11
11
  import { Settings } from '../Settings'
12
+ import { CheckPermissions } from '@strapi/helper-plugin'
13
+ import { PERMISSIONS } from '../../constants'
12
14
 
13
15
  const PluginTabs = () => {
14
16
  return (
@@ -20,14 +22,18 @@ const PluginTabs = () => {
20
22
  </Tabs>
21
23
  <TabPanels>
22
24
  <TabPanel>
23
- <Box color="neutral800" padding={4} background="neutral0">
24
- <CollectionTable />
25
- </Box>
25
+ <CheckPermissions permissions={PERMISSIONS.collections}>
26
+ <Box color="neutral800" padding={4} background="neutral0">
27
+ <CollectionTable />
28
+ </Box>
29
+ </CheckPermissions>
26
30
  </TabPanel>
27
31
  <TabPanel>
28
- <Box color="neutral800" padding={4} background="neutral0">
29
- <Settings />
30
- </Box>
32
+ <CheckPermissions permissions={PERMISSIONS.settings}>
33
+ <Box color="neutral800" padding={4} background="neutral0">
34
+ <Settings />
35
+ </Box>
36
+ </CheckPermissions>
31
37
  </TabPanel>
32
38
  </TabPanels>
33
39
  </TabGroup>
@@ -1,17 +1,12 @@
1
1
  import React, { memo } from 'react'
2
2
  import { Box, Button, TextInput, Typography } from '@strapi/design-system'
3
3
  import { useCredential } from '../../Hooks/useCredential'
4
+ import { CheckPermissions } from '@strapi/helper-plugin'
5
+ import { PERMISSIONS } from '../../constants'
4
6
 
5
7
  const Credentials = () => {
6
- const {
7
- host,
8
- apiKey,
9
- credentials,
10
- setHost,
11
- setApiKey,
12
- updateCredentials,
13
- } = useCredential()
14
-
8
+ const { host, apiKey, credentials, setHost, setApiKey, updateCredentials } =
9
+ useCredential()
15
10
  return (
16
11
  <Box>
17
12
  <Box padding={2}>
@@ -42,23 +37,30 @@ const Credentials = () => {
42
37
  <Typography variant="pi" style={{ color: 'red' }}>
43
38
  Do not use this API key on your front-end as it has too much rights.
44
39
  Instead, use the public key available using{' '}
45
- <a href="https://www.meilisearch.com/docs/reference/api/keys#get-keys">
40
+ <a
41
+ href="https://www.meilisearch.com/docs/reference/api/keys#get-keys"
42
+ target="_blank"
43
+ rel="noreferrer"
44
+ >
46
45
  the key route
47
46
  </a>
48
47
  .
49
48
  </Typography>
50
49
  </Box>
50
+
51
51
  <Box paddingTop={2} paddingLeft={2} paddingRight={2} paddingBottom={2}>
52
- <Button
53
- variant="secondary"
54
- onClick={() => updateCredentials()}
55
- disabled={
56
- credentials.ApiKeyIsFromConfigFile &&
57
- credentials.HostIsFromConfigFile
58
- }
59
- >
60
- Save
61
- </Button>
52
+ <CheckPermissions permissions={PERMISSIONS.settingsEdit}>
53
+ <Button
54
+ variant="secondary"
55
+ onClick={() => updateCredentials()}
56
+ disabled={
57
+ credentials.ApiKeyIsFromConfigFile &&
58
+ credentials.HostIsFromConfigFile
59
+ }
60
+ >
61
+ Save
62
+ </Button>
63
+ </CheckPermissions>
62
64
  </Box>
63
65
  </Box>
64
66
  )
@@ -1,6 +1,8 @@
1
1
  import pluginPkg from '../../package.json'
2
2
  import pluginId from './pluginId'
3
3
  import PluginIcon from './components/PluginIcon'
4
+ import Initializer from './components/Initializer'
5
+ import { PERMISSIONS } from './constants'
4
6
 
5
7
  const name = pluginPkg.strapi.name
6
8
 
@@ -8,9 +10,9 @@ export default {
8
10
  register(app) {
9
11
  app.registerPlugin({
10
12
  id: pluginId,
13
+ initializer: Initializer,
11
14
  isReady: true,
12
15
  name,
13
- description: 'TEST',
14
16
  })
15
17
 
16
18
  app.addMenuLink({
@@ -27,7 +29,7 @@ export default {
27
29
 
28
30
  return component
29
31
  },
30
- permissions: [],
32
+ permissions: PERMISSIONS.main,
31
33
  })
32
34
  },
33
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-meilisearch",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Synchronise and search in your Strapi content-types with Meilisearch",
5
5
  "scripts": {
6
6
  "playground:dev": "yarn --cwd ./playground && yarn --cwd ./playground dev",
@@ -25,10 +25,10 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@strapi/utils": "^4.14.4",
28
+ "@strapi/utils": "^4.21.1",
29
29
  "@strapi/icons": "^1.12.2",
30
30
  "@strapi/design-system": "^1.13.2",
31
- "meilisearch": "^0.37.0"
31
+ "meilisearch": "^0.38.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@strapi/strapi": "^4.0.0"
@@ -61,18 +61,18 @@
61
61
  },
62
62
  "homepage": "https://github.com/meilisearch/strapi-plugin-meilisearch#readme",
63
63
  "devDependencies": {
64
- "@types/jest": "^27.4.1",
65
- "concurrently": "^6.2.0",
64
+ "@types/jest": "^29.5.12",
65
+ "concurrently": "^8.2.2",
66
66
  "cypress": "^7.3.0",
67
67
  "eslint": "^8.2.0",
68
- "eslint-config-prettier": "^8.3.0",
68
+ "eslint-config-prettier": "^9.1.0",
69
69
  "eslint-plugin-cypress": "^2.12.1",
70
70
  "eslint-plugin-import": "^2.25.3",
71
71
  "eslint-plugin-node": "^11.1.0",
72
- "eslint-plugin-prettier": "^4.0.0",
73
- "eslint-plugin-promise": "^4.3.1",
72
+ "eslint-plugin-prettier": "^5.1.3",
73
+ "eslint-plugin-promise": "^6.1.1",
74
74
  "eslint-plugin-react": "^7.27.0",
75
- "jest": "^27.4.3",
76
- "prettier": "2.2.1"
75
+ "jest": "^29.7.0",
76
+ "prettier": "3.2.5"
77
77
  }
78
78
  }