Add plugin platform with modular backend and frontend registry.

Split graph/import/meta into Django apps, add API v1 with OpenAPI and pytest, and introduce plugin registry with the tags reference plugin on both FE and BE.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 09:35:51 +03:00
co-authored by Cursor
parent b694a596b8
commit cafc7335ad
77 changed files with 1801 additions and 490 deletions
@@ -0,0 +1,13 @@
import { tagRepository } from '../../plugins/tags/tagRepository.local'
/**
* Persist plugin form payload after contact create/update.
* @param {string|number} contactId
* @param {{ tags?: string[] }} pluginPayload
*/
export async function saveContactPluginData(contactId, pluginPayload = {}) {
if (!contactId || !pluginPayload) return
if (Array.isArray(pluginPayload.tags)) {
await tagRepository.setContactTags(contactId, pluginPayload.tags)
}
}
@@ -0,0 +1,57 @@
import { getGraphBundle } from '../usecases/graph'
import { getRelationTypes } from '../../infrastructure/repositories/repositoryFactory'
import { isLocalMode } from '../../infrastructure/config/dataMode'
import api from '../../api'
import { applyGraphExtensions } from '../../core/pluginRegistry'
/**
* Unified graph data access for GraphView and NetworkMapView.
*/
export async function fetchGraphBundle({ mapId = null } = {}) {
let bundle
if (isLocalMode()) {
bundle = await getGraphBundle({ mapId })
} else {
const endpoint = mapId
? `/network-map-graph/?map_id=${encodeURIComponent(mapId)}`
: '/graph/'
const [gRes, rtRes] = await Promise.all([
api.get(endpoint),
api.get('/relation-types/'),
])
bundle = {
nodes: gRes.data.nodes || [],
edges: gRes.data.edges || [],
relationTypes: rtRes.data || [],
}
}
return {
...bundle,
nodes: (bundle.nodes || []).map((n) => applyGraphExtensions('node', n)),
edges: (bundle.edges || []).map((e) => applyGraphExtensions('edge', e)),
}
}
export async function fetchGraphBundleFromStore(store, { mapId = null } = {}) {
const { buildGraphFromStore } = await import('../usecases/graph')
if (!store.contacts.length) await store.fetchContacts()
if (!store.relations.length) await store.fetchRelations()
const relationTypes = store.relationTypes.length
? store.relationTypes
: await store.fetchRelationTypes()
const bundle = buildGraphFromStore(store.contacts, store.relations, relationTypes)
if (mapId) {
return fetchGraphBundle({ mapId })
}
return {
...bundle,
nodes: bundle.nodes.map((n) => applyGraphExtensions('node', n)),
edges: bundle.edges.map((e) => applyGraphExtensions('edge', e)),
}
}
export async function fetchRelationTypes() {
return getRelationTypes()
}
@@ -0,0 +1,26 @@
import { describe, it, expect, vi } from 'vitest'
vi.mock('../../infrastructure/config/dataMode', () => ({
isLocalMode: () => true,
}))
vi.mock('../../core/pluginRegistry', () => ({
applyGraphExtensions: (_kind, payload) => payload,
}))
vi.mock('../usecases/graph', () => ({
getGraphBundle: vi.fn(async () => ({
nodes: [{ id: '1', label: 'A' }],
edges: [],
relationTypes: [],
})),
}))
describe('graphDataService', () => {
it('fetchGraphBundle returns nodes from local use case', async () => {
const { fetchGraphBundle } = await import('./graphDataService.js')
const bundle = await fetchGraphBundle()
expect(bundle.nodes).toHaveLength(1)
expect(bundle.nodes[0].label).toBe('A')
})
})