chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@botpresshub/synchronizer",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit",
|
||||
"build": "bp add -y && bp build",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpresshub/creatable": "workspace:*",
|
||||
"@botpresshub/deletable": "workspace:*",
|
||||
"@botpresshub/listable": "workspace:*",
|
||||
"@botpresshub/updatable": "workspace:*",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/semver": "^7.3.11",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"listable": "../../interfaces/listable",
|
||||
"creatable": "../../interfaces/creatable",
|
||||
"updatable": "../../interfaces/updatable",
|
||||
"deletable": "../../interfaces/deletable"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import deletable from './bp_modules/deletable'
|
||||
import listable from './bp_modules/listable'
|
||||
|
||||
const itemSchema = listable.definition.entities.item.schema
|
||||
|
||||
export default new sdk.PluginDefinition({
|
||||
name: 'synchronizer',
|
||||
version: '1.0.0',
|
||||
configuration: {
|
||||
schema: sdk.z.object({
|
||||
tableName: sdk.z.string().title('Table Name').describe('The name of the table to store items'),
|
||||
}),
|
||||
},
|
||||
actions: {
|
||||
synchronize: {
|
||||
title: 'Synchronize',
|
||||
description: 'Manually synchronize a page of items without waiting for the cron job',
|
||||
input: { schema: sdk.z.object({}) },
|
||||
output: { schema: sdk.z.object({ itemsLeft: sdk.z.boolean() }) },
|
||||
},
|
||||
clear: {
|
||||
title: 'Clear',
|
||||
description: 'Clear the table',
|
||||
input: { schema: sdk.z.object({}) },
|
||||
output: { schema: sdk.z.object({}) },
|
||||
},
|
||||
},
|
||||
states: {
|
||||
table: {
|
||||
type: 'bot',
|
||||
schema: sdk.z.object({
|
||||
tableCreated: sdk.z.boolean().optional().title('Table Created').describe('Whether the table has been created'),
|
||||
}),
|
||||
},
|
||||
job: {
|
||||
type: 'bot',
|
||||
schema: sdk.z.object({
|
||||
nextToken: sdk.z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Next Token')
|
||||
.describe('The token to use to get the next page of items'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
events: {
|
||||
listItems: {
|
||||
schema: sdk.z.object({}),
|
||||
},
|
||||
rowInserted: {
|
||||
schema: sdk.z.object({ row: itemSchema }),
|
||||
},
|
||||
rowUpdated: {
|
||||
schema: sdk.z.object({ row: itemSchema }),
|
||||
},
|
||||
rowDeleted: {
|
||||
schema: sdk.z.object({ row: itemSchema.pick({ id: true }) }),
|
||||
},
|
||||
},
|
||||
recurringEvents: {
|
||||
runListItem: {
|
||||
type: 'listItems',
|
||||
payload: {},
|
||||
schedule: {
|
||||
cron: '* * * * *', // every minute
|
||||
},
|
||||
},
|
||||
},
|
||||
interfaces: {
|
||||
listable: sdk.version.allWithinMajorOf(listable),
|
||||
deletable: sdk.version.allWithinMajorOf(deletable),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
export class SynchronizerError extends Error {
|
||||
public constructor(message: string, inner: Error) {
|
||||
const fullMessage = `${message}: ${inner.message}`
|
||||
super(fullMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const _toError = (thrown: unknown): Error => (thrown instanceof Error ? thrown : new Error(String(thrown)))
|
||||
export const mapError =
|
||||
(msg: string) =>
|
||||
(thrown: unknown): never => {
|
||||
throw new SynchronizerError(msg, _toError(thrown))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import * as error from './error'
|
||||
import * as table from './table'
|
||||
import * as vanilla from './vanilla-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const synchronize = async (props: bp.EventHandlerProps | bp.ActionHandlerProps): Promise<{ nextToken?: string }> => {
|
||||
const { state } = await props.client
|
||||
.getOrSetState({
|
||||
type: 'bot',
|
||||
id: props.ctx.botId,
|
||||
name: 'job',
|
||||
payload: { nextToken: undefined },
|
||||
})
|
||||
.catch(error.mapError('Failed to get state "job"'))
|
||||
|
||||
const { nextToken } = state.payload
|
||||
props.logger.info('Synchronizing...', nextToken)
|
||||
|
||||
const { client, configuration, actions } = props
|
||||
|
||||
const nextPage = await actions.listable.list({ nextToken }).catch(error.mapError('Failed to list items'))
|
||||
|
||||
await table.createTableIfNotExist(props, nextPage.items[0]).catch(error.mapError('Failed to create table'))
|
||||
|
||||
await vanilla
|
||||
.clientFrom(client)
|
||||
.upsertTableRows({
|
||||
table: configuration.tableName,
|
||||
rows: nextPage.items.map(table.escapeObject),
|
||||
keyColumn: table.PRIMARY_KEY,
|
||||
})
|
||||
.catch(error.mapError('Failed to upsert table rows'))
|
||||
|
||||
await props.client
|
||||
.setState({
|
||||
type: 'bot',
|
||||
id: props.ctx.botId,
|
||||
name: 'job',
|
||||
payload: { nextToken: nextPage.meta.nextToken },
|
||||
})
|
||||
.catch(error.mapError('Failed to set state "job"'))
|
||||
|
||||
return { nextToken: nextPage.meta.nextToken }
|
||||
}
|
||||
|
||||
const plugin = new bp.Plugin({
|
||||
actions: {
|
||||
synchronize: async (props) => {
|
||||
const { nextToken } = await synchronize(props)
|
||||
return { itemsLeft: !!nextToken }
|
||||
},
|
||||
clear: async (props) => {
|
||||
await table.deleteTableIfExist(props)
|
||||
return {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
plugin.on.event('listItems', async (props) => {
|
||||
const { event, logger } = props
|
||||
logger.info(`### Event "${event.type}"`, event.payload)
|
||||
await synchronize(props)
|
||||
})
|
||||
|
||||
plugin.on.event('rowDeleted', async (props) => {
|
||||
const { actions, event, logger } = props
|
||||
logger.info(`### Event "${event.type}"`, event.payload)
|
||||
await actions.deletable.delete({ id: props.event.payload.row.id })
|
||||
})
|
||||
|
||||
plugin.on.event('deletable:deleted', async (props) => {
|
||||
const { client, configuration, event, logger } = props
|
||||
logger.info(`### Event "${event.type}"`, event.payload)
|
||||
await vanilla.clientFrom(client).deleteTableRows({
|
||||
table: configuration.tableName,
|
||||
filter: { [table.PRIMARY_KEY]: { $eq: props.event.payload.id } },
|
||||
})
|
||||
})
|
||||
|
||||
export default plugin
|
||||
@@ -0,0 +1,93 @@
|
||||
import _ from 'lodash'
|
||||
import * as error from './error'
|
||||
import * as vanilla from './vanilla-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const TABLE_RESERVED_KEYWORDS = ['id', 'createdAt', 'updatedAt']
|
||||
|
||||
export const PRIMARY_KEY = '_id'
|
||||
|
||||
export const createTableIfNotExist = async (
|
||||
props: bp.EventHandlerProps | bp.ActionHandlerProps,
|
||||
item: bp.interfaces.listable.entities.item.Item | undefined
|
||||
) => {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
const { state } = await props.client
|
||||
.getOrSetState({
|
||||
type: 'bot',
|
||||
id: props.ctx.botId,
|
||||
name: 'table',
|
||||
payload: { tableCreated: false },
|
||||
})
|
||||
.catch(error.mapError('Failed to get state "table"'))
|
||||
|
||||
const { tableCreated } = state.payload
|
||||
if (tableCreated) {
|
||||
props.logger.debug(`Table "${props.configuration.tableName}" already exists`)
|
||||
return
|
||||
}
|
||||
|
||||
const client = vanilla.clientFrom(props.client)
|
||||
const schema = escapeObject(item)
|
||||
|
||||
props.logger.info(`Creating table "${props.configuration.tableName}" with schema ${JSON.stringify(schema)}`)
|
||||
await client
|
||||
.createTable({
|
||||
name: props.configuration.tableName,
|
||||
schema,
|
||||
})
|
||||
.catch(error.mapError('Failed to create table'))
|
||||
|
||||
await props.client
|
||||
.setState({ type: 'bot', id: props.ctx.botId, name: 'table', payload: { tableCreated: true } })
|
||||
.catch(error.mapError('Failed to set state "table"'))
|
||||
}
|
||||
|
||||
export const deleteTableIfExist = async (props: bp.EventHandlerProps | bp.ActionHandlerProps) => {
|
||||
const { state } = await props.client.getOrSetState({
|
||||
type: 'bot',
|
||||
id: props.ctx.botId,
|
||||
name: 'table',
|
||||
payload: { tableCreated: false },
|
||||
})
|
||||
|
||||
const { tableCreated } = state.payload
|
||||
if (!tableCreated) {
|
||||
props.logger.debug(`Table "${props.configuration.tableName}" does not exist`)
|
||||
return
|
||||
}
|
||||
|
||||
const client = vanilla.clientFrom(props.client)
|
||||
props.logger.info(`Deleting table "${props.configuration.tableName}"`)
|
||||
await client.deleteTable({
|
||||
table: props.configuration.tableName,
|
||||
})
|
||||
|
||||
await props.client.setState({ type: 'bot', id: props.ctx.botId, name: 'table', payload: { tableCreated: false } })
|
||||
}
|
||||
|
||||
export const escapeObject = (obj: object): object => {
|
||||
return _(obj)
|
||||
.toPairs()
|
||||
.map(([key, value]) => [escapeKey(key), value])
|
||||
.fromPairs()
|
||||
.value()
|
||||
}
|
||||
|
||||
export const unescapeObject = (obj: object): object => {
|
||||
return _(obj)
|
||||
.toPairs()
|
||||
.map(([key, value]) => [unescapeKey(key), value])
|
||||
.fromPairs()
|
||||
.value()
|
||||
}
|
||||
|
||||
export const escapeKey = (key: string): string => (TABLE_RESERVED_KEYWORDS.includes(key) ? `_${key}` : key)
|
||||
|
||||
export const unescapeKey = (key: string): string => {
|
||||
const escapedColumns = TABLE_RESERVED_KEYWORDS.map(escapeKey)
|
||||
return escapedColumns.includes(key) ? key.slice(1) : key
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as client from '@botpress/client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export * from '@botpress/client'
|
||||
export const clientFrom = (client: bp.Client): client.Client => {
|
||||
return client._inner
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import config from '../../vitest.config'
|
||||
export default config
|
||||
Reference in New Issue
Block a user