@exellix/graphs-studio-catalog 0.1.0

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/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@exellix/graphs-studio-catalog",
3
+ "version": "0.1.0",
4
+ "description": "Graphs Studio library UI helpers (list, status, soft-delete, job types, branches). Not @exellix/catalox-graphs persistence.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20.19.0 || >=22.12.0"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public",
11
+ "registry": "https://registry.npmjs.org/"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+ssh://git@github.com/exellix/graphs-studio.git",
16
+ "directory": "packages/graphs-studio-catalog"
17
+ },
18
+ "main": "./src/index.js",
19
+ "exports": {
20
+ ".": "./src/index.js"
21
+ },
22
+ "files": [
23
+ "src"
24
+ ],
25
+ "scripts": {
26
+ "prepublishOnly": "node --test tests/graphs-studio-catalog.test.mjs"
27
+ },
28
+ "dependencies": {
29
+ "@x12i/graphenix-authoring-format": "^2.11.0",
30
+ "@x12i/graphenix-executable-contracts": "^2.11.0"
31
+ }
32
+ }
@@ -0,0 +1,8 @@
1
+ export const GRAPH_CATALOG_IDS = {
2
+ graphs: 'graphs',
3
+ graphVersions: 'graph-versions',
4
+ graphTemplates: 'graph-templates',
5
+ jobTypes: 'job-types',
6
+ graphJobTypeRelations: 'graph-job-type-relations',
7
+ graphBranches: 'graph-branches',
8
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Browser-side Catalox graph management API (calls dev BFF /api/catalox/*).
3
+ */
4
+
5
+ const BASE = '/api/catalox';
6
+
7
+ async function fetchJson(url, init) {
8
+ const res = await fetch(url, init);
9
+ const body = await res.json().catch(() => ({}));
10
+ if (!res.ok) {
11
+ return { ok: false, error: body.error ?? res.statusText };
12
+ }
13
+ return body;
14
+ }
15
+
16
+ /**
17
+ * @param {{ limit?: number, status?: string, jobTypeId?: string, includeDeleted?: boolean }} [options]
18
+ */
19
+ export async function listGraphsForLibrary(options = {}) {
20
+ const params = new URLSearchParams();
21
+ if (options.limit) params.set('limit', String(options.limit));
22
+ if (options.status) params.set('status', options.status);
23
+ if (options.jobTypeId) params.set('jobTypeId', options.jobTypeId);
24
+ if (options.includeDeleted) params.set('includeDeleted', '1');
25
+ const qs = params.toString();
26
+ return fetchJson(`${BASE}/graphs${qs ? `?${qs}` : ''}`);
27
+ }
28
+
29
+ /** @param {string} graphId @param {string} title */
30
+ export async function renameGraphInCatalox(graphId, title) {
31
+ const load = await fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`);
32
+ if (!load.ok || !load.item) return load;
33
+ const data = { ...load.item.data, title, updatedAt: new Date().toISOString() };
34
+ return fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`, {
35
+ method: 'PUT',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({ data }),
38
+ });
39
+ }
40
+
41
+ /** @param {string} graphId @param {string} status */
42
+ export async function updateGraphStatusInCatalox(graphId, status) {
43
+ const load = await fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`);
44
+ if (!load.ok || !load.item) return load;
45
+ const data = { ...load.item.data, status, updatedAt: new Date().toISOString() };
46
+ return fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`, {
47
+ method: 'PUT',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ data }),
50
+ });
51
+ }
52
+
53
+ /** @param {string} graphId */
54
+ export async function softDeleteGraphInCatalox(graphId) {
55
+ const load = await fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`);
56
+ if (!load.ok || !load.item) return load;
57
+ const now = new Date().toISOString();
58
+ const data = {
59
+ ...load.item.data,
60
+ status: 'deleted',
61
+ deletedAt: now,
62
+ updatedAt: now,
63
+ };
64
+ return fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`, {
65
+ method: 'PUT',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ data }),
68
+ });
69
+ }
70
+
71
+ /** @param {string} graphId */
72
+ export async function restoreGraphInCatalox(graphId) {
73
+ const load = await fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`);
74
+ if (!load.ok || !load.item) return load;
75
+ const data = { ...load.item.data, status: 'draft', updatedAt: new Date().toISOString() };
76
+ delete data.deletedAt;
77
+ delete data.deletedBy;
78
+ return fetchJson(`${BASE}/graphs/${encodeURIComponent(graphId)}`, {
79
+ method: 'PUT',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ data }),
82
+ });
83
+ }
84
+
85
+ export async function listJobTypesFromCatalox() {
86
+ return fetchJson(`${BASE}/catalogs/job-types/items?limit=500`);
87
+ }
88
+
89
+ /** @param {Record<string, unknown>} jobTypeData */
90
+ export async function upsertJobTypeInCatalox(jobTypeData) {
91
+ const id = typeof jobTypeData.jobTypeId === 'string' ? jobTypeData.jobTypeId : '';
92
+ if (!id) return { ok: false, error: 'jobTypeId required' };
93
+ return fetchJson(`${BASE}/catalogs/job-types/items/${encodeURIComponent(id)}`, {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ itemId: id, data: jobTypeData }),
97
+ });
98
+ }
99
+
100
+ /** @param {string} jobTypeId */
101
+ export async function deleteJobTypeFromCatalox(jobTypeId) {
102
+ return fetchJson(`${BASE}/catalogs/job-types/items/${encodeURIComponent(jobTypeId)}`, {
103
+ method: 'DELETE',
104
+ });
105
+ }
106
+
107
+ export async function listGraphBranchesFromCatalox(graphId) {
108
+ const params = graphId ? `?limit=500&q=${encodeURIComponent(graphId)}` : '?limit=500';
109
+ return fetchJson(`${BASE}/catalogs/graph-branches/items${params}`);
110
+ }
111
+
112
+ /** @param {Record<string, unknown>} branchData */
113
+ export async function upsertGraphBranchInCatalox(branchData) {
114
+ const id = typeof branchData.branchId === 'string' ? branchData.branchId : '';
115
+ if (!id) return { ok: false, error: 'branchId required' };
116
+ return fetchJson(`${BASE}/catalogs/graph-branches/items/${encodeURIComponent(id)}`, {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify({ itemId: id, data: branchData }),
120
+ });
121
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @typedef {Object} GraphBranchRecord
3
+ * @property {string} branchId
4
+ * @property {string} graphId
5
+ * @property {string} branchName
6
+ * @property {string} [parentBranchId]
7
+ * @property {string} [headVersionId]
8
+ * @property {'active' | 'merged' | 'archived'} status
9
+ * @property {string} [createdAt]
10
+ * @property {string} [createdBy]
11
+ */
12
+
13
+ /**
14
+ * @param {Record<string, unknown>} raw
15
+ * @returns {GraphBranchRecord | null}
16
+ */
17
+ export function normalizeBranchRecord(raw) {
18
+ if (!raw || typeof raw !== 'object') return null;
19
+ const branchId = typeof raw.branchId === 'string' ? raw.branchId.trim() : '';
20
+ const graphId = typeof raw.graphId === 'string' ? raw.graphId.trim() : '';
21
+ const branchName = typeof raw.branchName === 'string' ? raw.branchName.trim() : '';
22
+ if (!branchId || !graphId || !branchName) return null;
23
+ const statusRaw = typeof raw.status === 'string' ? raw.status : 'active';
24
+ const status = statusRaw === 'merged' || statusRaw === 'archived' ? statusRaw : 'active';
25
+ return {
26
+ branchId,
27
+ graphId,
28
+ branchName,
29
+ parentBranchId:
30
+ typeof raw.parentBranchId === 'string' ? raw.parentBranchId : undefined,
31
+ headVersionId:
32
+ typeof raw.headVersionId === 'string' ? raw.headVersionId : undefined,
33
+ status,
34
+ createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : undefined,
35
+ createdBy: typeof raw.createdBy === 'string' ? raw.createdBy : undefined,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Fork a branch from a parent.
41
+ * @param {{ graphId: string, branchName: string, parentBranchId?: string, headVersionId?: string, actor?: string }} input
42
+ */
43
+ export function createBranchRecord(input) {
44
+ const branchId = `${input.graphId}::${input.branchName.replace(/\s+/g, '-').toLowerCase()}`;
45
+ return normalizeBranchRecord({
46
+ branchId,
47
+ graphId: input.graphId,
48
+ branchName: input.branchName,
49
+ parentBranchId: input.parentBranchId,
50
+ headVersionId: input.headVersionId,
51
+ status: 'active',
52
+ createdAt: new Date().toISOString(),
53
+ createdBy: input.actor,
54
+ });
55
+ }
@@ -0,0 +1,110 @@
1
+ import { normalizeGraphPublishStatus } from './graphStatus.js';
2
+
3
+ /**
4
+ * @typedef {Object} GraphListRow
5
+ * @property {string} id
6
+ * @property {string} graphId
7
+ * @property {string} title
8
+ * @property {string} status
9
+ * @property {string} [jobTypeId]
10
+ * @property {string} [branchId]
11
+ * @property {string} [branchName]
12
+ * @property {string} [createdAt]
13
+ * @property {string} [updatedAt]
14
+ * @property {string} [createdBy]
15
+ * @property {string} [modifiedBy]
16
+ * @property {string} [deletedAt]
17
+ */
18
+
19
+ /**
20
+ * Normalize a Catalox graphs catalog list item into a library row.
21
+ * @param {Record<string, unknown>} item
22
+ * @returns {GraphListRow | null}
23
+ */
24
+ export function normalizeGraphListItem(item) {
25
+ if (!item || typeof item !== 'object') return null;
26
+ const graphId =
27
+ (typeof item.itemId === 'string' && item.itemId.trim()) ||
28
+ (typeof item.graphId === 'string' && item.graphId.trim()) ||
29
+ '';
30
+ if (!graphId) return null;
31
+ const data = item.data && typeof item.data === 'object' ? item.data : item;
32
+ const title =
33
+ (typeof data.title === 'string' && data.title.trim()) ||
34
+ (typeof data.name === 'string' && data.name.trim()) ||
35
+ graphId;
36
+ const graph = data.graph && typeof data.graph === 'object' ? data.graph : null;
37
+ const md =
38
+ graph?.metadata && typeof graph.metadata === 'object' ? graph.metadata : {};
39
+ const pipeline =
40
+ md.jobPipeline && typeof md.jobPipeline === 'object' ? md.jobPipeline : {};
41
+ const jobTypeId =
42
+ (typeof data.jobTypeId === 'string' && data.jobTypeId) ||
43
+ (typeof pipeline.jobTypeId === 'string' && pipeline.jobTypeId) ||
44
+ undefined;
45
+
46
+ return {
47
+ id: graphId,
48
+ graphId,
49
+ title,
50
+ status: normalizeGraphPublishStatus(data.status ?? md.status),
51
+ jobTypeId,
52
+ branchId: typeof data.branchId === 'string' ? data.branchId : undefined,
53
+ branchName: typeof data.branchName === 'string' ? data.branchName : 'main',
54
+ createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,
55
+ updatedAt:
56
+ typeof data.updatedAt === 'string'
57
+ ? data.updatedAt
58
+ : typeof item.updatedAt === 'string'
59
+ ? item.updatedAt
60
+ : undefined,
61
+ createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
62
+ modifiedBy: typeof data.modifiedBy === 'string' ? data.modifiedBy : undefined,
63
+ deletedAt: typeof data.deletedAt === 'string' ? data.deletedAt : undefined,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * @param {GraphListRow[]} rows
69
+ * @param {{ search?: string, status?: string, jobTypeId?: string, deletedOnly?: boolean }} [filters]
70
+ */
71
+ export function filterGraphListRows(rows, filters = {}) {
72
+ let list = [...rows];
73
+ if (filters.deletedOnly) {
74
+ list = list.filter((r) => r.status === 'deleted' || r.deletedAt);
75
+ } else {
76
+ list = list.filter((r) => r.status !== 'deleted' && !r.deletedAt);
77
+ }
78
+ if (filters.status && filters.status !== 'all') {
79
+ list = list.filter((r) => r.status === filters.status);
80
+ }
81
+ if (filters.jobTypeId && filters.jobTypeId !== 'all') {
82
+ list = list.filter((r) => r.jobTypeId === filters.jobTypeId);
83
+ }
84
+ const q = filters.search?.trim().toLowerCase();
85
+ if (q) {
86
+ list = list.filter(
87
+ (r) =>
88
+ r.title.toLowerCase().includes(q) ||
89
+ r.graphId.toLowerCase().includes(q) ||
90
+ (r.jobTypeId ?? '').toLowerCase().includes(q),
91
+ );
92
+ }
93
+ return list;
94
+ }
95
+
96
+ /**
97
+ * @param {GraphListRow[]} rows
98
+ * @param {string} sortKey
99
+ * @param {'asc' | 'desc'} sortDir
100
+ */
101
+ export function sortGraphListRows(rows, sortKey = 'updatedAt', sortDir = 'desc') {
102
+ const copy = [...rows];
103
+ copy.sort((a, b) => {
104
+ const av = a[sortKey] ?? '';
105
+ const bv = b[sortKey] ?? '';
106
+ const cmp = String(av).localeCompare(String(bv));
107
+ return sortDir === 'asc' ? cmp : -cmp;
108
+ });
109
+ return copy;
110
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Soft-delete / recycle bin helpers (interim until Catalox native soft-delete).
3
+ */
4
+
5
+ /**
6
+ * @param {Record<string, unknown>} projectData
7
+ * @param {string} [actor]
8
+ * @returns {Record<string, unknown>}
9
+ */
10
+ export function markGraphSoftDeleted(projectData, actor) {
11
+ const now = new Date().toISOString();
12
+ return {
13
+ ...projectData,
14
+ status: 'deleted',
15
+ deletedAt: now,
16
+ deletedBy: actor ?? projectData.deletedBy,
17
+ updatedAt: now,
18
+ modifiedBy: actor ?? projectData.modifiedBy,
19
+ };
20
+ }
21
+
22
+ /**
23
+ * @param {Record<string, unknown>} projectData
24
+ * @param {string} [actor]
25
+ * @returns {Record<string, unknown>}
26
+ */
27
+ export function restoreSoftDeletedGraph(projectData, actor) {
28
+ const next = { ...projectData };
29
+ delete next.deletedAt;
30
+ delete next.deletedBy;
31
+ next.status = 'draft';
32
+ next.updatedAt = new Date().toISOString();
33
+ if (actor) next.modifiedBy = actor;
34
+ return next;
35
+ }
36
+
37
+ /** @param {Record<string, unknown>} projectData */
38
+ export function isGraphSoftDeleted(projectData) {
39
+ if (!projectData || typeof projectData !== 'object') return false;
40
+ return projectData.status === 'deleted' || Boolean(projectData.deletedAt);
41
+ }
@@ -0,0 +1,25 @@
1
+ /** Catalox catalog ids owned or extended by graphs-studio graph management. */
2
+ export const GRAPH_CATALOG_IDS = {
3
+ graphs: 'graphs',
4
+ graphVersions: 'graph-versions',
5
+ graphTemplates: 'graph-templates',
6
+ jobTypes: 'job-types',
7
+ graphJobTypeRelations: 'graph-job-type-relations',
8
+ graphBranches: 'graph-branches',
9
+ };
10
+
11
+ /** @typedef {'draft' | 'published' | 'deleted'} GraphPublishStatus */
12
+
13
+ export const GRAPH_PUBLISH_STATUSES = /** @type {const} */ (['draft', 'published', 'deleted']);
14
+
15
+ /** @param {unknown} value */
16
+ export function normalizeGraphPublishStatus(value) {
17
+ const s = typeof value === 'string' ? value.trim() : '';
18
+ if (s === 'published' || s === 'deleted') return s;
19
+ return 'draft';
20
+ }
21
+
22
+ /** @param {unknown} value */
23
+ export function isValidGraphPublishStatus(value) {
24
+ return GRAPH_PUBLISH_STATUSES.includes(normalizeGraphPublishStatus(value));
25
+ }
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ export { GRAPH_CATALOG_IDS } from './catalogIds.js';
2
+ export {
3
+ GRAPH_PUBLISH_STATUSES,
4
+ normalizeGraphPublishStatus,
5
+ isValidGraphPublishStatus,
6
+ } from './graphStatus.js';
7
+ export {
8
+ normalizeGraphListItem,
9
+ filterGraphListRows,
10
+ sortGraphListRows,
11
+ } from './graphList.js';
12
+ export {
13
+ markGraphSoftDeleted,
14
+ restoreSoftDeletedGraph,
15
+ isGraphSoftDeleted,
16
+ } from './graphRecycleBin.js';
17
+ export { normalizeJobTypeRecord, jobTypeToCatalogPayload } from './jobTypesCatalog.js';
18
+ export { normalizeBranchRecord, createBranchRecord } from './graphBranches.js';
19
+ export {
20
+ listGraphsForLibrary,
21
+ renameGraphInCatalox,
22
+ updateGraphStatusInCatalox,
23
+ softDeleteGraphInCatalox,
24
+ restoreGraphInCatalox,
25
+ listJobTypesFromCatalox,
26
+ upsertJobTypeInCatalox,
27
+ deleteJobTypeFromCatalox,
28
+ listGraphBranchesFromCatalox,
29
+ upsertGraphBranchInCatalox,
30
+ } from './cataloxGraphManagementApi.js';
@@ -0,0 +1,49 @@
1
+ import { GRAPH_CATALOG_IDS } from './catalogIds.js';
2
+
3
+ /**
4
+ * @typedef {{ jobTypeId: string, displayName: string, description?: string, createdAt?: string, updatedAt?: string, createdBy?: string, modifiedBy?: string }} JobTypeRecord
5
+ */
6
+
7
+ /**
8
+ * @param {Record<string, unknown>} raw
9
+ * @returns {JobTypeRecord | null}
10
+ */
11
+ export function normalizeJobTypeRecord(raw) {
12
+ if (!raw || typeof raw !== 'object') return null;
13
+ const jobTypeId = typeof raw.jobTypeId === 'string' ? raw.jobTypeId.trim() : '';
14
+ if (!jobTypeId) return null;
15
+ const displayName =
16
+ typeof raw.displayName === 'string' && raw.displayName.trim()
17
+ ? raw.displayName.trim()
18
+ : jobTypeId;
19
+ return {
20
+ jobTypeId,
21
+ displayName,
22
+ description: typeof raw.description === 'string' ? raw.description : undefined,
23
+ createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : undefined,
24
+ updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : undefined,
25
+ createdBy: typeof raw.createdBy === 'string' ? raw.createdBy : undefined,
26
+ modifiedBy: typeof raw.modifiedBy === 'string' ? raw.modifiedBy : undefined,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * @param {JobTypeRecord} jobType
32
+ * @param {string} [actor]
33
+ */
34
+ export function jobTypeToCatalogPayload(jobType, actor) {
35
+ const now = new Date().toISOString();
36
+ return {
37
+ catalogId: GRAPH_CATALOG_IDS.jobTypes,
38
+ itemId: jobType.jobTypeId,
39
+ data: {
40
+ ...jobType,
41
+ updatedAt: now,
42
+ modifiedBy: actor ?? jobType.modifiedBy,
43
+ createdAt: jobType.createdAt ?? now,
44
+ createdBy: jobType.createdBy ?? actor,
45
+ },
46
+ };
47
+ }
48
+
49
+ export { GRAPH_CATALOG_IDS };