Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add documentsFormatter to plugin config #9

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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