datajunction-ui
Version:
DataJunction UI
1,655 lines (1,528 loc) • 114 kB
JSX
import { DataJunctionAPI } from '../DJService';
import { mocks } from '../../../mocks/mockNodes';
describe('DataJunctionAPI', () => {
let originalEventSource;
const DJ_URL = 'http://localhost:8000';
process.env.REACT_APP_DJ_URL = DJ_URL;
beforeEach(() => {
fetch.resetMocks();
originalEventSource = global.EventSource;
global.EventSource = vi.fn();
});
afterEach(() => {
global.EventSource = originalEventSource;
});
it('calls whoami correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.whoami();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/whoami/`, {
credentials: 'include',
});
});
it('calls logout correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.logout();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/logout/`, {
credentials: 'include',
method: 'POST',
});
});
it('calls catalogs correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.catalogs();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/catalogs`, {
credentials: 'include',
});
});
it('calls engines correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.engines();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/engines`, {
credentials: 'include',
});
});
it('calls node correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.mockMetricNode));
const nodeData = await DataJunctionAPI.node(mocks.mockMetricNode.name);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${mocks.mockMetricNode.name}/`,
{
credentials: 'include',
},
);
expect(nodeData.name).toEqual(mocks.mockMetricNode.name);
expect(nodeData.display_name).toEqual(mocks.mockMetricNode.display_name);
expect(nodeData.type).toEqual(mocks.mockMetricNode.type);
expect(nodeData.primary_key).toEqual([]);
});
it('calls nodeDetails correctly', async () => {
fetch.mockResponseOnce(JSON.stringify([mocks.mockMetricNode]));
const nodeData = await DataJunctionAPI.nodeDetails();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/details/`, {
credentials: 'include',
});
expect(nodeData).toEqual([mocks.mockMetricNode]);
});
it('calls validate correctly', async () => {
fetch.mockResponseOnce(JSON.stringify([mocks.mockMetricNode]));
await DataJunctionAPI.validateNode(
'metric',
'default.num_repair_orders',
'aa',
'desc',
'select 1',
);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/validate`, {
credentials: 'include',
body: JSON.stringify({
name: 'default.num_repair_orders',
display_name: 'aa',
description: 'desc',
query: 'select 1',
type: 'metric',
mode: 'published',
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});
});
it('calls registerTable correctly', async () => {
fetch.mockResponseOnce(JSON.stringify([mocks.mockMetricNode]));
await DataJunctionAPI.registerTable('default', 'xyz', 'abc');
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/register/table/default/xyz/abc`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
},
);
});
it('node with errors are handled', async () => {
const mockNotFound = { message: 'node not found' };
fetch.mockResponseOnce(JSON.stringify(mockNotFound));
const nodeData = await DataJunctionAPI.node(mocks.mockMetricNode.name);
expect(nodeData.message).toEqual(mockNotFound.message);
});
it('calls nodes correctly', async () => {
const prefix = 'samplePrefix';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.nodes(prefix);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/?prefix=${prefix}`, {
credentials: 'include',
});
});
it('calls nodesWithType correctly', async () => {
const nodeType = 'transform';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.nodesWithType(nodeType);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/?node_type=${nodeType}`,
{
credentials: 'include',
},
);
});
it('calls createNode correctly', async () => {
const sampleArgs = [
'type',
'name',
'display_name',
'description',
'query',
'mode',
'namespace',
'primary_key',
undefined, // metric_direction
undefined, // metric_unit
undefined, // required_dimensions
{ key: 'value' }, // custom_metadata
];
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.createNode(...sampleArgs);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${sampleArgs[0]}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: sampleArgs[1],
display_name: sampleArgs[2],
description: sampleArgs[3],
query: sampleArgs[4],
mode: sampleArgs[5],
namespace: sampleArgs[6],
primary_key: sampleArgs[7],
metric_metadata: null,
required_dimensions: undefined,
custom_metadata: { key: 'value' },
}),
credentials: 'include',
});
});
it('calls patchNode correctly', async () => {
const sampleArgs = [
'name',
'display_name',
'description',
'query',
'mode',
'primary_key',
'neutral',
'',
null, // significant_digits
undefined, // required_dimensions
undefined, // owners
{ key: 'value' }, // custom_metadata
];
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.patchNode(...sampleArgs);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${sampleArgs[0]}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
display_name: sampleArgs[1],
description: sampleArgs[2],
query: sampleArgs[3],
mode: sampleArgs[4],
primary_key: sampleArgs[5],
metric_metadata: {
direction: 'neutral',
unit: '',
significant_digits: null,
},
required_dimensions: undefined,
owners: undefined,
custom_metadata: { key: 'value' },
}),
credentials: 'include',
});
fetch.mockResponseOnce([
JSON.stringify({ message: 'Update failed' }),
{ status: 200 },
]);
const response = await DataJunctionAPI.patchNode(...sampleArgs);
expect(response).toEqual({
json: { message: 'Update failed' },
status: 500,
});
});
it('calls createCube correctly', async () => {
const sampleArgs = [
'default.node_name',
'Node Display Name',
'Some readable description',
'draft',
['default.num_repair_orders'],
[
'default.date_dim.year',
'default.date_dim.month',
'default.date_dim.day',
],
[],
];
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.createCube(...sampleArgs);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/cube`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: sampleArgs[0],
display_name: sampleArgs[1],
description: sampleArgs[2],
metrics: sampleArgs[4],
dimensions: sampleArgs[5],
filters: sampleArgs[6],
mode: sampleArgs[3],
}),
credentials: 'include',
});
});
it('calls patchCube correctly', async () => {
const sampleArgs = [
'default.node_name',
'Node Display Name',
'Some readable description',
'draft',
['default.num_repair_orders'],
[
'default.date_dim.year',
'default.date_dim.month',
'default.date_dim.day',
],
[],
];
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.patchCube(...sampleArgs);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/default.node_name`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
display_name: sampleArgs[1],
description: sampleArgs[2],
metrics: sampleArgs[4],
dimensions: sampleArgs[5],
filters: sampleArgs[6],
mode: sampleArgs[3],
}),
credentials: 'include',
});
});
it('calls upstreams correctly', async () => {
const nodeName = 'sampleNode';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.upstreams(nodeName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/upstream/`,
{
credentials: 'include',
},
);
});
it('calls downstreams correctly', async () => {
const nodeName = 'sampleNode';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.downstreams(nodeName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/downstream/`,
{
credentials: 'include',
},
);
});
it('calls upstreamsGQL correctly with single node', async () => {
const nodeName = 'sampleNode';
fetch.mockResponseOnce(
JSON.stringify({
data: { upstreamNodes: [{ name: 'upstream1', type: 'SOURCE' }] },
}),
);
const result = await DataJunctionAPI.upstreamsGQL(nodeName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/graphql`,
expect.objectContaining({
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
}),
);
expect(result).toEqual([{ name: 'upstream1', type: 'SOURCE' }]);
});
it('calls upstreamsGQL correctly with multiple nodes', async () => {
const nodeNames = ['node1', 'node2'];
fetch.mockResponseOnce(
JSON.stringify({
data: {
upstreamNodes: [
{ name: 'upstream1', type: 'SOURCE' },
{ name: 'upstream2', type: 'TRANSFORM' },
],
},
}),
);
const result = await DataJunctionAPI.upstreamsGQL(nodeNames);
expect(result).toEqual([
{ name: 'upstream1', type: 'SOURCE' },
{ name: 'upstream2', type: 'TRANSFORM' },
]);
});
it('calls downstreamsGQL correctly with single node', async () => {
const nodeName = 'sampleNode';
fetch.mockResponseOnce(
JSON.stringify({
data: { downstreamNodes: [{ name: 'downstream1', type: 'METRIC' }] },
}),
);
const result = await DataJunctionAPI.downstreamsGQL(nodeName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/graphql`,
expect.objectContaining({
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
}),
);
expect(result).toEqual([{ name: 'downstream1', type: 'METRIC' }]);
});
it('calls downstreamsGQL correctly with multiple nodes', async () => {
const nodeNames = ['node1', 'node2'];
fetch.mockResponseOnce(
JSON.stringify({
data: {
downstreamNodes: [
{ name: 'downstream1', type: 'METRIC' },
{ name: 'downstream2', type: 'CUBE' },
],
},
}),
);
const result = await DataJunctionAPI.downstreamsGQL(nodeNames);
expect(result).toEqual([
{ name: 'downstream1', type: 'METRIC' },
{ name: 'downstream2', type: 'CUBE' },
]);
});
it('calls node_dag correctly', async () => {
const nodeName = 'sampleNode';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.node_dag(nodeName);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}/dag/`, {
credentials: 'include',
});
});
it('calls node_lineage correctly', async () => {
const nodeName = 'sampleNode';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.node_lineage(nodeName);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}/lineage/`, {
credentials: 'include',
});
});
it('calls metric correctly', async () => {
const metricName = 'sampleMetric';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.metric(metricName);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/metrics/${metricName}/`, {
credentials: 'include',
});
});
it('calls metrics correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.metrics('');
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/metrics/`, {
credentials: 'include',
});
});
it('calls listMetricMetadata correctly', async () => {
const nodeType = 'transform';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.listMetricMetadata();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/metrics/metadata`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
});
it('calls namespaces correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.namespaces();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/`, {
credentials: 'include',
});
});
it('calls clientCode correctly', async () => {
const name = 'sampleName';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.clientCode(name);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/datajunction-clients/python/new_node/${name}`,
{
credentials: 'include',
},
);
});
it('calls cube correctly', async () => {
const name = 'sampleCube';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.cube(name);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/cubes/${name}/`, {
credentials: 'include',
});
});
it('calls commonDimensions correctly', async () => {
const metrics = ['metric1', 'metric2'];
const query = metrics.map(m => `metric=${m}`).join('&');
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.commonDimensions(metrics);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/metrics/common/dimensions/?${query}`,
{
credentials: 'include',
},
);
});
it('calls history correctly', async () => {
const type = 'sampleType';
const name = 'sampleName';
const offset = 10;
const limit = 100;
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.history(type, name, offset, limit);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/history?node=${name}&offset=${offset}&limit=${limit}`,
{
credentials: 'include',
},
);
});
it('calls revisions correctly', async () => {
const name = 'sampleName';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.revisions(name);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${name}/revisions/`, {
credentials: 'include',
});
});
it('calls namespace correctly', async () => {
const nmspce = 'sampleNamespace';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.namespace(nmspce);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/namespaces/${nmspce}?edited_by=undefined&with_edited_by=true`,
{
credentials: 'include',
},
);
});
it('calls sql correctly', async () => {
const metric_name = 'sampleMetric';
const selection = { key: 'value' };
const params = new URLSearchParams(selection).toString();
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.sql(metric_name, selection);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/sql/${metric_name}?${params}`,
{
credentials: 'include',
},
);
});
it('calls nodesWithDimension correctly', async () => {
const name = 'sampleName';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.nodesWithDimension(name);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/dimensions/${name}/nodes/`, {
credentials: 'include',
});
});
it('calls materializations correctly', async () => {
const nodeName = 'default.sample_node';
const mockMaterializations = [
{ name: 'materialization1' },
{ name: 'materialization2' },
];
// Mock the first fetch call to return the list of materializations
fetch.mockResponseOnce(JSON.stringify(mockMaterializations));
// Mock the subsequent fetch calls to return clientCode for each materialization
fetch.mockResponses(
...mockMaterializations.map(mat => [
JSON.stringify('from dj import DJClient'),
]),
);
const result = await DataJunctionAPI.materializations(nodeName);
// Check the first fetch call
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/materializations?show_inactive=true&include_all_revisions=true`,
{
credentials: 'include',
},
);
// Ensure the result contains the clientCode for each materialization
expect(result).toEqual(mockMaterializations);
});
it('calls columns correctly', async () => {
const sampleNode = {
name: 'sampleNode',
columns: [
{ name: 'column1', dimension: { name: 'dimension1' } },
{ name: 'column2', dimension: null },
{ name: 'column3', dimension: { name: 'dimension2' } },
],
};
// Mock the fetch calls to return clientCode for columns with a dimension
const mockClientCodeResponses = sampleNode.columns
.filter(col => col.dimension)
.map(col => [JSON.stringify({ clientCode: col.clientCode })]);
fetch.mockResponses(...mockClientCodeResponses);
const result = await DataJunctionAPI.columns(sampleNode);
expect(result).toEqual(sampleNode.columns);
});
it('calls sqls correctly', async () => {
const metricSelection = ['metric1'];
const dimensionSelection = ['dimension1'];
const filters = 'sampleFilter';
const params = new URLSearchParams();
metricSelection.forEach(metric => params.append('metrics', metric));
dimensionSelection.forEach(dimension =>
params.append('dimensions', dimension),
);
params.append('filters', filters);
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.sqls(metricSelection, dimensionSelection, filters);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/sql/?${params}`, {
credentials: 'include',
});
});
it('calls data correctly', async () => {
const metricSelection = ['metric1'];
const dimensionSelection = ['dimension1'];
fetch.mockResponseOnce(
JSON.stringify({
state: 'FINISHED',
results: [{ rows: [['val']] }],
}),
);
await DataJunctionAPI.data(metricSelection, dimensionSelection);
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining(`${DJ_URL}/data/?`),
{ credentials: 'include' },
);
const url = fetch.mock.calls[0][0];
expect(url).toContain('metrics=metric1');
expect(url).toContain('dimensions=dimension1');
expect(url).toContain('limit=10000');
expect(url).toContain('async_=true');
});
it('calls compiledSql correctly', async () => {
const node = 'sampleNode';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.compiledSql(node);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/sql/${node}/`, {
credentials: 'include',
});
});
it('transforms node data correctly', async () => {
fetch.mockResolvedValueOnce({
json: () =>
Promise.resolve({
message: undefined,
columns: [
{
name: 'id',
attributes: [
{
attribute_type: {
name: 'primary_key',
},
},
],
},
],
}),
status: 200,
});
const nodeData = await DataJunctionAPI.node('sampleNodeName');
expect(nodeData.primary_key).toEqual(['id']);
});
it('calls stream correctly', () => {
const metricSelection = ['metric1', 'metric2'];
const dimensionSelection = ['dimension1', 'dimension2'];
const filters = 'sampleFilter';
DataJunctionAPI.stream(metricSelection, dimensionSelection, filters);
const params = new URLSearchParams();
metricSelection.map(metric => params.append('metrics', metric));
dimensionSelection.map(dimension => params.append('dimensions', dimension));
params.append('filters', filters);
expect(global.EventSource).toHaveBeenCalledWith(
`${DJ_URL}/stream/?${params}&limit=10000&async_=true`,
{ withCredentials: true },
);
});
it('calls streamNodeData correctly', () => {
const nodes = ['transform1'];
const dimensionSelection = ['dimension1', 'dimension2'];
const filters = 'sampleFilter';
DataJunctionAPI.streamNodeData(nodes[0], {
dimensions: dimensionSelection,
filters: filters,
});
const params = new URLSearchParams();
params.append('dimensions', dimensionSelection, 'filters', filters);
expect(global.EventSource).toHaveBeenCalledWith(
`${DJ_URL}/stream/transform1?filters=sampleFilter&dimensions=dimension1&dimensions=dimension2&limit=1000&async_=true`,
{ withCredentials: true },
);
});
it('calls nodeData correctly', () => {
const nodes = ['transform1'];
const dimensionSelection = ['dimension1', 'dimension2'];
const filters = 'sampleFilter';
fetch.mockResponseOnce(JSON.stringify({}));
DataJunctionAPI.nodeData(nodes[0], {
dimensions: dimensionSelection,
filters: filters,
});
const params = new URLSearchParams();
params.append('dimensions', dimensionSelection, 'filters', filters);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/data/transform1?filters=sampleFilter&dimensions=dimension1&dimensions=dimension2&limit=1000&async_=true`,
{
credentials: 'include',
headers: {
'Cache-Control': 'max-age=86400',
},
},
);
});
it('calls dag correctly and processes response', async () => {
const mockResponse = [
{
name: 'namespace.node1',
parents: [{ name: 'parent1' }, { name: null }],
columns: [
{
dimension: { name: 'dimension1' },
name: 'col1',
type: 'type1',
attributes: [],
},
{
dimension: null,
name: 'col2',
type: 'type2',
attributes: [{ attribute_type: { name: 'primary_key' } }],
},
],
table: 'table1',
schema_: 'schema1',
display_name: 'DisplayName1',
type: 'type1',
},
];
fetch.mockResponseOnce(JSON.stringify(mockResponse));
const result = await DataJunctionAPI.dag();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/`, {
credentials: 'include',
});
expect(result.edges).toEqual([
{
id: 'namespace.node1-parent1',
target: 'namespace.node1',
source: 'parent1',
animated: true,
markerEnd: { type: 'arrow' },
},
{
id: 'namespace.node1-dimension1',
target: 'namespace.node1',
source: 'dimension1',
draggable: true,
},
]);
expect(result.nodes).toEqual([
{
id: 'namespace.node1',
type: 'DJNode',
data: {
label: 'schema1.table1',
table: 'table1',
name: 'namespace.node1',
display_name: 'DisplayName1',
type: 'type1',
primary_key: ['col2'],
column_names: [
{ name: 'col1', type: 'type1' },
{ name: 'col2', type: 'type2' },
],
},
},
]);
expect(result.namespaces).toEqual([
{
id: 'namespace',
type: 'DJNamespace',
data: {
label: 'namespace',
},
},
]);
});
it('calls link and unlink dimension correctly', async () => {
const nodeName = 'default.transform1';
const dimensionName = 'default.dimension1';
const columnName = 'column1';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.unlinkDimension(nodeName, columnName, dimensionName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/columns/${columnName}?dimension=${dimensionName}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'DELETE',
},
);
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.linkDimension(nodeName, columnName, dimensionName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/columns/${columnName}?dimension=${dimensionName}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
},
);
});
it('calls add and remove complex dimension link correctly', async () => {
const nodeName = 'default.transform1';
const dimensionNode = 'default.dimension1';
const joinOn = 'blah';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.removeComplexDimensionLink(nodeName, dimensionNode);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}/link`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dimension_node: dimensionNode,
role: null,
}),
method: 'DELETE',
});
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.addComplexDimensionLink(
nodeName,
dimensionNode,
joinOn,
);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}/link`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dimension_node: dimensionNode,
join_type: null,
join_on: joinOn,
join_cardinality: null,
role: null,
default_value: null,
}),
method: 'POST',
});
});
it('calls deactivate correctly', async () => {
const nodeName = 'default.transform1';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.deactivate(nodeName);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'DELETE',
});
});
it('calls attributes correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.attributes));
await DataJunctionAPI.attributes();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/attributes`, {
credentials: 'include',
});
});
it('calls dimensions correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.dimensions));
await DataJunctionAPI.dimensions();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/dimensions`, {
credentials: 'include',
});
});
it('calls setAttributes correctly', async () => {
const nodeName = 'default.transform1';
const attributes = ['system'];
const columnName = 'column1';
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.setAttributes(nodeName, columnName, attributes);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/columns/${columnName}/attributes`,
{
credentials: 'include',
body: JSON.stringify([{ namespace: 'system', name: 'system' }]),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
},
);
});
it('calls addNamespace correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.addNamespace('test');
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/test`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});
});
it('calls listTags correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.tags));
const res = await DataJunctionAPI.listTags();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
});
expect(res).toEqual(mocks.tags);
});
it('calls getTag correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.tags[0]));
const res = await DataJunctionAPI.getTag('report.financials');
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags/report.financials`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
});
expect(res).toEqual(mocks.tags[0]);
});
it('calls listNodesForTag correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.mockLinkedNodes));
const res = await DataJunctionAPI.listNodesForTag('report.financials');
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/tags/report.financials/nodes`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
},
);
expect(res).toEqual(mocks.mockLinkedNodes);
});
it('calls tagsNode correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.mockLinkedNodes));
await DataJunctionAPI.tagsNode('default.num_repair_orders', [
'report.financials',
'report.forecasts',
]);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.num_repair_orders/tags?tag_names=report.financials&tag_names=report.forecasts`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
},
);
});
it('calls addTag correctly', async () => {
fetch.mockResponseOnce(JSON.stringify(mocks.mockLinkedNodes));
await DataJunctionAPI.addTag(
'report.financials',
'Financial Reports',
'report',
'financial reports',
);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'report.financials',
display_name: 'Financial Reports',
tag_type: 'report',
description: 'financial reports',
}),
method: 'POST',
});
});
it('calls editTag correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.editTag(
'report.financials',
'Financial reports',
'Financial Reports',
);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags/report.financials`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
description: 'Financial reports',
display_name: 'Financial Reports',
}),
method: 'PATCH',
});
});
it('calls setPartition correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.setPartition(
'default.hard_hat',
'hire_date',
'temporal',
'yyyyMMdd',
'day',
);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.hard_hat/columns/hire_date/partition`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type_: 'temporal',
format: 'yyyyMMdd',
granularity: 'day',
}),
method: 'POST',
},
);
});
it('calls materialize correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.materialize(
'default.hard_hat',
'spark_sql',
'full',
'@daily',
{},
);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.hard_hat/materialization`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
job: 'spark_sql',
strategy: 'full',
schedule: '@daily',
config: {},
}),
method: 'POST',
},
);
});
it('calls runBackfill correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.runBackfill('default.hard_hat', 'spark', [
{
columnName: 'hire_date',
range: ['20230101', '20230202'],
},
]);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.hard_hat/materializations/spark/backfill`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([
{
column_name: 'hire_date',
range: ['20230101', '20230202'],
},
]),
method: 'POST',
},
);
});
it('calls materializationInfo correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.materializationInfo();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/materialization/info`, {
credentials: 'include',
});
});
it('calls revalidate correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.revalidate('default.hard_hat');
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.hard_hat/validate`,
{
credentials: 'include',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
);
});
it('calls deleteMaterialization correctly', () => {
const nodeName = 'transform1';
const materializationName = 'sampleMaterialization';
fetch.mockResponseOnce(JSON.stringify({}));
DataJunctionAPI.deleteMaterialization(nodeName, materializationName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/materializations?materialization_name=${materializationName}`,
{
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
},
);
});
it('calls listNodesForLanding correctly', () => {
fetch.mockResponseOnce(JSON.stringify({}));
DataJunctionAPI.listNodesForLanding(
'',
['source'],
[],
'',
null,
null,
100,
{
key: 'updatedAt',
direction: 'descending',
},
);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/graphql`,
expect.objectContaining({
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
}),
);
});
it('passes search to findNodesPaginated', async () => {
const calls = [];
const originalFetch = global.fetch;
global.fetch = vi.fn(async (_url, opts) => {
calls.push(JSON.parse(opts.body));
return {
json: async () => ({
data: { findNodesPaginated: { edges: [], pageInfo: {} } },
}),
};
});
await DataJunctionAPI.listNodesForLanding(
'growth',
[],
[],
'',
null,
null,
50,
{ key: 'name', direction: 'ascending' },
null,
{ search: 'active' },
);
global.fetch = originalFetch;
expect(calls[0].variables.search).toBe('active');
});
it('calls getMetric correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
data: { findNodes: [{ name: 'default.num_repair_orders' }] },
}),
);
await DataJunctionAPI.getMetric('default.num_repair_orders');
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/graphql`,
expect.objectContaining({
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
}),
);
});
it('calls notebookExportCube correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.notebookExportCube('default.repairs_cube');
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/datajunction-clients/python/notebook/?cube=default.repairs_cube`,
{
credentials: 'include',
},
);
});
it('calls notebookExportNamespace correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({}));
await DataJunctionAPI.notebookExportNamespace('default');
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/datajunction-clients/python/notebook/?namespace=default`,
{
credentials: 'include',
},
);
});
it('calls node_counts_by_type correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
columns: ['system.dj.node_type.type', 'system.dj.number_of_nodes'],
rows: [
['cube', 226],
['dimension', 241],
['metric', 2853],
['source', 540],
['transform', 663],
],
}),
);
const results = await DataJunctionAPI.system.node_counts_by_type();
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/system/data/system.dj.number_of_nodes?dimensions=system.dj.node_type.type&filters=system.dj.is_active.active_id%3Dtrue&orderby=system.dj.node_type.type`,
{
credentials: 'include',
},
);
expect(results).toEqual([
{ name: 'cube', value: 226 },
{ name: 'dimension', value: 241 },
{ name: 'metric', value: 2853 },
{ name: 'source', value: 540 },
{ name: 'transform', value: 663 },
]);
});
it('calls node_counts_by_active correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
columns: ['system.dj.is_active.active_id', 'system.dj.number_of_nodes'],
rows: [
[false, 3136],
[true, 4523],
],
}),
);
const results = await DataJunctionAPI.system.node_counts_by_active();
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/system/data/system.dj.number_of_nodes?dimensions=system.dj.is_active.active_id`,
{
credentials: 'include',
},
);
expect(results).toEqual([
{ name: 'false', value: 3136 },
{ name: 'true', value: 4523 },
]);
});
it('calls node_counts_by_status correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
columns: ['system.dj.nodes.status', 'system.dj.number_of_nodes'],
rows: [
['VALID', 4333],
['INVALID', 190],
],
}),
);
const results = await DataJunctionAPI.system.node_counts_by_status();
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/system/data/system.dj.number_of_nodes?dimensions=system.dj.nodes.status&filters=system.dj.is_active.active_id%3Dtrue&orderby=system.dj.nodes.status`,
{
credentials: 'include',
},
);
expect(results).toEqual([
{ name: 'VALID', value: 4333 },
{ name: 'INVALID', value: 190 },
]);
});
it('calls nodes_without_description correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
columns: [
'system.dj.node_type.type',
'system.dj.node_without_description',
],
rows: [
['cube', 0.1],
['dimension', 0.2],
['metric', 0.3],
['source', 0.4],
['transform', 0.5],
],
}),
);
const results = await DataJunctionAPI.system.nodes_without_description();
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/system/data/system.dj.node_without_description?dimensions=system.dj.node_type.type&filters=system.dj.is_active.active_id%3Dtrue&orderby=system.dj.node_type.type`,
{
credentials: 'include',
},
);
expect(results).toEqual([
{ name: 'cube', value: 0.1 },
{ name: 'dimension', value: 0.2 },
{ name: 'metric', value: 0.3 },
{ name: 'source', value: 0.4 },
{ name: 'transform', value: 0.5 },
]);
});
it('calls node_trends correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
columns: [
'system.dj.nodes.created_at_week',
'system.dj.node_type.type',
'system.dj.number_of_nodes',
],
rows: [
[20250630, 'metric', 42],
[20250707, 'dimension', 21],
[20250707, 'metric', 9],
[20250714, 'metric', 3],
[20250714, 'dimension', 7],
],
}),
);
const results = await DataJunctionAPI.system.node_trends();
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/system/data/system.dj.number_of_nodes?dimensions=system.dj.nodes.created_at_week&dimensions=system.dj.node_type.type&filters=system.dj.nodes.created_at_week>=20240101&orderby=system.dj.nodes.created_at_week`,
{
credentials: 'include',
},
);
expect(results).toEqual([
{ date: 20250630, metric: 42 },
{ date: 20250707, dimension: 21, metric: 9 },
{ date: 20250714, dimension: 7, metric: 3 },
]);
});
it('calls materialization_counts_by_type correctly', async () => {
fetch.mockResponseOnce(
JSON.stringify({
columns: [
'system.dj.node_type.type',
'system.dj.number_of_materializations',
],
rows: [
['cube', 76],
['dimension', 3],
['transform', 9],
],
}),
);
const results =
await DataJunctionAPI.system.materialization_counts_by_type();
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/system/data/system.dj.number_of_materializations?dimensions=system.dj.node_type.type&filters=system.dj.is_active.active_id%3Dtrue&orderby=system.dj.node_type.type`,
{
credentials: 'include',
},
);
expect(results).toEqual([
{ name: 'cube', value: 76 },
{ name: 'dimension', value: 3 },
{ name: 'transform', value: 9 },
]);
});
it('calls system.dimensions correctly', async () => {
fetch.mockResponseOnce(JSON.stringify([]));
const results = await DataJunctionAPI.system.dimensions();
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/system/dimensions`, {
credentials: 'include',
});
});
it('calls availabilityStates correctly', async () => {
const nodeName = 'default.sample_node';
const mockAvailabilityStates = [
{
id: 1,
catalog: 'test_catalog',
schema_: 'test_schema',
table: 'test_table',
valid_through_ts: 1640995200,
url: 'http://example.com/table',
node_revision_id: 123,
node_version: '1.0.0',
},
];
fetch.mockResponseOnce(JSON.stringify(mockAvailabilityStates));
const result = await DataJunctionAPI.availabilityStates(nodeName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}/availability/`,
{
credentials: 'include',
},
);
expect(result).toEqual(mockAvailabilityStates);
});
it('calls refreshLatestMaterialization correctly', async () => {
const nodeName = 'default.sample_cube';
const mockResponse = { message: 'Materialization refreshed successfully' };
fetch.mockResponseOnce(JSON.stringify(mockResponse));
const result = await DataJunctionAPI.refreshLatestMaterialization(nodeName);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/${nodeName}?refresh_materialization=true`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
credentials: 'include',
},
);
expect(result).toEqual({ status: 200, json: mockResponse });
});
// Reference dimension links
it('calls addReferenceDimensionLink correctly', async () => {
const mockResponse = { message: 'Success' };
fetch.mockResponseOnce(JSON.stringify(mockResponse));
const result = await DataJunctionAPI.addReferenceDimensionLink(
'default.node1',
'column1',
'default.dimension1',
'dimension_col',
);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.node1/columns/column1/link?dimension_node=default.dimension1&dimension_column=dimension_col`,
expect.objectContaining({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
}),
);
expect(result).toEqual({ status: 200, json: mockResponse });
});
it('calls removeReferenceDimensionLink correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
const result = await DataJunctionAPI.removeReferenceDimensionLink(
'default.node1',
'column1',
);
expect(fetch).toHaveBeenCalledWith(
`${DJ_URL}/nodes/default.node1/columns/column1/link`,
expect.objectContaining({
method: 'DELETE',
}),
);
});
// Materialization
it('calls deleteMaterialization with nodeVersion correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
await DataJunctionAPI.deleteMaterialization(
'default.node1',
'mat1',
'v1.0',
);
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/nodes/default.node1/materializations?materialization_name=mat1&node_version=v1.0',
expect.objectContaining({ method: 'DELETE' }),
);
});
it('calls deleteMaterialization without nodeVersion correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
await DataJunctionAPI.deleteMaterialization('default.node1', 'mat1');
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/nodes/default.node1/materializations?materialization_name=mat1',
expect.objectContaining({ method: 'DELETE' }),
);
});
// Notifications
it('calls getNotificationPreferences correctly', async () => {
fetch.mockResponseOnce(JSON.stringify([]));
await DataJunctionAPI.getNotificationPreferences({ entity_type: 'node' });
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/notifications/?entity_type=node',
expect.objectContaining({ credentials: 'include' }),
);
});
it('calls subscribeToNotifications correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ status: 'subscribed' }));
await DataJunctionAPI.subscribeToNotifications({
entity_type: 'node',
entity_name: 'default.node1',
activity_types: ['create'],
alert_types: ['email'],
});
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/notifications/subscribe',
expect.objectContaining({
method: 'POST',
body: expect.any(String),
}),
);
});
it('calls unsubscribeFromNotifications correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ status: 'unsubscribed' }));
await DataJunctionAPI.unsubscribeFromNotifications({
entity_type: 'node',
entity_name: 'default.node1',
});
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/notifications/unsubscribe'),
expect.objectContaining({ method: 'DELETE' }),
);
});
// materializeCube (lines 1323-1339)
it('calls materializeCube correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
const result = await DataJunctionAPI.materializeCube(
'default.cube1',
'spark_sql',
'incremental_time',
'0 0 * * *',
'1 day',
);
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/nodes/default.cube1/materialization',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('lookback_window'),
}),
);
expect(result).toEqual({ status: 200, json: { message: 'Success' } });
});
// runBackfill
it('calls runBackfill correctly', async () => {
fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
const result = await DataJunctionAPI.runBackfill('default.node1', 'mat1', [
{
columnName: 'date',
range: ['2023-01-01', '2023-12-31'],
values: [],
},
]);
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/nodes/default.node1/materializations/mat1/backfill',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('column_name'),
}),
);
expect(result).toEqual({ status: 200, json: { message: 'Success' } });
});
// addReferenceDimensionLink with role parameter
it('calls addReferenceDimensionLink with role correctly', async () => {
const mockResponse = { message: 'Success' };
fetch.mockResponseOnce(JSON.stringify(mockResponse));
const result = await DataJunctionAPI.addReferenceDimensionLink(
'default.node1',
'column1',
'default.dimension1',
'dimension_col',
'birth_date',
);
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('role=birth_date'),
expect.objectContaining({
method: 'POST',
}),
);
expect(result).toEqual({ status: 200, json: mockResponse });
});
// Test getNotificationPreferences without params
it('calls getNotificationPreferences without params correctly', async () => {
fetch.mockResponseOnce(JSON.stringify([]));
await DataJunctionAPI.getNotificationPreferences();
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8000/notifications/',
expect