chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import rootConfig from '../../eslint.config.mjs'
export default [
...rootConfig,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
]
+32
View File
@@ -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"
}
}
+74
View File
@@ -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),
},
})
+13
View File
@@ -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))
}
+80
View File
@@ -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
+93
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": { "*": ["./*"] },
"outDir": "dist"
},
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
}
+2
View File
@@ -0,0 +1,2 @@
import config from '../../vitest.config'
export default config