Skip to content

Commit

Permalink
Merge pull request #9 from oramasearch/feature/orm-1817
Browse files Browse the repository at this point in the history
Feat: Add documentsFormatter to plugin config
  • Loading branch information
raiindev authored Nov 26, 2024
2 parents fe9e98b + 76a5709 commit d27deda
Show file tree
Hide file tree
Showing 15 changed files with 675 additions and 145 deletions.
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,97 @@ When the **Scheduled job** is executed, it checks the collection status, to avoi
already in sync. You can always trigger a new deployment manually.

<img src="https://raw.githubusercontent.com/askorama/orama-plugin-strapi/main/misc/assets/deploy.gif" alt="Manual deploy" width="600" />

---
## Advanced usage

### Documents transformation
The scope of the transformation is to modify the document before it is sent to the Orama Cloud API. This can be useful to add, remove or modify fields in the document.
A common use case is to change how a collection is handled (array of objects) to a flat structure [this is not supported by Orama Cloud].
Here is an example of how to transform a collection of objects to a flat structure:

#### Pre-requisites
- An Orama Cloud index.
- A Strapi collection already created, with relations.

Example document:
```json
{
"id": 1,
"owner": "John",
"cars": [
{
"brand": "Toyota",
"model": "Corolla"
},
{
"brand": "Ford",
"model": "Focus"
}
]
}
```
You can insert your transformer function directly inside the plugin configuration under `config/plugins.js` file:

```js
module.exports = ({ env }) => ({
"orama-cloud": {
config: {
privateApiKey: env("ORAMA_PRIVATE_API_KEY"),
collectionSettings: {
your_collection_index_id: {
/* Mandatory */
schema: {
id: { type: "integer" },
owner: { type: "string" },
cars: {
brands: { type: "string" },
models: { type: "string" },
},
},
/* Mandatory */
transformer: entry => {
return {
...entry,
owner: "Overriding owner",
cars: {
source: entry.cars,
...entry.cars.reduce(car => {
acc.brands.push(car.brand);
acc.models.push(car.model);
return acc;
}, {
brands: [],
models: [],
}),
},
}
},
}
}
},
},
})
```

In this way your cars will be transformed to:
```json
{
"id": 1,
"owner": "Overriding owner",
"cars": {
"brands": ["Toyota", "Ford"],
"models": ["Corolla", "Focus"]
}
}
```
And make you car brands and models searchable.

:warning: Both schema and transformer are mandatory.

:warning: The transformer function must return an object with the same schema as the one declared.

:warning: All the properties not declared in it will be included in the document, but ignored while searching.


