@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,103 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
|
5
|
+
import type { PropsWithChildren } from 'react'
|
|
6
|
+
import YAML from 'js-yaml'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
|
|
9
|
+
import { useCwd } from '../cwd-context'
|
|
10
|
+
import { readProjectConfig } from './readProjectConfig'
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILE_NAME = 'membrane.config.yml'
|
|
13
|
+
|
|
14
|
+
export const configSchema = z.object({
|
|
15
|
+
workspaceKey: z.string().optional(),
|
|
16
|
+
workspaceSecret: z.string().optional(),
|
|
17
|
+
apiUri: z.string().optional(),
|
|
18
|
+
testCustomerId: z.string().optional(),
|
|
19
|
+
anthropicApiKey: z.string().optional(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export type ProjectConfig = z.infer<typeof configSchema>
|
|
23
|
+
|
|
24
|
+
export const commandSchema = z.enum(['pull'])
|
|
25
|
+
export type Command = z.infer<typeof commandSchema>
|
|
26
|
+
|
|
27
|
+
interface IProjectConfigContext {
|
|
28
|
+
readonly config: ProjectConfig | null
|
|
29
|
+
readonly isLoading: boolean
|
|
30
|
+
readonly updateConfig: (newConfig: Partial<ProjectConfig>) => boolean
|
|
31
|
+
readonly validateConfig: () => boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ProjectConfigContext = createContext<IProjectConfigContext | undefined>(undefined)
|
|
35
|
+
|
|
36
|
+
export function ProjectConfigProvider({ children }: PropsWithChildren) {
|
|
37
|
+
const cwd = useCwd()
|
|
38
|
+
const [config, setConfig] = useState<ProjectConfig | null>(null)
|
|
39
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const config = readProjectConfig(cwd)
|
|
43
|
+
setConfig(config)
|
|
44
|
+
setIsLoading(false)
|
|
45
|
+
}, [cwd])
|
|
46
|
+
|
|
47
|
+
const updateConfig = useCallback(
|
|
48
|
+
(newConfig: Partial<ProjectConfig>): boolean => {
|
|
49
|
+
const configPath = path.join(cwd, CONFIG_FILE_NAME)
|
|
50
|
+
const currentConfig = readProjectConfig(cwd) ?? {}
|
|
51
|
+
const newConfigData = {
|
|
52
|
+
...currentConfig,
|
|
53
|
+
...newConfig,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const validationResult = configSchema.safeParse(newConfigData)
|
|
57
|
+
if (!validationResult.success) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(configPath, YAML.dump(validationResult.data))
|
|
62
|
+
setConfig(validationResult.data)
|
|
63
|
+
return true
|
|
64
|
+
},
|
|
65
|
+
[cwd],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const validateConfig = useCallback((): boolean => {
|
|
69
|
+
return !!(config?.workspaceKey && config?.workspaceSecret)
|
|
70
|
+
}, [config])
|
|
71
|
+
|
|
72
|
+
const value: IProjectConfigContext = {
|
|
73
|
+
config,
|
|
74
|
+
isLoading,
|
|
75
|
+
updateConfig,
|
|
76
|
+
validateConfig,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return <ProjectConfigContext.Provider value={value}>{children}</ProjectConfigContext.Provider>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useProjectConfig(): IProjectConfigContext {
|
|
83
|
+
const context = useContext(ProjectConfigContext)
|
|
84
|
+
if (context === undefined) {
|
|
85
|
+
throw new Error('useProjectConfig must be used within a ProjectConfigProvider')
|
|
86
|
+
}
|
|
87
|
+
return context
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function clearWorkspaceConfig(): void {
|
|
91
|
+
const cwd = process.cwd()
|
|
92
|
+
const configPath = path.join(cwd, CONFIG_FILE_NAME)
|
|
93
|
+
const currentConfig = readProjectConfig(cwd) ?? {}
|
|
94
|
+
const newConfigData = {
|
|
95
|
+
...currentConfig,
|
|
96
|
+
workspaceKey: undefined,
|
|
97
|
+
workspaceSecret: undefined,
|
|
98
|
+
}
|
|
99
|
+
// Remove keys if present
|
|
100
|
+
delete newConfigData.workspaceKey
|
|
101
|
+
delete newConfigData.workspaceSecret
|
|
102
|
+
fs.writeFileSync(configPath, YAML.dump(newConfigData))
|
|
103
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os from 'os'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import Conf from 'conf'
|
|
5
|
+
|
|
6
|
+
const schema = {
|
|
7
|
+
pat: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
},
|
|
10
|
+
workspace: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config = new Conf({
|
|
16
|
+
schema,
|
|
17
|
+
configName: 'config',
|
|
18
|
+
cwd: path.join(os.homedir(), '.membrane'),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const setPat = (token: string): void => {
|
|
22
|
+
config.set('pat', token)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getPat = (): string | undefined => {
|
|
26
|
+
return config.get('pat') as string | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const hasPat = (): boolean => {
|
|
30
|
+
return !!config.get('pat')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const clearPat = (): void => {
|
|
34
|
+
config.delete('pat')
|
|
35
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
import * as fs from 'node:fs'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import chokidar from 'chokidar'
|
|
7
|
+
import yaml from 'yaml'
|
|
8
|
+
|
|
9
|
+
import { FileChangeEventType, FileChangeEvent, FileChangeEventMap } from './types'
|
|
10
|
+
import { getMembraneDir, IGNORED_FILES } from '../config/project/paths'
|
|
11
|
+
|
|
12
|
+
import type { FSWatcher } from 'chokidar'
|
|
13
|
+
|
|
14
|
+
type WatchOptions = Parameters<typeof chokidar.watch>[1]
|
|
15
|
+
|
|
16
|
+
const DEFAULT_WATCHER_CONFIG: WatchOptions = {
|
|
17
|
+
ignored: IGNORED_FILES,
|
|
18
|
+
persistent: true,
|
|
19
|
+
ignoreInitial: true,
|
|
20
|
+
followSymlinks: false,
|
|
21
|
+
depth: 10,
|
|
22
|
+
awaitWriteFinish: {
|
|
23
|
+
stabilityThreshold: 500,
|
|
24
|
+
pollInterval: 100,
|
|
25
|
+
},
|
|
26
|
+
ignorePermissionErrors: true,
|
|
27
|
+
atomic: true,
|
|
28
|
+
usePolling: false,
|
|
29
|
+
alwaysStat: false,
|
|
30
|
+
interval: 1000,
|
|
31
|
+
binaryInterval: 300,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class FileWatcher extends EventEmitter<FileChangeEventMap> {
|
|
35
|
+
private isWatching = false
|
|
36
|
+
private watcher?: FSWatcher
|
|
37
|
+
private readonly membraneDir: string
|
|
38
|
+
private readonly contentCache: Record<string, string> = {}
|
|
39
|
+
|
|
40
|
+
constructor(private readonly options: { cwd: string }) {
|
|
41
|
+
super()
|
|
42
|
+
this.membraneDir = getMembraneDir(options.cwd)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Starts the file watcher
|
|
47
|
+
*/
|
|
48
|
+
async start(): Promise<void> {
|
|
49
|
+
if (this.isWatching) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (!fs.existsSync(this.membraneDir)) {
|
|
53
|
+
fs.mkdirSync(this.membraneDir, { recursive: true })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.watcher = chokidar.watch(this.membraneDir, DEFAULT_WATCHER_CONFIG)
|
|
57
|
+
this.watcher
|
|
58
|
+
.on('add', (filePath) => this.handleFileSystemEvent('added', filePath))
|
|
59
|
+
.on('change', (filePath) => this.handleFileSystemEvent('changed', filePath))
|
|
60
|
+
.on('unlink', (filePath) => this.handleFileSystemEvent('deleted', filePath))
|
|
61
|
+
.on('ready', () => (this.isWatching = true))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stops the file watcher
|
|
66
|
+
*/
|
|
67
|
+
async stop(): Promise<void> {
|
|
68
|
+
if (!this.isWatching || !this.watcher) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this.watcher.close()
|
|
73
|
+
this.isWatching = false
|
|
74
|
+
this.watcher = undefined
|
|
75
|
+
this.clearCache()
|
|
76
|
+
this.emit('stopped')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getCwd(): string {
|
|
80
|
+
return this.options.cwd
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handles file system events and only emits when content actually changes
|
|
85
|
+
*/
|
|
86
|
+
private handleFileSystemEvent(eventType: FileChangeEventType, filePath: string): void {
|
|
87
|
+
const relativePath = path.relative(this.membraneDir, filePath)
|
|
88
|
+
|
|
89
|
+
if (eventType === 'deleted') {
|
|
90
|
+
this.removeFromCache(relativePath)
|
|
91
|
+
const event: FileChangeEvent = {
|
|
92
|
+
filePath,
|
|
93
|
+
relativePath,
|
|
94
|
+
data: undefined,
|
|
95
|
+
}
|
|
96
|
+
this.emit(eventType, event)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fileContent = this.readFileContent(filePath)
|
|
101
|
+
|
|
102
|
+
const hasContentChanged = this.hasContentChanged(relativePath, fileContent, eventType)
|
|
103
|
+
if (!hasContentChanged) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const event = this.processFileEvent(filePath, fileContent)
|
|
108
|
+
this.updateCache(relativePath, fileContent)
|
|
109
|
+
this.emit(eventType, event)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private readFileContent(filePath: string): string {
|
|
113
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private processFileEvent(filePath: string, fileContent: string): FileChangeEvent | null {
|
|
117
|
+
const relativePath = path.relative(this.membraneDir, filePath)
|
|
118
|
+
|
|
119
|
+
let data: any
|
|
120
|
+
try {
|
|
121
|
+
data = fileContent ? yaml.parse(fileContent) : undefined
|
|
122
|
+
} catch (_error) {
|
|
123
|
+
data = undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { filePath, relativePath, data }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private hasContentChanged(relativePath: string, fileContent: string, eventType: FileChangeEventType): boolean {
|
|
130
|
+
if (eventType === 'added') {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!fileContent) {
|
|
135
|
+
return this.contentCache[relativePath] !== undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const contentHash = this.getContentHash(fileContent)
|
|
139
|
+
const cachedHash = this.contentCache[relativePath]
|
|
140
|
+
|
|
141
|
+
return cachedHash !== contentHash
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private getContentHash(fileContent: string): string {
|
|
145
|
+
return crypto.createHash('sha256').update(fileContent).digest('hex')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private updateCache(relativePath: string, fileContent: string | null): void {
|
|
149
|
+
if (!fileContent) {
|
|
150
|
+
delete this.contentCache[relativePath]
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.contentCache[relativePath] = this.getContentHash(fileContent)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private removeFromCache(relativePath: string): void {
|
|
158
|
+
delete this.contentCache[relativePath]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private clearCache(): void {
|
|
162
|
+
Object.keys(this.contentCache).forEach((key) => {
|
|
163
|
+
delete this.contentCache[key]
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FileChangeEvent {
|
|
2
|
+
filePath: string
|
|
3
|
+
relativePath: string
|
|
4
|
+
data?: any
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type FileChangeEventMap = {
|
|
8
|
+
added: [FileChangeEvent]
|
|
9
|
+
changed: [FileChangeEvent]
|
|
10
|
+
deleted: [FileChangeEvent]
|
|
11
|
+
stopped: []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FileChangeEventType = keyof FileChangeEventMap
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useProjectConfig } from '../config/project'
|
|
2
|
+
import { hasPat } from '../config/system'
|
|
3
|
+
|
|
4
|
+
export const useSetup = () => {
|
|
5
|
+
const { config } = useProjectConfig()
|
|
6
|
+
|
|
7
|
+
const isAuthenticated = hasPat()
|
|
8
|
+
const workspaceIsConfigured = !!(config?.workspaceKey && config?.workspaceSecret)
|
|
9
|
+
const isSetupComplete = isAuthenticated && workspaceIsConfigured
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
isAuthenticated,
|
|
13
|
+
workspaceIsConfigured,
|
|
14
|
+
isSetupComplete,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useCwd } from '../config/cwd-context'
|
|
2
|
+
import { useProjectConfig } from '../config/project'
|
|
3
|
+
import { useSetup } from '../setup/useSetup'
|
|
4
|
+
|
|
5
|
+
export const useStatus = () => {
|
|
6
|
+
const { isSetupComplete } = useSetup()
|
|
7
|
+
const { config } = useProjectConfig()
|
|
8
|
+
const cwd = useCwd()
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
isSetupComplete,
|
|
12
|
+
workspaceKey: config?.workspaceKey,
|
|
13
|
+
config,
|
|
14
|
+
cwd,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { WorkspaceElementType } from '@integration-app/sdk'
|
|
2
|
+
|
|
3
|
+
export interface ElementConfig {
|
|
4
|
+
element: string
|
|
5
|
+
elements: string
|
|
6
|
+
exportable?: boolean
|
|
7
|
+
integrationSpecific?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const INTEGRATION_ELEMENTS: Record<WorkspaceElementType, ElementConfig> = {
|
|
11
|
+
[WorkspaceElementType.Integration]: {
|
|
12
|
+
element: 'integration',
|
|
13
|
+
elements: 'integrations',
|
|
14
|
+
exportable: false,
|
|
15
|
+
},
|
|
16
|
+
[WorkspaceElementType.Connector]: {
|
|
17
|
+
element: 'connector',
|
|
18
|
+
elements: 'connectors',
|
|
19
|
+
exportable: false,
|
|
20
|
+
},
|
|
21
|
+
[WorkspaceElementType.Action]: {
|
|
22
|
+
element: 'action',
|
|
23
|
+
elements: 'actions',
|
|
24
|
+
integrationSpecific: true,
|
|
25
|
+
},
|
|
26
|
+
[WorkspaceElementType.AppDataSchema]: {
|
|
27
|
+
element: 'appDataSchema',
|
|
28
|
+
elements: 'appDataSchemas',
|
|
29
|
+
},
|
|
30
|
+
[WorkspaceElementType.AppEventType]: {
|
|
31
|
+
element: 'appEventType',
|
|
32
|
+
elements: 'appEventTypes',
|
|
33
|
+
},
|
|
34
|
+
[WorkspaceElementType.DataLinkTable]: {
|
|
35
|
+
element: 'dataLinkTable',
|
|
36
|
+
elements: 'dataLinkTables',
|
|
37
|
+
},
|
|
38
|
+
[WorkspaceElementType.DataSource]: {
|
|
39
|
+
element: 'dataSource',
|
|
40
|
+
elements: 'dataSources',
|
|
41
|
+
integrationSpecific: true,
|
|
42
|
+
},
|
|
43
|
+
[WorkspaceElementType.FieldMapping]: {
|
|
44
|
+
element: 'fieldMapping',
|
|
45
|
+
elements: 'fieldMappings',
|
|
46
|
+
integrationSpecific: true,
|
|
47
|
+
},
|
|
48
|
+
[WorkspaceElementType.Flow]: {
|
|
49
|
+
element: 'flow',
|
|
50
|
+
elements: 'flows',
|
|
51
|
+
integrationSpecific: true,
|
|
52
|
+
},
|
|
53
|
+
[WorkspaceElementType.Customer]: {
|
|
54
|
+
element: 'customer',
|
|
55
|
+
elements: 'customers',
|
|
56
|
+
},
|
|
57
|
+
[WorkspaceElementType.FlowInstance]: {
|
|
58
|
+
element: 'flowInstance',
|
|
59
|
+
elements: 'flowInstances',
|
|
60
|
+
},
|
|
61
|
+
[WorkspaceElementType.FlowRun]: {
|
|
62
|
+
element: 'flowRun',
|
|
63
|
+
elements: 'flowRuns',
|
|
64
|
+
},
|
|
65
|
+
[WorkspaceElementType.Scenario]: {
|
|
66
|
+
element: 'scenario',
|
|
67
|
+
elements: 'scenarios',
|
|
68
|
+
},
|
|
69
|
+
[WorkspaceElementType.ActionInstance]: {
|
|
70
|
+
element: 'actionInstance',
|
|
71
|
+
elements: 'actionInstances',
|
|
72
|
+
},
|
|
73
|
+
[WorkspaceElementType.Connection]: {
|
|
74
|
+
element: 'connection',
|
|
75
|
+
elements: 'connections',
|
|
76
|
+
},
|
|
77
|
+
[WorkspaceElementType.FieldMappingInstance]: {
|
|
78
|
+
element: 'fieldMappingInstance',
|
|
79
|
+
elements: 'fieldMappingInstances',
|
|
80
|
+
},
|
|
81
|
+
[WorkspaceElementType.DataSourceInstance]: {
|
|
82
|
+
element: 'dataSourceInstance',
|
|
83
|
+
elements: 'dataSourceInstances',
|
|
84
|
+
},
|
|
85
|
+
[WorkspaceElementType.DataLinkTableInstance]: {
|
|
86
|
+
element: 'dataLinkTableInstance',
|
|
87
|
+
elements: 'dataLinkTableInstances',
|
|
88
|
+
},
|
|
89
|
+
[WorkspaceElementType.AppEventSubscription]: {
|
|
90
|
+
element: 'appEventSubscription',
|
|
91
|
+
elements: 'appEventSubscriptions',
|
|
92
|
+
},
|
|
93
|
+
[WorkspaceElementType.AppDataSchemaInstance]: {
|
|
94
|
+
element: 'appDataSchemaInstance',
|
|
95
|
+
elements: 'appDataSchemaInstances',
|
|
96
|
+
},
|
|
97
|
+
[WorkspaceElementType.ExternalEventSubscription]: {
|
|
98
|
+
element: 'externalEventSubscription',
|
|
99
|
+
elements: 'externalEventSubscriptions',
|
|
100
|
+
},
|
|
101
|
+
[WorkspaceElementType.ExternalEventLogRecord]: {
|
|
102
|
+
element: 'externalEventLogRecord',
|
|
103
|
+
elements: 'externalEventLogRecords',
|
|
104
|
+
},
|
|
105
|
+
[WorkspaceElementType.ExternalEventPull]: {
|
|
106
|
+
element: 'externalEventPull',
|
|
107
|
+
elements: 'externalEventPulls',
|
|
108
|
+
},
|
|
109
|
+
[WorkspaceElementType.DataCollection]: {
|
|
110
|
+
element: 'dataCollection',
|
|
111
|
+
elements: 'dataCollections',
|
|
112
|
+
},
|
|
113
|
+
[WorkspaceElementType.Screen]: {
|
|
114
|
+
element: 'screen',
|
|
115
|
+
elements: 'screens',
|
|
116
|
+
},
|
|
117
|
+
[WorkspaceElementType.ActionRunLogRecord]: {
|
|
118
|
+
element: 'actionRunLogRecord',
|
|
119
|
+
elements: 'actionRunLogRecords',
|
|
120
|
+
},
|
|
121
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { WorkspaceElementSpecs, WorkspaceElementType } from '@integration-app/sdk'
|
|
4
|
+
|
|
5
|
+
export interface WorkspaceElementKeyReference {
|
|
6
|
+
readonly type: WorkspaceElementType
|
|
7
|
+
readonly key: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getTypeAndKeyFromPath(relativePath: string): WorkspaceElementKeyReference | undefined {
|
|
11
|
+
// NOTE: skip connector files for now
|
|
12
|
+
if (isConnectorFile(relativePath)) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (isWorkspaceElementFile(relativePath)) {
|
|
17
|
+
return extractWorkspaceElementKeyReference(relativePath)
|
|
18
|
+
}
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractWorkspaceElementKeyReference(relativePath: string): WorkspaceElementKeyReference | undefined {
|
|
23
|
+
const elementTypes = Object.values(WorkspaceElementSpecs).map((spec) => spec.apiPath)
|
|
24
|
+
|
|
25
|
+
// <element-type>/<element-key>/spec.yml or .yaml
|
|
26
|
+
const universalElementPattern = new RegExp(
|
|
27
|
+
`^(?<elementTypePlural>${elementTypes.join('|')})\/(?<elementKey>[^\/]+)\/spec\.y[a]*ml$`,
|
|
28
|
+
)
|
|
29
|
+
const universalMatch = relativePath.match(universalElementPattern)
|
|
30
|
+
if (universalMatch && universalMatch.groups) {
|
|
31
|
+
const { elementTypePlural, elementKey } = universalMatch.groups
|
|
32
|
+
const type = getWorkspaceElementTypeFromFolderName(elementTypePlural)
|
|
33
|
+
if (type) {
|
|
34
|
+
return { key: elementKey, type }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// integrations/<integration-key>/<element-type>/<element-key>/spec.yml or .yaml
|
|
39
|
+
const integratedElementPattern = new RegExp(
|
|
40
|
+
`^integrations\/(?<integrationKey>[^\/]+)\/(?<elementTypePlural>${elementTypes.join('|')})\/(?<elementKey>[^\/]+)\/spec\.y[a]*ml$`,
|
|
41
|
+
)
|
|
42
|
+
const integratedMatch = relativePath.match(integratedElementPattern)
|
|
43
|
+
if (integratedMatch && integratedMatch.groups) {
|
|
44
|
+
const { elementTypePlural, elementKey } = integratedMatch.groups
|
|
45
|
+
const type = getWorkspaceElementTypeFromFolderName(elementTypePlural)
|
|
46
|
+
if (type) {
|
|
47
|
+
return { key: elementKey, type }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isWorkspaceElementFile(relativePath: string): boolean {
|
|
55
|
+
return relativePath.endsWith('.yml') || relativePath.endsWith('.yaml')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isConnectorFile(relativePath: string): boolean {
|
|
59
|
+
const [rootDir] = relativePath.split(path.sep)
|
|
60
|
+
return rootDir === WorkspaceElementSpecs[WorkspaceElementType.Connector].apiPath
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getWorkspaceElementTypeFromFolderName(folderName: string): WorkspaceElementType | undefined {
|
|
64
|
+
return Object.values(WorkspaceElementType).find((type) => getWorkspaceElementTypeFolderName(type) === folderName)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getWorkspaceElementTypeFolderName(type: WorkspaceElementType): string {
|
|
68
|
+
return WorkspaceElementSpecs[type].apiPath
|
|
69
|
+
}
|