diff --git a/bun.lockb b/bun.lockb index ff60b54747a65e9deaa18ce00b1089ed3bf2c76a..b1ce2cd440ec2c66722e45c4062ef2c1c528fcb9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js index 3c5557f3c9cfc6487225d101a1499678711b2439..7a97a5da4a6bac950fb5c3916b32e479aea38d67 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,20 +1,20 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; export default tseslint.config( - { ignores: ["dist"] }, + { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: {}, rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', }, - } + }, ); diff --git a/index.ts b/index.ts index e6918d588f7f7964eda57ad37b65277554b68f51..8420b1093fdb2f83a26e50df2bb75d893e814251 100644 --- a/index.ts +++ b/index.ts @@ -1 +1 @@ -export * from './src' \ No newline at end of file +export * from './src'; diff --git a/src/neo4jConnection/index.ts b/neo4j/index.ts similarity index 92% rename from src/neo4jConnection/index.ts rename to neo4j/index.ts index 008c4c90e29d8a38be3dfa9ff0f89d701fbc23a8..feca82fbc05116417b1e03a529dd50dcbec5742c 100644 --- a/src/neo4jConnection/index.ts +++ b/neo4j/index.ts @@ -1,6 +1,6 @@ import neo4j, { Driver, type QueryResult, type RecordShape } from 'neo4j-driver'; -import type { DbConnection } from '../model/saveStateModel'; -import { formatTimeDifference, log } from '../logger/logger'; +import type { DbConnection } from '../src/model/saveStateModel'; +import { formatTimeDifference, log } from '../src/logger/logger'; export class Neo4jConnection { private driver: Driver; diff --git a/package.json b/package.json index 13083f911fb64607b9d3e5208d95e37d60c0781e..81d8d74c26456c814326a4ab093e804ce1603881 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,13 @@ "main": "./index.ts", "description": "", "type": "module", - "files": [], + "files": [ + "index.ts", + "src" + ], "scripts": { - "lint": "eslint src/**/* --no-error-on-unmatched-pattern" + "lint": "eslint src/**/* --no-error-on-unmatched-pattern", + "type": "tsc --noEmit --skipLibCheck" }, "devDependencies": { "@types/amqplib": "^0.10.5", diff --git a/src/brokerReader/index.ts b/rabbitMq/index.ts similarity index 100% rename from src/brokerReader/index.ts rename to rabbitMq/index.ts diff --git a/src/brokerReader/rabbitmqBroker.ts b/rabbitMq/rabbitmqBroker.ts similarity index 97% rename from src/brokerReader/rabbitmqBroker.ts rename to rabbitMq/rabbitmqBroker.ts index 4b57d9ce6fb39117f547a72782b293cdf3e8466b..dd9dcc2908457912fa3b39f0f634a388d68d781b 100644 --- a/src/brokerReader/rabbitmqBroker.ts +++ b/rabbitMq/rabbitmqBroker.ts @@ -1,7 +1,7 @@ import { type Channel, type Connection, type ConsumeMessage, type Options } from 'amqplib'; -import type { BackendMessageHeader, SessionData } from '../model/headerModel'; -import { formatTimeDifference, log } from '../logger/logger'; -import type { WsMessageBackend2Frontend, WsMessageBody } from '../model/webSocket'; +import type { BackendMessageHeader, SessionData } from '../src/model/headerModel'; +import { formatTimeDifference, log } from '../src/logger/logger'; +import type { WsMessageBackend2Frontend, WsMessageBody } from '../src/model/webSocket'; import type { RabbitMqConnection } from './rabbitmqConnection'; export class RabbitMqBroker { diff --git a/src/brokerReader/rabbitmqConnection.ts b/rabbitMq/rabbitmqConnection.ts similarity index 97% rename from src/brokerReader/rabbitmqConnection.ts rename to rabbitMq/rabbitmqConnection.ts index f6b616fd6e7d29062bdeca0c2804dfa6e50b9af6..cbcb83944a43b4d71294f361a5ff1618d4954370 100644 --- a/src/brokerReader/rabbitmqConnection.ts +++ b/rabbitMq/rabbitmqConnection.ts @@ -1,5 +1,5 @@ import { connect, type Connection, type Options } from 'amqplib'; -import { log } from '../logger/logger'; +import { log } from '../src/logger/logger'; export class RabbitMqConnection { private connection?: Connection; diff --git a/src/redisConnection/index.ts b/redis/index.ts similarity index 98% rename from src/redisConnection/index.ts rename to redis/index.ts index ac3dedede9949ab714601cf42a296830e21c6bf1..83ccbe4020a02a4c861074376ac0ff5340ff37db 100644 --- a/src/redisConnection/index.ts +++ b/redis/index.ts @@ -1,6 +1,6 @@ import { createClient, type RedisClientType } from 'redis'; import { lock, tryLock, type ReleaseFunc, type TryLockOptions } from 'simple-redis-mutex'; -import { log } from '../logger/logger'; +import { log } from '../src/logger/logger'; export type Routing = { queueID: string; diff --git a/src/index.ts b/src/index.ts index 61ea6d80d3d69f4494514e798392533e8a9e0723..767a4e9d5ff2993a1c3d051ee85375fade12ced5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,3 @@ -export * from './neo4jConnection'; -export * from './redisConnection'; -export * from './brokerReader'; export * from './model'; export * from './userManagementServiceAPI'; export * from './logger'; diff --git a/src/logger/logger.ts b/src/logger/logger.ts index c9d9a0f01b00dbda6b0ef55670e24af98111579f..f64b1e8ee24739965c885f3d31b1a0b6b6f516ab 100644 --- a/src/logger/logger.ts +++ b/src/logger/logger.ts @@ -1,6 +1,7 @@ import { Logger } from '.'; -export const log = new Logger(parseInt(process.env.LOG_LEVEL || '1') as any, 'ts-common'); +const logLevel = typeof process !== 'undefined' && process.env.LOG_LEVEL ? process.env.LOG_LEVEL : '1'; +export const log = new Logger(parseInt(logLevel) as any, 'ts-common'); export function formatTimeDifference(milliseconds: number): string { const seconds = Math.floor((milliseconds / 1000) % 60); diff --git a/src/model/graphology.ts b/src/model/graphology.ts index 62f419ff9ccfc32afb733b2ca08ab260432c0d20..afeee7f67b9544cf3020244b60ce67805e2e1e1f 100644 --- a/src/model/graphology.ts +++ b/src/model/graphology.ts @@ -78,3 +78,5 @@ export type QueryGraphEdges = { }; export type QueryMultiGraph = SerializedGraph<QueryGraphNodes, QueryGraphEdges, GAttributes>; + +export type QueryGraphEdgesOpt = Partial<QueryGraphEdges>; diff --git a/src/model/index.ts b/src/model/index.ts index da0824c27cdacf99b5deb725079035a33971d040..2e1338afd9b4df3b4d73b7c6cc361c2e2ef0b1dd 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -4,3 +4,6 @@ export * from './query'; export * from './saveStateModel'; export * from './insight'; export * from './webSocket'; +export * from './graphology'; +export * from './reactflow'; +export * from './utils'; diff --git a/src/model/insight.ts b/src/model/insight.ts index 0d70a38834b9c9fa4b8e1389d0bf72d6214f6d92..70bf65279abec0a04dc3567a0135105b9396869b 100644 --- a/src/model/insight.ts +++ b/src/model/insight.ts @@ -9,7 +9,7 @@ export type InsightRequest = { saveStateId: string; userId?: number; status?: boolean; - type: 'report' | 'alert'; + type: InsightType; alarmMode: 'disabled' | 'diff' | 'conditional' | 'always'; }; @@ -27,3 +27,21 @@ export type InsightModel = { value: number; }[]; } & InsightRequest; + +export const InsightConditionMap: Record<string, InsightCondition['operator']> = { + 'Greater than': '>', + 'Equal than': '==', + 'Smaller than': '<', + 'Not equal': '!=', + 'Greater or equal': '>=', + 'Smaller or equal': '<=', +}; +export const InsightConditionMapReverse = Object.fromEntries(Object.entries(InsightConditionMap).map(([key, value]) => [value, key])); + +export type InsightCondition = { + nodeLabel: string; + property: string; + statistic: string; + operator: '>' | '==' | '<' | '!=' | '>=' | '<='; + value: number; +}; diff --git a/src/model/query/index.ts b/src/model/query/index.ts index 7188f798dc725eb29133f9d4bbb381a7a837124f..e89aad16cef36ad00d726df28bf9449cacae36c9 100644 --- a/src/model/query/index.ts +++ b/src/model/query/index.ts @@ -1,5 +1,7 @@ export * from './machineLearningModel'; export * from './queryRequestModel'; +export * from './queryBuilderModel'; export * from './queryResultModel'; export * from './mlModel'; -export * from './statisticsResultModel'; +export * from './statistics'; +export * from './logic'; diff --git a/src/model/query/logic/boolFilters.ts b/src/model/query/logic/boolFilters.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ccb849abbd115343ccfc18357313763be5eab94 --- /dev/null +++ b/src/model/query/logic/boolFilters.ts @@ -0,0 +1,37 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +import { BoolFilterTypes, type GeneralDescription } from './general'; + +export const BoolFilters: Record<BoolFilterTypes, GeneralDescription<BoolFilterTypes>> = { + [BoolFilterTypes.EQUAL]: { + key: 'boolFilterEqual', + name: 'Equal', + type: BoolFilterTypes.EQUAL, + description: 'Equal to another value', + input: { name: 'Value', type: 'bool', default: 0 }, + numExtraInputs: 1, + inputs: [{ name: '1', type: 'bool', default: 0 }], + output: { name: '==', type: 'bool' }, + logic: ['==', '@i', '@1'], + icon: 'icon-[ic--baseline-equals]', + }, + [BoolFilterTypes.NOT_EQUAL]: { + key: 'boolFilterNotEqual', + name: 'Not Equal', + type: BoolFilterTypes.NOT_EQUAL, + description: 'Not equal to another value', + input: { name: 'Value', type: 'bool', default: 0 }, + numExtraInputs: 1, + inputs: [{ name: '1', type: 'bool', default: 0 }], + output: { name: '!=', type: 'bool' }, + logic: ['!=', '@i', '@1'], + icon: 'icon-[ic--baseline-not-equal]', + }, +}; + +/** All available functions in the function bar. */ +export const BoolFilterArray: Array<GeneralDescription<BoolFilterTypes>> = Object.values(BoolFilters); diff --git a/src/model/query/logic/general.ts b/src/model/query/logic/general.ts index 063bb32cd0fea1a2f43082a6a703832567a3ad15..e6b25a3f3598f07eee456feeb43411dfa4095d4c 100644 --- a/src/model/query/logic/general.ts +++ b/src/model/query/logic/general.ts @@ -2,6 +2,11 @@ export type InputNodeTypeTypes = string | number | boolean; export type InputNodeType = 'string' | 'float' | 'int' | 'bool' | 'date' | 'time' | 'datetime' | 'duration'; export type InputNodeDimension = 'categorical' | 'numerical' | 'temporal' | 'spatial'; +export enum BoolFilterTypes { + EQUAL = '==', + NOT_EQUAL = '!=', +} + export enum NumberFilterTypes { EQUAL = '==', NOT_EQUAL = '!=', diff --git a/src/model/query/logic/index.ts b/src/model/query/logic/index.ts index 18429b6180b0de61e765ddfb6ef348aabf75e9ea..c4b0ea8c44f70a076d31e01679e543c1c810ebbc 100644 --- a/src/model/query/logic/index.ts +++ b/src/model/query/logic/index.ts @@ -27,3 +27,5 @@ export * from './numberFunctions'; export * from './numberFilters'; export * from './stringFunctions'; export * from './stringFilters'; +export * from './boolFilters'; +export * from './general'; diff --git a/src/model/query/logic/numberAggregations.tsx b/src/model/query/logic/numberAggregations.ts similarity index 100% rename from src/model/query/logic/numberAggregations.tsx rename to src/model/query/logic/numberAggregations.ts diff --git a/src/model/query/logic/numberFilters.tsx b/src/model/query/logic/numberFilters.ts similarity index 100% rename from src/model/query/logic/numberFilters.tsx rename to src/model/query/logic/numberFilters.ts diff --git a/src/model/query/logic/numberFunctions.tsx b/src/model/query/logic/numberFunctions.ts similarity index 100% rename from src/model/query/logic/numberFunctions.tsx rename to src/model/query/logic/numberFunctions.ts diff --git a/src/model/query/logic/stringFilters.tsx b/src/model/query/logic/stringFilters.ts similarity index 100% rename from src/model/query/logic/stringFilters.tsx rename to src/model/query/logic/stringFilters.ts diff --git a/src/model/query/logic/stringFunctions.tsx b/src/model/query/logic/stringFunctions.ts similarity index 100% rename from src/model/query/logic/stringFunctions.tsx rename to src/model/query/logic/stringFunctions.ts diff --git a/src/model/query/machineLearningModel.ts b/src/model/query/machineLearningModel.ts index ee64ba3c55b9d5b1730651b5d04a524bc5bff79a..fbf9bc72d7ba8b43ed11d636102aef551ee271e7 100644 --- a/src/model/query/machineLearningModel.ts +++ b/src/model/query/machineLearningModel.ts @@ -1,12 +1,19 @@ -export type MLTypes = 'centrality' | 'linkPrediction' | 'communityDetection' | 'shortestPath'; -export const allMLTypes: MLTypes[] = ['centrality', 'linkPrediction', 'communityDetection', 'shortestPath']; export enum MLTypesEnum { - CENTRALITY = 'centrality', - LINK_PREDICTION = 'linkPrediction', - COMMUNITY_DETECTION = 'communityDetection', - SHORTEST_PATH = 'shortestPath', + centrality = 'centrality', + linkPrediction = 'linkPrediction', + communityDetection = 'communityDetection', + shortestPath = 'shortestPath', } +export const allMLTypes = [ + MLTypesEnum.centrality, + MLTypesEnum.linkPrediction, + MLTypesEnum.communityDetection, + MLTypesEnum.shortestPath, +] as const; + +export type MLTypes = keyof typeof MLTypesEnum; + export type LinkPredictionInstance = { attributes: { jaccard_coefficient: number }; from: string; @@ -16,7 +23,7 @@ export type LinkPredictionInstance = { export type CommunityDetectionInstance = string[]; // set of ids -export type MLInstance<T> = { +export type MLInstance<T = object> = { enabled: boolean; result: T; }; @@ -33,10 +40,10 @@ export type ShortestPath = { }; export type ML = { - [MLTypesEnum.LINK_PREDICTION]: MLInstance<LinkPredictionInstance[]>; - [MLTypesEnum.CENTRALITY]: MLInstance<Record<string, number>>; - [MLTypesEnum.COMMUNITY_DETECTION]: CommunityDetection; - [MLTypesEnum.SHORTEST_PATH]: ShortestPath; + [MLTypesEnum.linkPrediction]: MLInstance<LinkPredictionInstance[]>; + [MLTypesEnum.centrality]: MLInstance<Record<string, number>>; + [MLTypesEnum.communityDetection]: CommunityDetection; + [MLTypesEnum.shortestPath]: ShortestPath; }; export type MLInstanceTypes = ML[keyof ML]; diff --git a/src/model/query/queryResultModel.ts b/src/model/query/queryResultModel.ts index c1d993010c45fe9a737ebd1ea4f1c89b392dc87b..cff277fb85ff2d106b4d1644853139be57f0857e 100644 --- a/src/model/query/queryResultModel.ts +++ b/src/model/query/queryResultModel.ts @@ -1,4 +1,4 @@ -import type { GraphStatistics } from './statisticsResultModel'; +import type { GraphStatistics } from './statistics/statisticsResultModel'; export type NodeAttributes = { [key: string]: unknown }; diff --git a/src/model/query/statistics/graphStatistics.ts b/src/model/query/statistics/graphStatistics.ts new file mode 100644 index 0000000000000000000000000000000000000000..05f9852a973b137a6ecb43d999cadf6269d33e2e --- /dev/null +++ b/src/model/query/statistics/graphStatistics.ts @@ -0,0 +1,71 @@ +import type { GraphQueryResultFromBackend } from '../../webSocket'; +import type { GraphStatistics } from './statisticsResultModel'; +import { getAttributeType, initializeStatistics, updateStatistics } from './utils'; + +const getGraphStatistics = (graph: GraphQueryResultFromBackend): GraphStatistics => { + const { nodes, edges } = graph; + + const n_nodes = nodes.length; + const n_edges = edges.length; + + const density = n_nodes < 2 ? 0 : (n_edges * 2) / (n_nodes * (n_nodes - 1)); + + // general nodes and edges statistics + const metaData: GraphStatistics = { + topological: { density, self_loops: 0 }, + nodes: { labels: [], count: n_nodes, types: {} }, + edges: { labels: [], count: n_edges, types: {} }, + }; + + // attributes based statistics + nodes.forEach(node => { + const nodeType = node.label; + if (!metaData.nodes.labels.includes(nodeType)) { + metaData.nodes.labels.push(nodeType); + } + + if (!metaData.nodes.types[nodeType]) { + metaData.nodes.types[nodeType] = { count: 0, attributes: {} }; + } + metaData.nodes.types[nodeType].count++; + + Object.entries(node.attributes).forEach(([attributeId, attributeValue]) => { + const attributeType = getAttributeType(attributeValue); + + if (!metaData.nodes.types[nodeType].attributes[attributeId]) { + metaData.nodes.types[nodeType].attributes[attributeId] = { attributeType, statistics: initializeStatistics(attributeType) }; + } + updateStatistics(metaData.nodes.types[nodeType].attributes[attributeId], attributeValue); + }); + }); + + // Process edges + edges.forEach(edge => { + const edgeType = edge.label; + if (!metaData.edges.labels.includes(edgeType)) { + metaData.edges.labels.push(edgeType); + } + + if (!metaData.edges.types[edgeType]) { + metaData.edges.types[edgeType] = { count: 0, attributes: {} }; + } + + metaData.edges.types[edgeType].count++; + + if (edge.from === edge.to) metaData.topological.self_loops++; + + Object.entries(edge.attributes).forEach(([attributeId, attributeValue]) => { + const attributeType = getAttributeType(attributeValue); + + if (!metaData.edges.types[edgeType].attributes[attributeId]) { + metaData.edges.types[edgeType].attributes[attributeId] = { attributeType, statistics: initializeStatistics(attributeType) }; + } + + updateStatistics(metaData.edges.types[edgeType].attributes[attributeId], attributeValue); + }); + }); + + return metaData; +}; + +export { getGraphStatistics }; diff --git a/src/model/query/statistics/index.ts b/src/model/query/statistics/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa1cac65d4e127386067001bb19de74acc46f8f3 --- /dev/null +++ b/src/model/query/statistics/index.ts @@ -0,0 +1,2 @@ +export * from './graphStatistics'; +export * from './statisticsResultModel'; diff --git a/src/model/query/statisticsResultModel.ts b/src/model/query/statistics/statisticsResultModel.ts similarity index 56% rename from src/model/query/statisticsResultModel.ts rename to src/model/query/statistics/statisticsResultModel.ts index cdc6e187eaf35d9f531343f3e28b4abb497973dc..f4294458d66e4dbec80567afb65bceb924c4d4f5 100644 --- a/src/model/query/statisticsResultModel.ts +++ b/src/model/query/statistics/statisticsResultModel.ts @@ -1,10 +1,10 @@ -type GraphStatistics = { +export type GraphStatistics = { topological: TopologicalStats; - nodes: NodeOrEdgeStats; - edges: NodeOrEdgeStats; + nodes: QueryNodeOrEdgeStats; + edges: QueryNodeOrEdgeStats; }; -type NodeOrEdgeStats = { +export type QueryNodeOrEdgeStats = { count: number; labels: string[]; types: { @@ -19,28 +19,28 @@ type NodeOrEdgeStats = { }; }; -type AttributeStats<T extends AttributeType> = { +export type AttributeStats<T extends AttributeType> = { attributeType: T; statistics: AttributeTypeStats<T>; }; -type AttributeTypeStats<T extends AttributeType> = T extends 'string' +export type AttributeTypeStats<T extends AttributeType> = T extends 'string' ? CategoricalStats : T extends 'boolean' - ? BooleanStats - : T extends 'number' - ? NumericalStats - : T extends 'date' | 'time' | 'datetime' | 'timestamp' - ? TemporalStats - : T extends 'array' - ? ArrayStats - : T extends 'object' - ? ObjectStats - : never; + ? BooleanStats + : T extends 'number' + ? NumericalStats + : T extends 'date' | 'time' | 'datetime' | 'timestamp' + ? TemporalStats + : T extends 'array' + ? ArrayStats + : T extends 'object' + ? ObjectStats + : never; -type AttributeType = 'string' | 'boolean' | 'number' | 'array' | 'object' | TemporalType; +export type AttributeType = 'string' | 'boolean' | 'number' | 'array' | 'object' | TemporalType; -type TemporalType = 'date' | 'time' | 'datetime' | 'timestamp'; +export type TemporalType = 'date' | 'time' | 'datetime' | 'timestamp'; // Date: Date in the YYYY-MM-DD format (ISO 8601 syntax) (e.g., 2021-09-28) // Time: Time in the hh:mm:ss format for the time of day, time since an event, or time interval between events (e.g., 12:00:59) // Datetime: Date and time together in the YYYY-MM-DD hh:mm:ss format (e.g., 2021-09-28 12:00:59) @@ -51,49 +51,36 @@ type TopologicalStats = { self_loops: number; }; -type NumericalStats = { +export type NumericalStats = { min: number; max: number; average: number; count: number; }; -type BooleanStats = { +export type BooleanStats = { true: number; false: number; }; -type CategoricalStats = { +export type CategoricalStats = { uniqueItems: number; values: string[]; mode: string; count: number; }; -type TemporalStats = { +export type TemporalStats = { min: number; max: number; range: number; }; -type ArrayStats = { +export type ArrayStats = { length: number; count: number; }; -type ObjectStats = { +export type ObjectStats = { length: number; }; - -export type { - GraphStatistics, - AttributeStats, - NumericalStats, - CategoricalStats, - BooleanStats, - TemporalStats, - AttributeType, - AttributeTypeStats, - ArrayStats, - ObjectStats, -}; diff --git a/src/model/query/statistics/tests/attributeStats.spec.ts b/src/model/query/statistics/tests/attributeStats.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd7175b001561c6d33139a338628d143e5d1a676 --- /dev/null +++ b/src/model/query/statistics/tests/attributeStats.spec.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'bun:test'; +import { + updateArrayStats, + updateBooleanStats, + updateCategoricalStats, + updateNumericalStats, + updateTemporalStats, + updateObjectStats, + initializeStatistics, +} from '../utils/attributeStats'; +import type { ArrayStats, BooleanStats, CategoricalStats, NumericalStats, TemporalStats, ObjectStats } from '../statisticsResultModel'; + +describe('updateArrayStats', () => { + it('should update the length of the array', () => { + const stats: ArrayStats = { length: 0, count: 0 }; + const value = [1, 2, 3]; + updateArrayStats(stats, value); + expect(stats.length).toBe(3); + expect(stats.count).toBe(1); + }); +}); + +describe('updateBooleanStats', () => { + it('should update true count when value is true', () => { + const stats: BooleanStats = { true: 0, false: 0 }; + updateBooleanStats(stats, true); + expect(stats.true).toBe(1); + expect(stats.false).toBe(0); + }); + + it('should update false count when value is false', () => { + const stats: BooleanStats = { true: 0, false: 0 }; + updateBooleanStats(stats, false); + expect(stats.false).toBe(1); + expect(stats.true).toBe(0); + }); +}); + +describe('updateCategoricalStats', () => { + it('should update mode and unique items count', () => { + const stats: CategoricalStats = { uniqueItems: 0, values: [], mode: '', count: 0 }; + updateCategoricalStats(stats, 'apple'); + updateCategoricalStats(stats, 'banana'); + updateCategoricalStats(stats, 'apple'); + + expect(stats.values).toEqual(['apple', 'banana', 'apple']); + expect(stats.uniqueItems).toBe(2); + expect(stats.mode).toBe('apple'); + expect(stats.count).toBe(3); + }); +}); + +describe('updateNumericalStats', () => { + it('should update min, max, average, and count', () => { + const stats: NumericalStats = { min: Infinity, max: -Infinity, average: 0, count: 0 }; + updateNumericalStats(stats, 10); + updateNumericalStats(stats, 20); + updateNumericalStats(stats, 5); + + expect(stats.min).toBe(5); + expect(stats.max).toBe(20); + expect(stats.average).toBeCloseTo(11.67, 2); + expect(stats.count).toBe(3); + }); +}); + +describe('updateTemporalStats', () => { + it('should update min, max, and range for temporal values', () => { + const stats: TemporalStats = { min: Infinity, max: -Infinity, range: 0 }; + updateTemporalStats(stats, '2022-01-01'); + updateTemporalStats(stats, '2022-01-05'); + + expect(stats.min).toBe(new Date('2022-01-01').getTime()); + expect(stats.max).toBe(new Date('2022-01-05').getTime()); + expect(stats.range).toBe(new Date('2022-01-05').getTime() - new Date('2022-01-01').getTime()); + }); +}); + +describe('updateObjectStats', () => { + it('should update the length of the object keys', () => { + const stats: ObjectStats = { length: 0 }; + const value = { key1: 'value1', key2: 'value2' }; + updateObjectStats(stats, value); + expect(stats.length).toBe(2); + }); +}); + +describe('initializeStatistics', () => { + it('should initialize statistics for string type', () => { + const stats = initializeStatistics('string'); + expect(stats).toEqual({ uniqueItems: 0, values: [], mode: '', count: 0 }); + }); + + it('should initialize statistics for boolean type', () => { + const stats = initializeStatistics('boolean'); + expect(stats).toEqual({ true: 0, false: 0 }); + }); + + it('should initialize statistics for number type', () => { + const stats = initializeStatistics('number'); + expect(stats).toEqual({ min: Infinity, max: -Infinity, average: 0, count: 0 }); + }); + + it('should throw an error for an unknown type', () => { + expect(() => initializeStatistics('unknown' as any)).toThrow('Unknown attribute type: unknown'); + }); +}); diff --git a/src/model/query/statistics/tests/getAttributeType.spec.ts b/src/model/query/statistics/tests/getAttributeType.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b8923c41c3c47795e9fb485ffd3aa21ac6688bb --- /dev/null +++ b/src/model/query/statistics/tests/getAttributeType.spec.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'bun:test'; +import { getAttributeType } from '../utils/getAttributeType'; + +// Sample values for testing +const invalidDate = '2023-13-03'; +const validTime = '12:30:45'; +const invalidTime = '25:61:61'; +const invalidDatetime = '2023-10-03 25:61:61'; +const validNumber = '123.45'; +const invalidNumber = 'abc123'; +const booleanTrue = true; +const booleanFalse = false; +const numberValue = 123; +const arrayValue = [1, 2, 3]; +const objectValue = { key: 'value' }; +const dateInstance = new Date('2023-10-03T12:30:45'); + +// Unit tests for getAttributeType function +describe('getAttributeType', () => { + it('should correctly identify numbers as type "number"', () => { + expect(getAttributeType(validNumber)).toBe('number'); + expect(getAttributeType(numberValue)).toBe('number'); + }); + + it('should correctly identify strings as valid "time"', () => { + expect(getAttributeType(validTime)).toBe('time'); + }); + + it('should identify invalid datetime strings as "string"', () => { + expect(getAttributeType(invalidDatetime)).toBe('string'); + }); + + it('should identify invalid date strings as "string"', () => { + expect(getAttributeType(invalidDate)).toBe('string'); + }); + + it('should identify invalid time strings as "string"', () => { + expect(getAttributeType(invalidTime)).toBe('string'); + }); + + it('should correctly identify boolean values as type "boolean"', () => { + expect(getAttributeType(booleanTrue)).toBe('boolean'); + expect(getAttributeType(booleanFalse)).toBe('boolean'); + }); + + it('should correctly identify arrays as type "array"', () => { + expect(getAttributeType(arrayValue)).toBe('array'); + }); + + it('should correctly identify objects as type "object"', () => { + expect(getAttributeType(objectValue)).toBe('object'); + }); + + it('should correctly identify Date instances as type "datetime"', () => { + expect(getAttributeType(dateInstance)).toBe('datetime'); + }); + + it('should identify string representations of invalid numbers as "string"', () => { + expect(getAttributeType(invalidNumber)).toBe('string'); + }); + + it('should identify a regular string as type "string"', () => { + expect(getAttributeType('random string')).toBe('string'); + }); +}); diff --git a/src/model/query/statistics/tests/graphStatistics.spec.ts b/src/model/query/statistics/tests/graphStatistics.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..70ea8da4040730113c976ed5a4bcba52bc5489b8 --- /dev/null +++ b/src/model/query/statistics/tests/graphStatistics.spec.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from 'bun:test'; +import { getGraphStatistics } from '../graphStatistics'; +import type { GraphQueryResultFromBackend } from '../../../webSocket'; + +describe('getGraphStatistics', () => { + it('should return correct statistics for a graph with no nodes and edges', () => { + const graph: GraphQueryResultFromBackend = { + nodes: [], + edges: [], + }; + + const stats = getGraphStatistics(graph); + + expect(stats).toEqual({ + topological: { density: 0, self_loops: 0 }, + nodes: { labels: [], count: 0, types: {} }, + edges: { labels: [], count: 0, types: {} }, + }); + }); + + it('should return correct statistics for a graph with nodes and no edges', () => { + const graph: GraphQueryResultFromBackend = { + nodes: [ + { _id: '1', label: 'Person', attributes: { age: 25 } }, + { _id: '2', label: 'Person', attributes: { age: 30, city: 'New York' } }, + ], + edges: [], + }; + + const stats = getGraphStatistics(graph); + + expect(stats).toEqual({ + topological: { density: 0, self_loops: 0 }, + nodes: { + labels: ['Person'], // Assuming default label + count: 2, + types: { + Person: { + count: 2, + attributes: { + age: { attributeType: 'number', statistics: expect.any(Object) }, + city: { attributeType: 'string', statistics: expect.any(Object) }, + }, + }, + }, + }, + edges: { labels: [], count: 0, types: {} }, + }); + }); + + it('should return correct statistics for a graph with edges and nodes', () => { + const graph: GraphQueryResultFromBackend = { + nodes: [ + { _id: '1', label: 'Person', attributes: { age: 25 } }, + { _id: '2', label: 'Person', attributes: { age: 30 } }, + ], + edges: [ + { _id: 'e1', label: 'Relationship', attributes: { weight: 5 }, from: '1', to: '2' }, + { _id: 'e2', label: 'Relationship', attributes: { weight: 10 }, from: '2', to: '2' }, // self-loop + ], + }; + + const stats = getGraphStatistics(graph); + + expect(stats).toEqual({ + topological: { density: 2, self_loops: 1 }, + nodes: { + labels: ['Person'], // Assuming default label + count: 2, + types: { + Person: { + count: 2, + attributes: { + age: { attributeType: 'number', statistics: expect.any(Object) }, + }, + }, + }, + }, + edges: { + labels: ['Relationship'], // Assuming default edge type + count: 2, + types: { + Relationship: { + count: 2, + attributes: { + weight: { attributeType: 'number', statistics: expect.any(Object) }, + }, + }, + }, + }, + }); + }); + + it('should correctly count self-loops', () => { + const graph: GraphQueryResultFromBackend = { + nodes: [{ _id: '1', attributes: {}, label: 'Person' }], + edges: [ + { _id: 'e1', attributes: {}, from: '1', to: '1', label: 'Person' }, // self-loop + ], + }; + + const stats = getGraphStatistics(graph); + + expect(stats.topological.self_loops).toBe(1); + }); + + it('should correctly compute density for a graph with nodes and edges', () => { + const graph: GraphQueryResultFromBackend = { + nodes: [ + { _id: '1', attributes: {}, label: 'Person' }, + { _id: '2', attributes: {}, label: 'Person' }, + ], + edges: [{ _id: 'e1', attributes: {}, from: '1', to: '2', label: 'Person' }], + }; + + const stats = getGraphStatistics(graph); + + // Density = (n_edges * 2) / (n_nodes * (n_nodes - 1)) = (1 * 2) / (2 * 1) = 1 + expect(stats.topological.density).toBe(1); + }); +}); diff --git a/src/model/query/statistics/utils/attributeStats/array.ts b/src/model/query/statistics/utils/attributeStats/array.ts new file mode 100644 index 0000000000000000000000000000000000000000..915e56d720785849c812939bcb2f8d46e816aeb0 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/array.ts @@ -0,0 +1,6 @@ +import type { ArrayStats } from '../../statisticsResultModel'; + +export const updateArrayStats = (stats: ArrayStats, value: any[]) => { + stats.length = value.length; + stats.count++; +}; diff --git a/src/model/query/statistics/utils/attributeStats/boolean.ts b/src/model/query/statistics/utils/attributeStats/boolean.ts new file mode 100644 index 0000000000000000000000000000000000000000..952aea8e6fc9aa82d170c42e0193c5d3d5523e97 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/boolean.ts @@ -0,0 +1,9 @@ +import type { BooleanStats } from '../../statisticsResultModel'; + +export const updateBooleanStats = (stats: BooleanStats, value: boolean) => { + if (value) { + stats.true += 1; + } else { + stats.false += 1; + } +}; diff --git a/src/model/query/statistics/utils/attributeStats/categorical.ts b/src/model/query/statistics/utils/attributeStats/categorical.ts new file mode 100644 index 0000000000000000000000000000000000000000..1932817b2bddb9b7db93bbdb4fccc84d41e592f0 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/categorical.ts @@ -0,0 +1,15 @@ +import { type CategoricalStats } from '../../statisticsResultModel'; + +export const updateCategoricalStats = (stats: CategoricalStats, value: string | boolean) => { + if (!stats.values) stats.values = []; + stats.values.push(value.toString()); + + stats.uniqueItems = new Set(stats.values).size; + + const frequencyMap: { [key: string]: number } = {}; + stats.values.forEach(val => { + frequencyMap[val] = (frequencyMap[val] || 0) + 1; + }); + stats.mode = Object.keys(frequencyMap).reduce((a, b) => (frequencyMap[a] > frequencyMap[b] ? a : b)); + stats.count++; +}; diff --git a/src/model/query/statistics/utils/attributeStats/index.ts b/src/model/query/statistics/utils/attributeStats/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..42c450156100639b4eaa6c3a57ad63f4eef20fb9 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/index.ts @@ -0,0 +1,7 @@ +export * from './array'; +export * from './categorical'; +export * from './numerical'; +export * from './object'; +export * from './temporal'; +export * from './boolean'; +export * from './initialize'; diff --git a/src/model/query/statistics/utils/attributeStats/initialize.ts b/src/model/query/statistics/utils/attributeStats/initialize.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcc6238293da678b5fd051ea24f9d5bc93adc1f9 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/initialize.ts @@ -0,0 +1,45 @@ +import type { AttributeType, AttributeTypeStats } from '../../statisticsResultModel'; + +export const initializeStatistics = <T extends AttributeType>(type: T): AttributeTypeStats<T> => { + switch (type) { + case 'string': + return { + uniqueItems: 0, + values: [], + mode: '', + count: 0, + } as unknown as AttributeTypeStats<T>; + case 'boolean': + return { + true: 0, + false: 0, + } as unknown as AttributeTypeStats<T>; + case 'number': + return { + min: Infinity, + max: -Infinity, + average: 0, + count: 0, + } as unknown as AttributeTypeStats<T>; + case 'date': + case 'time': + case 'datetime': + case 'timestamp': + return { + min: Infinity, + max: -Infinity, + range: 0, + } as unknown as AttributeTypeStats<T>; + case 'array': + return { + length: 0, + count: 0, + } as unknown as AttributeTypeStats<T>; + case 'object': + return { + length: 0, + } as unknown as AttributeTypeStats<T>; + default: + throw new Error(`Unknown attribute type: ${type}`); + } +}; diff --git a/src/model/query/statistics/utils/attributeStats/numerical.ts b/src/model/query/statistics/utils/attributeStats/numerical.ts new file mode 100644 index 0000000000000000000000000000000000000000..d845d661cba370fb32f7580a9c7572c370f94f90 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/numerical.ts @@ -0,0 +1,9 @@ +import type { NumericalStats } from '../../statisticsResultModel'; + +export const updateNumericalStats = (stats: NumericalStats, value: number) => { + if (stats.min === undefined || value < stats.min) stats.min = value; + if (stats.max === undefined || value > stats.max) stats.max = value; + + stats.count++; + stats.average = (stats.average * (stats.count - 1) + value) / stats.count; +}; diff --git a/src/model/query/statistics/utils/attributeStats/object.ts b/src/model/query/statistics/utils/attributeStats/object.ts new file mode 100644 index 0000000000000000000000000000000000000000..26ed4ec838d49f0d359cb559ef78eec5c7d64350 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/object.ts @@ -0,0 +1,5 @@ +import type { ObjectStats } from '../../statisticsResultModel'; + +export const updateObjectStats = (stats: ObjectStats, value: object) => { + stats.length = Object.keys(value).length; +}; diff --git a/src/model/query/statistics/utils/attributeStats/temporal.ts b/src/model/query/statistics/utils/attributeStats/temporal.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2a2a116df3540bb2af9e5f567ae0b6f63b75d37 --- /dev/null +++ b/src/model/query/statistics/utils/attributeStats/temporal.ts @@ -0,0 +1,10 @@ +import type { TemporalStats } from '../../statisticsResultModel'; + +export const updateTemporalStats = (stats: TemporalStats, value: string | Date) => { + const timestamp = value instanceof Date ? value.getTime() : new Date(value).getTime(); + + if (stats.min === undefined || timestamp < stats.min) stats.min = timestamp; + if (stats.max === undefined || timestamp > stats.max) stats.max = timestamp; + + stats.range = stats.max - stats.min; +}; diff --git a/src/model/query/statistics/utils/getAttributeType.ts b/src/model/query/statistics/utils/getAttributeType.ts new file mode 100644 index 0000000000000000000000000000000000000000..f017b2e37fc31b067fa5e5f8e8e087342791430e --- /dev/null +++ b/src/model/query/statistics/utils/getAttributeType.ts @@ -0,0 +1,76 @@ +import type { AttributeType } from '../statisticsResultModel'; + +// Check if a string is a valid date in the YYYY-MM-DD format +const isValidDate = (value: string): boolean => { + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; // Matches YYYY-MM-DD format + const [year, month, day] = value.split('-').map(Number); + const date = new Date(value); + + // Check if the regex matches, and if the date is valid (correct month/day conversion) + return ( + dateRegex.test(value) && + date.getFullYear() === year && + date.getMonth() + 1 === month && // Months are 0-based in JS Date + date.getDate() === day + ); +}; + +// Check if a string is a valid time in the hh:mm:ss format +const isValidTime = (value: string): boolean => { + const timeRegex = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/; + return timeRegex.test(value); +}; + +// Check if a string is a valid datetime in the YYYY-MM-DD hh:mm:ss format +const isValidDatetime = (value: string): boolean => { + const datetimeRegex = /^\d{4}-\d{2}-\d{2} ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/; + const [date, time] = value.split(' '); + + return datetimeRegex.test(value) && isValidDate(date) && isValidTime(time); +}; + +// Check if a string is a valid number +const isValidNumber = (value: string): boolean => { + return !isNaN(Number(value)) && !isNaN(parseFloat(value)); +}; + +// Determines the type of an attribute +export const getAttributeType = (value: any): AttributeType => { + if (typeof value === 'string') { + if (isValidNumber(value)) { + return 'number'; + } + if (isValidDatetime(value)) { + return 'datetime'; + } + if (isValidDate(value)) { + return 'date'; + } + if (isValidTime(value)) { + return 'time'; + } + return 'string'; + } + + if (typeof value === 'boolean') { + return 'boolean'; + } + + if (typeof value === 'number') { + return 'number'; + } + + if (Array.isArray(value)) { + return 'array'; + } + + if (value instanceof Date) { + return 'datetime'; + } + + if (typeof value === 'object' && value !== null) { + return 'object'; + } + + return 'string'; +}; diff --git a/src/model/query/statistics/utils/index.ts b/src/model/query/statistics/utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fb895dedbce79d9c53ebcfe4625058436efe185 --- /dev/null +++ b/src/model/query/statistics/utils/index.ts @@ -0,0 +1,3 @@ +export * from './getAttributeType'; +export * from './attributeStats'; +export * from './updateStatistics'; diff --git a/src/model/query/statistics/utils/updateStatistics.ts b/src/model/query/statistics/utils/updateStatistics.ts new file mode 100644 index 0000000000000000000000000000000000000000..82cab1527dca5322607333ca15647e070f81f507 --- /dev/null +++ b/src/model/query/statistics/utils/updateStatistics.ts @@ -0,0 +1,45 @@ +import type { + AttributeStats, + AttributeType, + NumericalStats, + CategoricalStats, + BooleanStats, + TemporalStats, + ArrayStats, + ObjectStats, +} from '../statisticsResultModel'; +import { + updateArrayStats, + updateCategoricalStats, + updateNumericalStats, + updateObjectStats, + updateTemporalStats, + updateBooleanStats, +} from './attributeStats'; + +// Update statistics based on attribute type and value +export const updateStatistics = (attribute: AttributeStats<AttributeType>, value: any) => { + switch (attribute.attributeType) { + case 'number': + updateNumericalStats(attribute.statistics as NumericalStats, value); + break; + case 'string': + updateCategoricalStats(attribute.statistics as CategoricalStats, value); + break; + case 'boolean': + updateBooleanStats(attribute.statistics as BooleanStats, value); + break; + case 'datetime': + case 'timestamp': + case 'date': + case 'time': + updateTemporalStats(attribute.statistics as TemporalStats, value); + break; + case 'array': + updateArrayStats(attribute.statistics as ArrayStats, value); + break; + case 'object': + updateObjectStats(attribute.statistics as ObjectStats, value); + break; + } +}; diff --git a/src/model/reactflow.ts b/src/model/reactflow.ts index 1f7289238e61e855790759740278fa4bbbff742b..5f90c9d872b37389d8777480a1d8e53c0cc41d8b 100644 --- a/src/model/reactflow.ts +++ b/src/model/reactflow.ts @@ -21,3 +21,20 @@ export enum QueryElementTypes { // Function = 'function', Logic = 'logic', } + +export function isRelationHandle(handle: Handles): boolean { + return handle.startsWith(Handles.RelationLeft) || handle.startsWith(Handles.RelationRight); +} + +export function isEntityHandle(handle: Handles): boolean { + return handle.startsWith(Handles.EntityLeft) || handle.startsWith(Handles.EntityRight); +} + +export function isLogicHandle(handle: Handles): boolean { + return ( + handle.startsWith(Handles.LogicLeft) || + handle.startsWith(Handles.LogicRight) || + handle.startsWith(Handles.RelationAttribute) || + handle.startsWith(Handles.EntityAttribute) + ); +} diff --git a/src/model/saveStateModel.ts b/src/model/saveStateModel.ts index 25b38297b700a1cf51be958ad6d737c193bfbef5..bf2890de564793fd6dbee235e678eedd372fd488 100644 --- a/src/model/saveStateModel.ts +++ b/src/model/saveStateModel.ts @@ -1,3 +1,4 @@ +import type { QueryGraphEdgeHandle } from './graphology'; import type { QueryBuilderSettings } from './query/queryBuilderModel'; export type DbConnection = { @@ -22,7 +23,7 @@ export type Query = { id?: number; graph: Graph; settings: QueryBuilderSettings; - attributesBeingShown: any[]; + attributesBeingShown: QueryGraphEdgeHandle[]; }; export type Visualization = { diff --git a/src/model/schemaModel.ts b/src/model/schemaModel.ts index f107e4df1470b827ec00704d81633914bf36a338..e115c8c645b181ac7db756898105c84190a7ae24 100644 --- a/src/model/schemaModel.ts +++ b/src/model/schemaModel.ts @@ -54,15 +54,15 @@ export type SchemaGraphInference = { export type SchemaGraphStats = { nodes: { count: number; - stats: Record<string, NodeOrEdgeStats>; // node key -> Stats + stats: Record<string, SchemaNodeOrEdgeStats>; // node key -> Stats }; edges: { count: number; - stats: Record<string, NodeOrEdgeStats>; // edge key -> Stats + stats: Record<string, SchemaNodeOrEdgeStats>; // edge key -> Stats }; }; -export type NodeOrEdgeStats = { +export type SchemaNodeOrEdgeStats = { key: string; type?: string; count: number; diff --git a/src/model/utils.ts b/src/model/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fa144fa170077ab3db2ff7467337405fa22591b --- /dev/null +++ b/src/model/utils.ts @@ -0,0 +1,5 @@ +export type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// Example usage: +// type Union = { a: string } | { b: number }; +// type Intersection = UnionToIntersection<Union>; // { a: string } & { b: number } diff --git a/src/model/webSocket/graphResult.ts b/src/model/webSocket/graphResult.ts index deebcf0d54f499715976edfd10170cb381a6b788..5da8126e5ab916d215a71bc6c34c2508a59b2d75 100644 --- a/src/model/webSocket/graphResult.ts +++ b/src/model/webSocket/graphResult.ts @@ -1,21 +1,19 @@ import type { NodeQueryResult, EdgeQueryResult, GraphStatistics } from '../query'; -export interface GraphQueryResultFromBackend { +export type GraphQueryResultFromBackend = { nodes: NodeQueryResult[]; edges: EdgeQueryResult[]; // TODO: Also include type in node and edge // TODO: The backend should send all the different entitytypes and relationtypes in the result -} +}; -export interface GraphQueryResultMetaFromBackend { - nodes: NodeQueryResult[]; - edges: EdgeQueryResult[]; +export type GraphQueryResultMetaFromBackend = GraphQueryResultFromBackend & { metaData: GraphStatistics; // TODO: Also include type in node and edge // TODO: The backend should send all the different entitytypes and relationtypes in the result -} +}; export type GraphQueryTranslationResultMessage = { queryID: string; diff --git a/src/model/webSocket/index.ts b/src/model/webSocket/index.ts index 0d903cfbc9e52c90ad84c50a4f68b98ed8a03edf..af4a8c1d535fa58020eb12308926d6f99b3f8189 100644 --- a/src/model/webSocket/index.ts +++ b/src/model/webSocket/index.ts @@ -2,3 +2,5 @@ export * from './message2Backend'; export * from './message2Frontend'; export * from './schema'; export * from './graphResult'; +export * from './policy'; +export * from './model'; diff --git a/src/model/webSocket/message2Backend.ts b/src/model/webSocket/message2Backend.ts index 77b559e1f600959947aeeb1a0ed33b2e818adaf2..d6e4732f01df638aafec879928d24b3a54fed588 100644 --- a/src/model/webSocket/message2Backend.ts +++ b/src/model/webSocket/message2Backend.ts @@ -63,7 +63,7 @@ export type WsMessageBody = | WsMessageBodyI<wsKeys.schema, wsSubKeys.get, { cached: boolean; saveStateID: string }> | WsMessageBodyI<wsKeys.schema, wsSubKeys.getSchemaStats, { cached: boolean; saveStateID: string }> // Query - | WsMessageBodyI<wsKeys.query, wsSubKeys.get, { saveStateID: string; ml: MLInstanceTypes[]; cached: boolean }> + | WsMessageBodyI<wsKeys.query, wsSubKeys.get, { saveStateID: string; ml: MLInstanceTypes[]; cached: boolean; queryID: string }> | WsMessageBodyI<wsKeys.query, wsSubKeys.manual, string> // Insights | WsMessageBodyI<wsKeys.insight, wsSubKeys.create, InsightRequest> diff --git a/src/model/webSocket/message2Frontend.ts b/src/model/webSocket/message2Frontend.ts index af0e1164e1ec0f6d60441ce4188df1e9b35fe34c..8d8fde30627e71d016a12ac857b2f8e0d7162c91 100644 --- a/src/model/webSocket/message2Frontend.ts +++ b/src/model/webSocket/message2Frontend.ts @@ -4,6 +4,7 @@ import type { DBConnectionResult } from './dbConnection'; import type { GraphQueryTranslationResultMessage, QueryStatusResult } from './graphResult'; import type { InsightModel } from '../insight'; import type { SaveStateAuthorizationHeaders, UserAuthorizationHeaders } from './policy'; +import type { MLTypesEnum } from '../query'; export enum wsReturnKey { testedConnection = 'tested_connection', @@ -25,8 +26,10 @@ export enum wsReturnKey { userPolicy = 'user_policy', error = 'error', reconnect = 'reconnect', + schemaInference = 'schema_inference', } export type WsReturnKey = keyof typeof wsReturnKey; +export type wsReturnKeyWithML = wsReturnKey | MLTypesEnum; type WsMessageBody2FrontendI<T extends wsReturnKey, V> = { callID: string; @@ -65,4 +68,9 @@ export type WsMessageBackend2Frontend = // Reconnect | WsMessageBody2FrontendI<wsReturnKey.reconnect, { sessionID: string }>; -export type ResponseCallback = (data: WsMessageBackend2Frontend['value'], status: string) => void; +export type ResponseCallback<T extends wsReturnKey> = ( + data: Extract<WsMessageBackend2Frontend, { type: T }>['value'] | null, + status: string, +) => void; + +export type WsFrontendCall<T, R extends wsReturnKey = never> = (params: T, callback?: ResponseCallback<R>) => void; diff --git a/src/model/webSocket/model.ts b/src/model/webSocket/model.ts index 8b137891791fe96927ad78e64b0aad7bded08bdc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/model/webSocket/model.ts +++ b/src/model/webSocket/model.ts @@ -1 +0,0 @@ - diff --git a/src/model/webSocket/policy.ts b/src/model/webSocket/policy.ts index 6c40dbd6096f5970439ef0905f9f3769431f6b26..22c074e205714b8e5f03a737da2b0b3bc5140521 100644 --- a/src/model/webSocket/policy.ts +++ b/src/model/webSocket/policy.ts @@ -1,5 +1,3 @@ -export type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; - export enum UserAuthorizationObjectsEnum { savestate = 'savestate', demoUser = 'demoUser', @@ -48,3 +46,21 @@ export type PolicyItem = { obj: string; act: string; }; + +export interface UserPolicy { + name: string; + email: string; + type: string; +} + +export type UserAuthenticationHeader = { + username: string; + userID: number; + roomID: string; + jwt: string; +}; + +export interface PolicyResourcesState { + read: string[]; + write: string[]; +}