Skip to content

Commit

Permalink
feat(keystone): DOMA-10804 field refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
vovaaxeapolla committed Jan 13, 2025
1 parent e5b2f1c commit 1f08a15
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 149 deletions.
4 changes: 2 additions & 2 deletions packages/keystone/KSv5v6/v5/registerSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const {
SignedDecimal,
Text,
EncryptedText,
LargeText,
CloudStorageText,
} = require('../../fields')
const { HiddenRelationship } = require('../../plugins/utils/HiddenRelationship')
const { AuthedRelationship, Relationship } = require('../../plugins/utils/Relationship')
Expand Down Expand Up @@ -86,7 +86,7 @@ function convertStringToTypes (schema) {
SignedDecimal,
Text,
EncryptedText,
LargeText,
CloudStorageText,
}
const allTypesForPrint = Object.keys(mapping).map(item => `"${item}"`).join(', ')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
const { Text } = require('@keystonejs/fields')

class LargeTextImplementation extends Text.implementation {
class CloudStorageTextImplementation extends Text.implementation {
constructor (path, {
adapter,
}) {
super(...arguments)
this.fileAdapter = adapter
this.isMultiline = true
}
}

module.exports = {
LargeTextImplementation,
CloudStorageTextImplementation,
}
49 changes: 49 additions & 0 deletions packages/keystone/fields/CloudStorageText/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# CloudStorageText

The `CloudStorageText` field simplifies the storage of large text data by abstracting the implementation of cloud storage usage.

## Basic Usage

Simply add the `CloudStorageText` field type and a file adapter, and your text data will be stored in cloud storage. This helps avoid storing large datasets directly in the database. You can use it just like a common `Text` field in your code.

```js
const fileAdapter = new FileAdapter('LOG_FILE_NAME')

keystone.createList('Log', {
fields: {
xmlLog: {
type: 'CloudStorageText',
adapter: fileAdapter,
},
},
});
```

# GraphQL

The `CloudStorageText` field behaves like a standard string (`Text` field). During create/update operations, the input value is saved to cloud storage, and only a reference to the saved file is stored in the database. For read operations, the saved file is downloaded from cloud storage, and its contents are provided as a string.

# Storage

The text value is stored as a file in the cloud, while the database only holds a reference (link) to the source file.

# Configuration

You will need to configure a file adapter to work with cloud storage. Example:

```js
const fileAdapter = new FileAdapter('LOG_FILE_NAME')
```

or

```js
const fileAdapter = new FileAdapter('LOG_FILE_NAME', false, false, { bucket: 'BUCKET_FOR_LOGS'})
```

Ensure your cloud storage configuration is properly set up and accessible for file storage.

# Notes

- The `CloudStorageText` field is ideal for storing large logs, parsed values, or other text data that may exceed the typical size limits of database fields.
- The text is stored as a file in cloud storage, reducing the load on the database while still providing easy access to the data.
84 changes: 84 additions & 0 deletions packages/keystone/fields/CloudStorageText/adapters/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const { Text } = require('@keystonejs/fields')
const cuid = require('cuid')
const isNil = require('lodash/isNil')

const { bufferToStream, readFileFromStream, getObjectStream } = require('@open-condo/keystone/file')

const CommonInterface = superclass => class extends superclass {

constructor () {
super(...arguments)
if (!this.config.adapter) {
throw new Error('CloudStorageText field cannot be used without a file adapter')
}
this.fileAdapter = this.config.adapter
}

setupHooks ({ addPreSaveHook, addPostReadHook }) {
addPreSaveHook(async (item) => {
if (this._isFieldDefined(item)) {
item[this.path] = await this._saveFileToAdapter(item[this.path])
}
return item
})

addPostReadHook(async (item) => {
if (item[this.path]) {
item[this.path] = await this._readFileFromAdapter(item[this.path])
}
return item
})
}

_isFieldDefined (item) {
return item && !isNil(item[this.path])
}

async _saveFileToAdapter (content) {
const stream = bufferToStream(content)
const originalFilename = this._generateFilename()
const mimetype = 'text/plain'
const encoding = 'utf8'

const { id, filename, _meta } = await this.fileAdapter.save({
stream,
filename: originalFilename,
mimetype,
encoding,
id: cuid(),
})

return {
id,
filename,
originalFilename,
mimetype,
encoding,
_meta,
}
}

async _readFileFromAdapter (fileMetadata) {
const fileStream = await getObjectStream(fileMetadata, this.fileAdapter)
const fileContent = await readFileFromStream(fileStream)
return fileContent.toString()
}

_generateFilename () {
return `${new Date().toISOString()}`
}

addToTableSchema (table) {
table.jsonb(this.path)
}
}

class CloudStorageTextKnexFieldAdapter extends CommonInterface(Text.adapters.knex) {}
class CloudStorageTextMongooseFieldAdapter extends CommonInterface(Text.adapters.mongoose) {}
class CloudStorageTextPrismaFieldAdapter extends CommonInterface(Text.adapters.prisma) {}

module.exports = {
mongoose: CloudStorageTextKnexFieldAdapter,
knex: CloudStorageTextMongooseFieldAdapter,
prisma: CloudStorageTextPrismaFieldAdapter,
}
14 changes: 14 additions & 0 deletions packages/keystone/fields/CloudStorageText/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { Text } = require('@keystonejs/fields')

const CloudStorageTextAdapters = require('./adapters')
const { CloudStorageTextImplementation } = require('./Implementation')

module.exports = {
type: 'CloudStorageText',
implementation: CloudStorageTextImplementation,
views: {
...Text.views,
Controller: require.resolve('./views/Controller'),
},
adapters: CloudStorageTextAdapters,
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import FieldController from '@keystonejs/fields/Controller'

export default class LargeTextController extends FieldController {
export default class CloudStorageTextController extends FieldController {
getFilterTypes = () => []
}
49 changes: 0 additions & 49 deletions packages/keystone/fields/LargeText/README.md

This file was deleted.

76 changes: 0 additions & 76 deletions packages/keystone/fields/LargeText/adapters/index.js

This file was deleted.

14 changes: 0 additions & 14 deletions packages/keystone/fields/LargeText/index.js

This file was deleted.

4 changes: 2 additions & 2 deletions packages/keystone/fields/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const DateInterval = require('./DateInterval')
const EncryptedText = require('./EncryptedText')
const FileWithUTF8Name = require('./FileWithUTF8Name')
const Json = require('./Json')
const LargeText = require('./LargeText')
const CloudStorageText = require('./CloudStorageText')

Check failure on line 7 in packages/keystone/fields/index.js

View workflow job for this annotation

GitHub Actions / Lint source code

`./CloudStorageText` import should occur before import of `./DateInterval`
const LocalizedText = require('./LocalizedText')
const Options = require('./Options')
const Select = require('./Select')
Expand All @@ -25,5 +25,5 @@ module.exports = {
FileWithUTF8Name,
Text,
EncryptedText,
LargeText,
CloudStorageText,
}
22 changes: 19 additions & 3 deletions packages/keystone/fileAdapter/fileAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ class LocalFilesMiddleware {
}
}

//Needs to thing how to make configurable file adapter
class FileAdapter {
constructor (folder, isPublic = false, saveFileName = false, customConfig = {}) {
const type = conf.FILE_FIELD_ADAPTER || DEFAULT_FILE_ADAPTER
Expand Down Expand Up @@ -118,11 +117,28 @@ class FileAdapter {
's3Options.access_key_id',
's3Options.secret_access_key',
])
config = { ...config, ...customConfig }

if (!config) {
return null
}
return new SberCloudFileAdapter({ ...config, folder: this.folder, isPublic: this.isPublic, saveFileName: this.saveFileName })

config = { ...config, ...customConfig }

if (!this.isConfigValid(config, [
'bucket',
's3Options.server',
's3Options.access_key_id',
's3Options.secret_access_key',
])) {
return null
}

return new SberCloudFileAdapter({
...config,
folder: this.folder,
isPublic: this.isPublic,
saveFileName: this.saveFileName,
})
}

// TODO(pahaz): DOMA-1569 it's better to create just a function. But we already use FileAdapter in many places. I just want to save a backward compatibility
Expand Down

0 comments on commit 1f08a15

Please sign in to comment.