@membranehq/cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +9 -0
- package/CHANGELOG.md +7 -0
- package/README.md +54 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +335 -0
- package/package.json +46 -0
- package/scripts/add-shebang.sh +6 -0
- package/scripts/prepare-package-json.ts +29 -0
- package/src/agent.tsx +50 -0
- package/src/cli.ts +72 -0
- package/src/commands/open.command.ts +51 -0
- package/src/commands/pull.command.ts +75 -0
- package/src/commands/push.command.ts +79 -0
- package/src/commands/test.command.ts +99 -0
- package/src/components/AddMcpServerScreen.tsx +215 -0
- package/src/components/AgentStatus.tsx +15 -0
- package/src/components/Main.tsx +64 -0
- package/src/components/OverviewSection.tsx +24 -0
- package/src/components/PersonalAccessTokenInput.tsx +56 -0
- package/src/components/RecentChanges.tsx +65 -0
- package/src/components/SelectWorkspace.tsx +112 -0
- package/src/components/Setup.tsx +121 -0
- package/src/components/WorkspaceStatus.tsx +61 -0
- package/src/contexts/FileWatcherContext.tsx +81 -0
- package/src/index.ts +27 -0
- package/src/legacy/commands/pullWorkspace.ts +70 -0
- package/src/legacy/commands/pushWorkspace.ts +246 -0
- package/src/legacy/integrationElements.ts +78 -0
- package/src/legacy/push/types.ts +17 -0
- package/src/legacy/reader/index.ts +113 -0
- package/src/legacy/types.ts +17 -0
- package/src/legacy/util.ts +149 -0
- package/src/legacy/workspace-elements/connectors.ts +397 -0
- package/src/legacy/workspace-elements/index.ts +27 -0
- package/src/legacy/workspace-tools/commands/pullWorkspace.ts +70 -0
- package/src/legacy/workspace-tools/integrationElements.ts +78 -0
- package/src/legacy/workspace-tools/util.ts +149 -0
- package/src/mcp/server-status.ts +27 -0
- package/src/mcp/server.ts +36 -0
- package/src/mcp/tools/getTestAccessToken.ts +32 -0
- package/src/modules/api/account-api-client.ts +89 -0
- package/src/modules/api/index.ts +3 -0
- package/src/modules/api/membrane-api-client.ts +116 -0
- package/src/modules/api/workspace-api-client.ts +11 -0
- package/src/modules/config/cwd-context.tsx +11 -0
- package/src/modules/config/project/getAgentVersion.ts +16 -0
- package/src/modules/config/project/index.ts +8 -0
- package/src/modules/config/project/paths.ts +25 -0
- package/src/modules/config/project/readProjectConfig.ts +27 -0
- package/src/modules/config/project/useProjectConfig.tsx +103 -0
- package/src/modules/config/system/index.ts +35 -0
- package/src/modules/file-watcher/index.ts +166 -0
- package/src/modules/file-watcher/types.ts +14 -0
- package/src/modules/setup/steps.ts +9 -0
- package/src/modules/setup/useSetup.ts +16 -0
- package/src/modules/status/useStatus.ts +16 -0
- package/src/modules/workspace-element-service/constants.ts +121 -0
- package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +69 -0
- package/src/modules/workspace-element-service/index.ts +304 -0
- package/src/testing/environment.ts +172 -0
- package/src/testing/runners/base.runner.ts +27 -0
- package/src/testing/runners/test.runner.ts +123 -0
- package/src/testing/scripts/generate-test-report.ts +757 -0
- package/src/testing/test-suites/base.ts +92 -0
- package/src/testing/test-suites/data-collection.ts +128 -0
- package/src/testing/testers/base.ts +115 -0
- package/src/testing/testers/create.ts +273 -0
- package/src/testing/testers/delete.ts +155 -0
- package/src/testing/testers/find-by-id.ts +135 -0
- package/src/testing/testers/list.ts +110 -0
- package/src/testing/testers/match.ts +149 -0
- package/src/testing/testers/search.ts +148 -0
- package/src/testing/testers/spec.ts +30 -0
- package/src/testing/testers/update.ts +284 -0
- package/src/utils/auth.ts +19 -0
- package/src/utils/constants.ts +27 -0
- package/src/utils/fields.ts +83 -0
- package/src/utils/logger.ts +106 -0
- package/src/utils/templating.ts +50 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Box, Text } from 'ink'
|
|
2
|
+
import useSWRImmutable from 'swr/immutable'
|
|
3
|
+
|
|
4
|
+
import { RecentChanges } from './RecentChanges'
|
|
5
|
+
import { useStatus } from '../modules/status/useStatus'
|
|
6
|
+
|
|
7
|
+
import type { OrgWorkspace } from '@integration-app/sdk'
|
|
8
|
+
|
|
9
|
+
function truncateWorkspaceName(name: string, maxLength: number = 30): string {
|
|
10
|
+
if (name.length <= maxLength) {
|
|
11
|
+
return name
|
|
12
|
+
}
|
|
13
|
+
return name.slice(0, maxLength - 3) + '...'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const WorkspaceSync = () => {
|
|
17
|
+
const { workspaceKey, cwd } = useStatus()
|
|
18
|
+
const width = Math.min(100, process.stdout.columns || 100)
|
|
19
|
+
|
|
20
|
+
// Fetch workspace data to get the workspace name
|
|
21
|
+
const { data } = useSWRImmutable('/account')
|
|
22
|
+
const workspaces = (data as any)?.workspaces as OrgWorkspace[]
|
|
23
|
+
|
|
24
|
+
// Find the current workspace by key to get its name
|
|
25
|
+
const currentWorkspace = workspaces?.find((ws: any) => ws.key === workspaceKey)
|
|
26
|
+
const workspaceName = currentWorkspace?.name
|
|
27
|
+
const displayName = workspaceName ? truncateWorkspaceName(workspaceName) : workspaceKey
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection='column' paddingX={1} borderStyle='round' borderTop width={width}>
|
|
31
|
+
{/* Header row with sync emoji and status */}
|
|
32
|
+
<Box marginTop={-1} marginBottom={1}>
|
|
33
|
+
<Text bold>
|
|
34
|
+
🔄 Workspaces — <Text color='green'>in sync</Text>
|
|
35
|
+
</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
{/* Local row */}
|
|
38
|
+
<Box>
|
|
39
|
+
<Box width={12}>
|
|
40
|
+
<Text color='grey'>Local:</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
<Text color='grey'>{cwd}</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
{/* Remote row (always shown) */}
|
|
45
|
+
<Box>
|
|
46
|
+
<Box width={12}>
|
|
47
|
+
<Text color='grey'>Remote:</Text>
|
|
48
|
+
</Box>
|
|
49
|
+
{workspaceKey ? (
|
|
50
|
+
<Text color='grey'>{displayName} [o: open in console] [w: change]</Text>
|
|
51
|
+
) : (
|
|
52
|
+
<Text>
|
|
53
|
+
<Text color='yellow'>not selected</Text> [w: select]
|
|
54
|
+
</Text>
|
|
55
|
+
)}
|
|
56
|
+
</Box>
|
|
57
|
+
|
|
58
|
+
<RecentChanges />
|
|
59
|
+
</Box>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { WorkspaceElementType } from '@integration-app/sdk'
|
|
3
|
+
|
|
4
|
+
import { FileChangeEventType } from '../modules/file-watcher/types'
|
|
5
|
+
import { getTypeAndKeyFromPath } from '../modules/workspace-element-service/getTypeAndKeyFromPath'
|
|
6
|
+
|
|
7
|
+
import type { FileWatcher } from '../modules/file-watcher'
|
|
8
|
+
import type { FileChangeEvent } from '../modules/file-watcher/types'
|
|
9
|
+
|
|
10
|
+
interface FileChangeWithType extends FileChangeEvent {
|
|
11
|
+
readonly type: FileChangeEventType
|
|
12
|
+
readonly elementKey: string
|
|
13
|
+
readonly elementType: WorkspaceElementType
|
|
14
|
+
readonly timestamp: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FileWatcherContextValue {
|
|
18
|
+
readonly fileWatcher: FileWatcher | null
|
|
19
|
+
readonly recentChanges: readonly FileChangeWithType[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FileWatcherContext = createContext<FileWatcherContextValue | null>(null)
|
|
23
|
+
|
|
24
|
+
interface FileWatcherProviderProps {
|
|
25
|
+
readonly children: React.ReactNode
|
|
26
|
+
readonly fileWatcher: FileWatcher
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function FileWatcherProvider({ children, fileWatcher }: FileWatcherProviderProps) {
|
|
30
|
+
const [recentChanges, setRecentChanges] = useState<FileChangeWithType[]>([])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const handleFileChange = (type: FileChangeEventType) => (event: FileChangeEvent) => {
|
|
34
|
+
const elementRef = getTypeAndKeyFromPath(event.relativePath)
|
|
35
|
+
if (!elementRef) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const changeWithType: FileChangeWithType = {
|
|
40
|
+
...event,
|
|
41
|
+
elementType: elementRef.type,
|
|
42
|
+
elementKey: elementRef.key,
|
|
43
|
+
type,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setRecentChanges((prev) => [changeWithType, ...prev.slice(0, 2)]) // keep only last 3 changes
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleAdded = handleFileChange('added')
|
|
51
|
+
const handleChanged = handleFileChange('changed')
|
|
52
|
+
const handleDeleted = handleFileChange('deleted')
|
|
53
|
+
|
|
54
|
+
fileWatcher.on('added', handleAdded)
|
|
55
|
+
fileWatcher.on('changed', handleChanged)
|
|
56
|
+
fileWatcher.on('deleted', handleDeleted)
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
fileWatcher.off('added', handleAdded)
|
|
60
|
+
fileWatcher.off('changed', handleChanged)
|
|
61
|
+
fileWatcher.off('deleted', handleDeleted)
|
|
62
|
+
}
|
|
63
|
+
}, [fileWatcher])
|
|
64
|
+
|
|
65
|
+
const value: FileWatcherContextValue = {
|
|
66
|
+
fileWatcher,
|
|
67
|
+
recentChanges,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return <FileWatcherContext.Provider value={value}>{children}</FileWatcherContext.Provider>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useFileWatcher(): FileWatcherContextValue {
|
|
74
|
+
const context = useContext(FileWatcherContext)
|
|
75
|
+
|
|
76
|
+
if (!context) {
|
|
77
|
+
throw new Error('useFileWatcher must be used within a FileWatcherProvider')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return context
|
|
81
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runAgent } from './agent'
|
|
4
|
+
import { runCLI } from './cli'
|
|
5
|
+
|
|
6
|
+
const AGENT_ARGUMENTS = ['--cwd']
|
|
7
|
+
|
|
8
|
+
function hasCLIArguments(): boolean {
|
|
9
|
+
const args = process.argv.slice(2)
|
|
10
|
+
return args.some((arg, index) => {
|
|
11
|
+
// Skip the value of an agent argument
|
|
12
|
+
if (index > 0 && AGENT_ARGUMENTS.includes(args[index - 1])) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
// Check if current argument is not an agent argument
|
|
16
|
+
return !AGENT_ARGUMENTS.includes(arg)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if there are any arguments that are not agent arguments
|
|
21
|
+
if (1 || hasCLIArguments()) {
|
|
22
|
+
// Run single-command CLI mode
|
|
23
|
+
runCLI()
|
|
24
|
+
} else {
|
|
25
|
+
// Run in agent mode (interactive)
|
|
26
|
+
runAgent()
|
|
27
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import { IntegrationAppClient, WorkspaceElementType } from '@integration-app/sdk'
|
|
5
|
+
import YAML from 'js-yaml'
|
|
6
|
+
|
|
7
|
+
import { getWorkspaceData, coloredLog } from '../util'
|
|
8
|
+
import { getWorkspaceElementTypePath } from '../workspace-elements'
|
|
9
|
+
import { pullConnectors } from '../workspace-elements/connectors'
|
|
10
|
+
|
|
11
|
+
interface ExportPackageOptions {
|
|
12
|
+
outputPath?: string
|
|
13
|
+
allConnectors?: boolean
|
|
14
|
+
client?: IntegrationAppClient
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function pullWorkspace(options: ExportPackageOptions): Promise<void> {
|
|
18
|
+
const { outputPath, allConnectors, client } = options
|
|
19
|
+
|
|
20
|
+
await pullConnectors({
|
|
21
|
+
client,
|
|
22
|
+
basePath: outputPath,
|
|
23
|
+
allConnectors,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const workspaceData = await getWorkspaceData(client)
|
|
27
|
+
|
|
28
|
+
for (const elementType of Object.keys(workspaceData)) {
|
|
29
|
+
if (workspaceData[elementType].length > 0) {
|
|
30
|
+
try {
|
|
31
|
+
fs.readdirSync(outputPath)
|
|
32
|
+
} catch (_err) {
|
|
33
|
+
fs.mkdirSync(outputPath, { recursive: true })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const elementTypePath = getWorkspaceElementTypePath(elementType as WorkspaceElementType)
|
|
37
|
+
|
|
38
|
+
for (const element of workspaceData[elementType]) {
|
|
39
|
+
try {
|
|
40
|
+
fs.readdirSync(path.join(outputPath, elementTypePath, `${element.key}`))
|
|
41
|
+
} catch (_err) {
|
|
42
|
+
fs.mkdirSync(path.join(outputPath, elementTypePath, `${element.key}`), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
if (element.integration || element.integrationKey) {
|
|
47
|
+
const integrationKey = element.integration ? element.integration.key : element.integrationKey
|
|
48
|
+
try {
|
|
49
|
+
fs.readdirSync(path.join(outputPath, elementTypePath, `${element.key}`, `${integrationKey}`))
|
|
50
|
+
} catch (_err) {
|
|
51
|
+
fs.mkdirSync(path.join(outputPath, elementTypePath, `${element.key}`, `${integrationKey}`), {
|
|
52
|
+
recursive: true,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(outputPath, elementTypePath, `${element.key}`, `${integrationKey}`, `${integrationKey}.yaml`),
|
|
57
|
+
YAML.dump(element),
|
|
58
|
+
)
|
|
59
|
+
} else {
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(outputPath, elementTypePath, element.key, `${element.key}.yaml`),
|
|
62
|
+
YAML.dump(element),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
coloredLog(`Data written to ${outputPath} successfully.`, 'BgGreen')
|
|
70
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { WorkspaceElementSpecs } from '@integration-app/sdk'
|
|
2
|
+
import { toCamelCase } from 'js-convert-case'
|
|
3
|
+
|
|
4
|
+
import { Logger } from '../../utils/logger'
|
|
5
|
+
import { INTEGRATION_ELEMENTS } from '../integrationElements'
|
|
6
|
+
import { PushContext, WorkspaceData } from '../push/types'
|
|
7
|
+
import { readIntegrationLevelElements, readTopLevelElements } from '../reader/index'
|
|
8
|
+
import { getWorkspaceData, hasParent, splitWorkspaceData, coloredLog } from '../util'
|
|
9
|
+
import { pushConnectors } from '../workspace-elements/connectors'
|
|
10
|
+
import { mergeWorkspaceData } from '../workspace-elements/index'
|
|
11
|
+
|
|
12
|
+
export async function pushWorkspace({ basePath, client }) {
|
|
13
|
+
const response = await client.get('org-workspace-id')
|
|
14
|
+
const workspaceId = response.id
|
|
15
|
+
|
|
16
|
+
const workspaceData = await getWorkspaceData(client, 'minified')
|
|
17
|
+
|
|
18
|
+
const data: WorkspaceData = {}
|
|
19
|
+
Logger.info(`Loading local workspace data`)
|
|
20
|
+
|
|
21
|
+
const topLevelElements = await readTopLevelElements()
|
|
22
|
+
mergeWorkspaceData(data, topLevelElements)
|
|
23
|
+
|
|
24
|
+
const integrationLevelElements = await readIntegrationLevelElements()
|
|
25
|
+
mergeWorkspaceData(data, integrationLevelElements)
|
|
26
|
+
|
|
27
|
+
const { universalElements, integrationSpecificElements } = splitWorkspaceData(data)
|
|
28
|
+
|
|
29
|
+
const pushContext: PushContext = {
|
|
30
|
+
connectorsMapping: {},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await syncIntegrations({
|
|
34
|
+
basePath,
|
|
35
|
+
sourceData: data,
|
|
36
|
+
workspaceData,
|
|
37
|
+
pushContext,
|
|
38
|
+
iApp: client,
|
|
39
|
+
workspaceId,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Re-fetch latest integrations
|
|
43
|
+
workspaceData.integration = await client.integrations.findAll()
|
|
44
|
+
|
|
45
|
+
coloredLog(`Syncing Universal Elements`, 'BgBlue')
|
|
46
|
+
Logger.group('Syncing Universal Elements')
|
|
47
|
+
await syncElements(universalElements, workspaceData, client)
|
|
48
|
+
Logger.groupEnd()
|
|
49
|
+
|
|
50
|
+
coloredLog(`Syncing Integration Specific Elements`, 'BgBlue')
|
|
51
|
+
Logger.group('Syncing Integration Specific Elements')
|
|
52
|
+
|
|
53
|
+
await syncElements(integrationSpecificElements, workspaceData, client)
|
|
54
|
+
Logger.groupEnd()
|
|
55
|
+
|
|
56
|
+
Logger.success(`Data written successfully.`)
|
|
57
|
+
|
|
58
|
+
return pushContext
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function syncIntegrations({
|
|
62
|
+
basePath,
|
|
63
|
+
sourceData,
|
|
64
|
+
workspaceData,
|
|
65
|
+
pushContext,
|
|
66
|
+
iApp,
|
|
67
|
+
workspaceId,
|
|
68
|
+
}: {
|
|
69
|
+
basePath: string
|
|
70
|
+
sourceData: any
|
|
71
|
+
workspaceData: WorkspaceData
|
|
72
|
+
pushContext: PushContext
|
|
73
|
+
iApp: any
|
|
74
|
+
workspaceId: string
|
|
75
|
+
}) {
|
|
76
|
+
coloredLog('Matching imported integrations and connectors with existing ones', 'BgBlue')
|
|
77
|
+
|
|
78
|
+
await pushConnectors({
|
|
79
|
+
client: iApp,
|
|
80
|
+
basePath,
|
|
81
|
+
workspaceData,
|
|
82
|
+
pushContext,
|
|
83
|
+
workspaceId,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const integrationMissmatchErrors = []
|
|
87
|
+
for (const integration of sourceData.integration) {
|
|
88
|
+
const matchedIntegration = workspaceData.integration.find((item) => item.key == integration.key)
|
|
89
|
+
|
|
90
|
+
const integrationPayload = {
|
|
91
|
+
connectorId: integration.connectorId,
|
|
92
|
+
connectorVersion: integration.connectorVersion,
|
|
93
|
+
name: integration.name,
|
|
94
|
+
key: integration.key,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pushContext.connectorsMapping[integration.connectorId]) {
|
|
98
|
+
integrationPayload.connectorId = pushContext.connectorsMapping[integration.connectorId]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!matchedIntegration) {
|
|
102
|
+
console.debug(`[Push] Creating integration ${integration.key}`)
|
|
103
|
+
await iApp.integrations.create(integrationPayload)
|
|
104
|
+
} else {
|
|
105
|
+
console.debug(`[Push] Updating integration ${integration.key}`)
|
|
106
|
+
await iApp.integration((matchedIntegration as any).id).patch(integrationPayload)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
coloredLog(`Imported integration ${integration.name} (${integration.key})`, 'Green')
|
|
110
|
+
}
|
|
111
|
+
if (integrationMissmatchErrors.length > 0) {
|
|
112
|
+
Logger.table(
|
|
113
|
+
integrationMissmatchErrors.map((item) => ({
|
|
114
|
+
key: item.key,
|
|
115
|
+
name: item.name,
|
|
116
|
+
})),
|
|
117
|
+
)
|
|
118
|
+
coloredLog(
|
|
119
|
+
'Integration missmatch errors. Make sure you have those applications in your destination workspace',
|
|
120
|
+
'BgRed',
|
|
121
|
+
)
|
|
122
|
+
throw new Error('Integration missmatch errors')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function syncElements(data, workspaceData, iApp) {
|
|
127
|
+
for (const elementType of Object.keys(data)) {
|
|
128
|
+
const isExportable = INTEGRATION_ELEMENTS[elementType]?.exportable
|
|
129
|
+
|
|
130
|
+
if (isExportable === false) continue
|
|
131
|
+
|
|
132
|
+
for (const element of data[elementType]) {
|
|
133
|
+
// Cleanup
|
|
134
|
+
delete element.integrationId
|
|
135
|
+
|
|
136
|
+
const destinationElement = matchElement(element, elementType, workspaceData)
|
|
137
|
+
const singleElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].name)
|
|
138
|
+
const pluralElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].namePlural)
|
|
139
|
+
|
|
140
|
+
if (destinationElement) {
|
|
141
|
+
if (!hasParent(element) || (elementIsCustomized(element) && hasParent(element))) {
|
|
142
|
+
// Update the element without parent OR the integration-specific element that should be customized
|
|
143
|
+
await iApp[singleElementAccessorKey](destinationElement.id).put(element)
|
|
144
|
+
coloredLog(
|
|
145
|
+
`Updated ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
|
|
146
|
+
'Cyan',
|
|
147
|
+
)
|
|
148
|
+
} else if (hasParent(element)) {
|
|
149
|
+
// Reset Integration-specific element if it has universal parent
|
|
150
|
+
try {
|
|
151
|
+
await iApp[singleElementAccessorKey](destinationElement.id).reset()
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.debug(destinationElement, element)
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
coloredLog(
|
|
157
|
+
`Customization reset ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
|
|
158
|
+
'Magenta',
|
|
159
|
+
)
|
|
160
|
+
} else {
|
|
161
|
+
coloredLog(
|
|
162
|
+
`Corrupted. Migrate Manually ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
|
|
163
|
+
'Red',
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Create the element
|
|
168
|
+
|
|
169
|
+
if (hasParent(element)) {
|
|
170
|
+
try {
|
|
171
|
+
await iApp[singleElementAccessorKey]({
|
|
172
|
+
key: element.key,
|
|
173
|
+
}).apply([element.integrationKey])
|
|
174
|
+
} catch (_error) {
|
|
175
|
+
try {
|
|
176
|
+
await iApp[singleElementAccessorKey]({
|
|
177
|
+
key: element.key,
|
|
178
|
+
integrationKey: element.integrationKey,
|
|
179
|
+
}).put(element)
|
|
180
|
+
} catch (_error) {
|
|
181
|
+
await iApp[pluralElementAccessorKey].create({
|
|
182
|
+
...element,
|
|
183
|
+
integrationId: workspaceData.integration.find((item) => item.key == element.integrationKey).id,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (elementIsCustomized(element)) {
|
|
188
|
+
try {
|
|
189
|
+
await iApp[singleElementAccessorKey]({
|
|
190
|
+
key: element.key,
|
|
191
|
+
integrationKey: element.integrationKey,
|
|
192
|
+
}).put(element)
|
|
193
|
+
} catch (_error) {
|
|
194
|
+
console.debug(destinationElement, element)
|
|
195
|
+
throw _error
|
|
196
|
+
}
|
|
197
|
+
coloredLog(
|
|
198
|
+
`Applied & Customized ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
|
|
199
|
+
'Green',
|
|
200
|
+
)
|
|
201
|
+
} else {
|
|
202
|
+
coloredLog(
|
|
203
|
+
`Applied universal ${INTEGRATION_ELEMENTS[elementType].element} ${element.key} to ${element.integrationKey} `,
|
|
204
|
+
'Green',
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
try {
|
|
209
|
+
if (element.integrationKey) {
|
|
210
|
+
delete element.integration
|
|
211
|
+
element.integrationId = workspaceData.integration.find((item) => item.key == element.integrationKey).id
|
|
212
|
+
}
|
|
213
|
+
await iApp[pluralElementAccessorKey].create(element)
|
|
214
|
+
} catch (_error) {
|
|
215
|
+
console.debug(`Error creating element ${elementType}`, hasParent(element), element.integrationKey, element)
|
|
216
|
+
throw _error
|
|
217
|
+
}
|
|
218
|
+
coloredLog(
|
|
219
|
+
`Created ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
|
|
220
|
+
'Green',
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//console.debug(elementType, element.key, element.integrationKey || "universal")
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function elementIsCustomized(element) {
|
|
230
|
+
return element.integrationKey && (element.customized || element.isCustomized)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function matchElement(element, elementType, workspaceData) {
|
|
234
|
+
const matchedElements = workspaceData[elementType]?.filter(
|
|
235
|
+
(item) => item.key == element.key && item.integrationKey == element.integrationKey && !item.archivedAt,
|
|
236
|
+
)
|
|
237
|
+
if (matchedElements.length > 1) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`More than one ${element.integrationKey || 'universal'} ${elementType} with key ${element.key} found in the workspace`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
if (matchedElements.length == 1) {
|
|
243
|
+
return matchedElements[0]
|
|
244
|
+
}
|
|
245
|
+
return
|
|
246
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { WorkspaceElementType } from '@integration-app/sdk'
|
|
2
|
+
|
|
3
|
+
export const INTEGRATION_ELEMENTS = {
|
|
4
|
+
[WorkspaceElementType.Integration]: {
|
|
5
|
+
element: 'integration',
|
|
6
|
+
elements: 'integrations',
|
|
7
|
+
exportable: false,
|
|
8
|
+
exportCleanup: (el) => {
|
|
9
|
+
return {
|
|
10
|
+
id: el.id,
|
|
11
|
+
key: el.key,
|
|
12
|
+
name: el.name,
|
|
13
|
+
connectorId: el.connectorId,
|
|
14
|
+
baseUri: el.baseUri,
|
|
15
|
+
connectorVersion: el.connectorVersion,
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
[WorkspaceElementType.Connector]: {
|
|
20
|
+
element: 'connector',
|
|
21
|
+
elements: 'connectors',
|
|
22
|
+
exportable: false,
|
|
23
|
+
},
|
|
24
|
+
[WorkspaceElementType.Action]: {
|
|
25
|
+
element: 'action',
|
|
26
|
+
elements: 'actions',
|
|
27
|
+
integrationSpecific: true,
|
|
28
|
+
exportCleanup: (el) => {
|
|
29
|
+
delete el.integration
|
|
30
|
+
return el
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
[WorkspaceElementType.AppDataSchema]: {
|
|
34
|
+
element: 'appDataSchema',
|
|
35
|
+
elements: 'appDataSchemas',
|
|
36
|
+
},
|
|
37
|
+
[WorkspaceElementType.AppEventType]: {
|
|
38
|
+
element: 'appEventType',
|
|
39
|
+
elements: 'appEventTypes',
|
|
40
|
+
},
|
|
41
|
+
[WorkspaceElementType.DataLinkTable]: {
|
|
42
|
+
element: 'dataLinkTable',
|
|
43
|
+
elements: 'dataLinkTables',
|
|
44
|
+
},
|
|
45
|
+
[WorkspaceElementType.DataSource]: {
|
|
46
|
+
element: 'dataSource',
|
|
47
|
+
elements: 'dataSources',
|
|
48
|
+
integrationSpecific: true,
|
|
49
|
+
},
|
|
50
|
+
[WorkspaceElementType.FieldMapping]: {
|
|
51
|
+
element: 'fieldMapping',
|
|
52
|
+
elements: 'fieldMappings',
|
|
53
|
+
integrationSpecific: true,
|
|
54
|
+
exportCleanup: (fieldMapping) => {
|
|
55
|
+
delete fieldMapping.dataSourceId
|
|
56
|
+
return fieldMapping
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
[WorkspaceElementType.Flow]: {
|
|
60
|
+
element: 'flow',
|
|
61
|
+
elements: 'flows',
|
|
62
|
+
integrationSpecific: true,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function baseExportCleanup(element) {
|
|
67
|
+
delete element.workspaceId
|
|
68
|
+
delete element.createdAt
|
|
69
|
+
delete element.updatedAt
|
|
70
|
+
delete element.revision
|
|
71
|
+
delete element.parentRevision
|
|
72
|
+
Object.keys(element).map((key) => {
|
|
73
|
+
if (key.match(/universal.*Revision/g)) {
|
|
74
|
+
delete element[key]
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
return element
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WorkspaceElementType } from '@integration-app/sdk'
|
|
2
|
+
|
|
3
|
+
export type ConnectorsMapping = Record<string, string>
|
|
4
|
+
|
|
5
|
+
export interface PushContext {
|
|
6
|
+
connectorsMapping: ConnectorsMapping
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface WorkspaceElement {
|
|
10
|
+
key: string
|
|
11
|
+
integration?: {
|
|
12
|
+
key: string
|
|
13
|
+
}
|
|
14
|
+
integrationKey?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WorkspaceData = Partial<Record<WorkspaceElementType, WorkspaceElement[]>>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import { WorkspaceElementType, WorkspaceElement, WorkspaceElementSpecs } from '@integration-app/sdk'
|
|
5
|
+
import { toCamelCase } from 'js-convert-case'
|
|
6
|
+
import yaml from 'js-yaml'
|
|
7
|
+
|
|
8
|
+
import { getPaths } from '../../utils/constants'
|
|
9
|
+
import { Logger } from '../../utils/logger'
|
|
10
|
+
import { WorkspaceData } from '../push/types'
|
|
11
|
+
import { getWorkspaceElementTypePath, PUSHABLE_ELEMENT_TYPES } from '../workspace-elements'
|
|
12
|
+
|
|
13
|
+
export async function readTopLevelElements(): Promise<WorkspaceData> {
|
|
14
|
+
const result: WorkspaceData = {}
|
|
15
|
+
|
|
16
|
+
for (const elementType of PUSHABLE_ELEMENT_TYPES) {
|
|
17
|
+
Logger.info(`Loading ${WorkspaceElementSpecs[elementType].namePlural}`)
|
|
18
|
+
|
|
19
|
+
const elements: WorkspaceElement[] = []
|
|
20
|
+
|
|
21
|
+
const paths = getPaths()
|
|
22
|
+
const basePath = paths.membraneDirPath
|
|
23
|
+
|
|
24
|
+
const elementsDirs = [path.join(basePath, getWorkspaceElementTypePath(elementType))]
|
|
25
|
+
|
|
26
|
+
const legacyDir = path.join(basePath, toCamelCase(getWorkspaceElementTypePath(elementType))) // Legacy: we used `dataSources` instead of `data-sources`.
|
|
27
|
+
|
|
28
|
+
if (!elementsDirs.includes(legacyDir)) {
|
|
29
|
+
elementsDirs.push(legacyDir)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const elementsDir of elementsDirs) {
|
|
33
|
+
if (!fs.existsSync(elementsDir)) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
elements.push(...(await readWorkspaceElementsFromDir(elementsDir)))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
result[elementType] = [...(result[elementType] || []), ...elements]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function readIntegrationLevelElements(): Promise<WorkspaceData> {
|
|
47
|
+
const result: WorkspaceData = {}
|
|
48
|
+
const paths = getPaths()
|
|
49
|
+
const basePath = paths.membraneDirPath
|
|
50
|
+
const integrationsPath = path.join(basePath, getWorkspaceElementTypePath(WorkspaceElementType.Integration))
|
|
51
|
+
|
|
52
|
+
const integrationItems = fs.readdirSync(integrationsPath)
|
|
53
|
+
|
|
54
|
+
for (const integrationPath of integrationItems) {
|
|
55
|
+
if (fs.statSync(path.join(integrationsPath, integrationPath)).isDirectory()) {
|
|
56
|
+
const integration = await readWorkspaceElementFromDirItem(integrationsPath, integrationPath)
|
|
57
|
+
|
|
58
|
+
for (const elementType of PUSHABLE_ELEMENT_TYPES) {
|
|
59
|
+
const elementTypeDir = path.join(integrationsPath, integrationPath, getWorkspaceElementTypePath(elementType))
|
|
60
|
+
const elements = await readWorkspaceElementsFromDir(elementTypeDir)
|
|
61
|
+
elements.forEach((element) => {
|
|
62
|
+
;(element as any).integrationKey = integration?.key
|
|
63
|
+
})
|
|
64
|
+
result[elementType] = [...(result[elementType] || []), ...elements]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readWorkspaceElementsFromDir(dirPath: string): Promise<WorkspaceElement[]> {
|
|
73
|
+
const elements: WorkspaceElement[] = []
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(dirPath)) {
|
|
76
|
+
return elements
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Logger.info(`Reading ${dirPath}`)
|
|
80
|
+
|
|
81
|
+
const elementItems = fs.readdirSync(dirPath)
|
|
82
|
+
|
|
83
|
+
for (const elementPath of elementItems) {
|
|
84
|
+
const element = await readWorkspaceElementFromDirItem(dirPath, elementPath)
|
|
85
|
+
|
|
86
|
+
if (element) {
|
|
87
|
+
elements.push(element)
|
|
88
|
+
|
|
89
|
+
Logger.success(`Loaded ${elementPath}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return elements
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function readWorkspaceElementFromDirItem(
|
|
97
|
+
dirPath: string,
|
|
98
|
+
elementPath: string,
|
|
99
|
+
): Promise<WorkspaceElement | undefined> {
|
|
100
|
+
const fullElementPath = path.join(dirPath, elementPath)
|
|
101
|
+
|
|
102
|
+
const elementPathIsDirectory = fs.statSync(fullElementPath).isDirectory()
|
|
103
|
+
|
|
104
|
+
const elementSpecPath = elementPathIsDirectory ? path.join(fullElementPath, `${elementPath}.yaml`) : fullElementPath
|
|
105
|
+
|
|
106
|
+
if (fs.existsSync(elementSpecPath)) {
|
|
107
|
+
const elementSpec = yaml.load(fs.readFileSync(elementSpecPath, 'utf8'))
|
|
108
|
+
|
|
109
|
+
return elementSpec as WorkspaceElement
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return undefined
|
|
113
|
+
}
|