For more information about the plugin, please visit the [Orama Cloud documentation](https://docs.orama.com/cloud/data-sources/native-integrations/strapi).
1 change: 1 addition & 0 deletions admin/src/components/RelationsSelect/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const RelationsSelect = ({ onChange, collectionRelations, relations = [] }) => (
disabled={relations.length === 0}
onChange={onChange}
value={collectionRelations}
withTags
>
{relations.map((relation, i) => (
<MultiSelectOption key={relation.value + i} value={relation.value}>
Expand Down
211 changes: 156 additions & 55 deletions admin/src/components/SchemaMapper/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,63 @@
import React from 'react'
import { Box, Checkbox, Flex, Switch, Table, Thead, Tbody, Tr, Th, Td, Typography } from '@strapi/design-system'
import { getSchemaFromAttributes, getSelectedAttributesFromSchema } from '../../../../utils/schema'
import {
Box,
Button,
Checkbox,
Flex,
Switch,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Typography,
Tooltip,
Status
} from '@strapi/design-system'
import WarningIcon from '../WarningIcon'
import { getSelectedPropsFromObj, getSelectedAttributesFromSchema } from '../../../../utils'

const isCollection = (value) => Array.isArray(value) && value.length > 0 && typeof value[0] === 'object'

const handleObjectField = (acc, fieldKey, fieldValue, relations) => {
if (relations.includes(fieldKey)) {
Object.keys(fieldValue).forEach((key) =>
acc.push({
field: `${fieldKey}.${key}`,
searchable: true
})
)
}
}

const handleCollectionField = (acc, fieldKey, fieldValue, relations) => {
if (relations.includes(fieldKey)) {
acc.push({
field: fieldKey,
searchable: false
})
}
}

const generateSelectableAttributesFromSchema = ({ schema, relations }) => {
const handlers = {
object: handleObjectField,
collection: handleCollectionField
}

return Object.entries(schema).reduce((acc, [fieldKey, fieldValue]) => {
if (typeof fieldValue === 'object') {
if (relations.includes(fieldKey)) {
Object.keys(fieldValue).forEach((key) => acc.push(`${fieldKey}.${key}`))
}
} else {
acc.push(fieldKey)
const fieldType = fieldValue === 'collection' ? 'collection' : typeof fieldValue

if (fieldType in handlers) {
handlers[fieldType](acc, fieldKey, fieldValue, relations)
} else if (!isCollection(fieldValue)) {
acc.push({
field: fieldKey,
searchable: true
})
}

return acc
}, [])
}
Expand All @@ -29,9 +76,9 @@ const SchemaMapper = ({ collection, contentTypeSchema, onSchemaChange }) => {
})

React.useEffect(() => {
const schema = getSchemaFromAttributes({
attributes: selectedAttributes,
schema: contentTypeSchema
const schema = getSelectedPropsFromObj({
props: selectedAttributes,
obj: contentTypeSchema
})

onSchemaChange({ schema, searchableAttributes })
Expand Down Expand Up @@ -73,10 +120,14 @@ const SchemaMapper = ({ collection, contentTypeSchema, onSchemaChange }) => {
setSelectedAttributes([])
setSearchableAttributes([])
} else {
setSelectedAttributes(schemaAttributes)
setSelectedAttributes(schemaAttributes.map((field) => field.field))
}
}

const handleDocumentationRedirect = () => {
window.open('https://docs.orama.com/cloud/data-sources/native-integrations/strapi', '_blank', 'noopener')
}

return (
<Box marginBottom={2} width="100%">
<Typography variant="beta" fontWeight="bold">
Expand All @@ -88,50 +139,100 @@ const SchemaMapper = ({ collection, contentTypeSchema, onSchemaChange }) => {
</Typography>
</Flex>
<Box>
<Table colCount={3} rowCount={schemaAttributes.length}>
<Thead>
<Tr>
<Th>
<Checkbox
aria-label="Select all entries"
checked={selectedAttributes.length === schemaAttributes.length}
onChange={() => selectAllAttributes()}
/>
</Th>
<Th style={{ minWidth: '300px' }}>
<Typography variant="sigma">Attribute</Typography>
</Th>
<Th>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%'
}}
>
<Typography variant="sigma">Searchable</Typography>
</div>
</Th>
</Tr>
</Thead>
<Tbody>
{schemaAttributes.map((field) => (
<Tr key={field}>
<Td>
<Checkbox checked={isChecked(field)} onChange={() => handleCheck(field)} />
</Td>
<Td onClick={() => handleCheck(field)} style={{ cursor: 'pointer' }}>
<Typography textColor="neutral800">{field}</Typography>
</Td>
<Td>
<Flex justifyContent="flex-end">
<Switch selected={isSearchableSelected(field)} onChange={() => handleSearchable(field)} />
</Flex>
</Td>
{/*TODO: style this*/}
{collection.hasSettings && (
<Typography variant="omega">
This is handled by the Orama Cloud plugin settings, under{' '}
<code
style={{
color: 'orange'
}}
>
config/plugins.js
</code>{' '}
directory.
</Typography>
)}
{!collection.hasSettings && (
<Table colCount={3} rowCount={schemaAttributes.length}>
<Thead>
<Tr>
<Th>
<Checkbox
aria-label="Select all entries"
checked={selectedAttributes.length === schemaAttributes.length}
onChange={() => selectAllAttributes()}
/>
</Th>
<Th style={{ minWidth: '300px' }}>
<Typography variant="sigma">Attribute</Typography>
</Th>
<Th>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%'
}}
>
<Typography variant="sigma">Searchable</Typography>
</div>
</Th>
</Tr>
))}
</Tbody>
</Table>
</Thead>
<Tbody>
{schemaAttributes.map(({ field, searchable }) => (
<Tr key={field}>
<Td>
<Checkbox checked={isChecked(field)} onChange={() => handleCheck(field)} />
</Td>
<Td
onClick={() => onCheck(field)}
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
>
<Typography textColor="neutral800">{field}</Typography>
{!searchable && (
<>
<Tooltip
position="right"
label="You need to transform this attribute's data. Click for more info."
>
<Status
variant="primary"
size="S"
showBullet={false}
style={{ marginLeft: 10 }}
onClick={handleDocumentationRedirect}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
columnGap: '5px'
}}
>
<WarningIcon size={12} fill="#ddaa00" />
<Typography variant="pi">Action required</Typography>
</div>
</Status>
</Tooltip>
</>
)}
</Td>
<Td>
<Flex justifyContent="flex-end">
{searchable && (
<Switch selected={isSearchableSelected(field)} onChange={() => handleSearchable(field)} />
)}
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
</Box>
</Box>
)
Expand Down
7 changes: 7 additions & 0 deletions admin/src/components/WarningIcon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react'

export default ({ size = 24, fill, ...rest }) => (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} fill={fill} {...rest} viewBox="0 0 256 256">
<path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"></path>
</svg>
)
Loading

0 comments on commit d27deda

Please sign in to comment.