import { config } from '../config.js';
export interface SSBTable {
id: string;
label: string;
updated: string;
firstPeriod: string;
lastPeriod: string;
variableNames: string[];
subjectCode: string;
timeUnit: string;
}
export interface SSBTableMetadata {
title: string;
variables: {
code: string;
text: string;
values: string[];
valueTexts: string[];
}[];
}
export interface SSBDataQuery {
query: {
code: string;
selection: {
filter: string;
values: string[];
};
}[];
response: {
format: string;
};
}
export interface SSBTimeSeries {
period: string;
value: number;
}
export interface SSBTrendAnalysis {
direction: 'increasing' | 'decreasing' | 'stable';
percentageChange: number;
averageGrowth: number;
periods: number;
startValue: number;
endValue: number;
}
export class SSBClient {
private baseUrl = config.ssb.baseUrl;
private username = config.ssb.username;
private db: any; // Database instance for caching
constructor(database?: any) {
this.db = database;
}
private async fetch(endpoint: string, options: RequestInit = {}): Promise<any> {
const url = `${this.baseUrl}${endpoint}`;
console.error(`Fetching from SSB: ${url}`);
const response = await fetch(url, {
...options,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': `CompanyIQ/${this.username}`,
...options.headers
}
});
if (!response.ok) {
throw new Error(`SSB API error: ${response.status}`);
}
return await response.json();
}
/**
* Search for tables by keyword
*/
async searchTables(query: string, limit: number = 20): Promise<SSBTable[]> {
try {
const data = await this.fetch(`/tables?query=${encodeURIComponent(query)}&page=1&size=${limit}`);
return data.tables || [];
} catch (error) {
console.error('Error searching SSB tables:', error);
return [];
}
}
/**
* Get high-growth enterprises ("gaseller") by industry
* Table 13706: High-growth firms by industry (NACE)
*/
async getHighGrowthEnterprises(naceCode?: string): Promise<any> {
try {
const tables = await this.searchTables('høgvekstføretak næring', 5);
const table = tables.find(t => t.id === '13706' || t.id === '13758');
if (table) {
return {
tableId: table.id,
label: table.label,
period: `${table.firstPeriod}-${table.lastPeriod}`,
description: 'High-growth enterprises and gazelles by industry sector'
};
}
return null;
} catch (error) {
console.error('Error fetching high-growth enterprises:', error);
return null;
}
}
/**
* Get new enterprises statistics
* Table 09028: New enterprises by industry (quarterly)
*/
async getNewEnterprises(): Promise<any> {
try {
const tables = await this.searchTables('nye foretak', 5);
const table = tables.find(t => t.id === '09028');
if (table) {
return {
tableId: table.id,
label: table.label,
period: `${table.firstPeriod}-${table.lastPeriod}`,
timeUnit: table.timeUnit,
description: 'Newly established enterprises (quarterly updates)'
};
}
return null;
} catch (error) {
console.error('Error fetching new enterprises:', error);
return null;
}
}
/**
* Get employment statistics by industry
* Table 11656: Employment and wages by industry
*/
async getEmploymentByIndustry(): Promise<any> {
try {
const tables = await this.searchTables('sysselsatte næring', 10);
const table = tables.find(t => t.id === '11656' || t.id === '13926');
if (table) {
return {
tableId: table.id,
label: table.label,
period: `${table.firstPeriod}-${table.lastPeriod}`,
description: 'Employment and wages across industry sectors'
};
}
return null;
} catch (error) {
console.error('Error fetching employment data:', error);
return null;
}
}
/**
* Get production and income statistics by industry
* Table 09170: Production and income by industry
*/
async getProductionByIndustry(): Promise<any> {
try {
const tables = await this.searchTables('produksjon næring', 10);
const table = tables.find(t => t.id === '09170');
if (table) {
return {
tableId: table.id,
label: table.label,
period: `${table.firstPeriod}-${table.lastPeriod}`,
description: 'Production and income by industry sector (1970-2024)'
};
}
return null;
} catch (error) {
console.error('Error fetching production data:', error);
return null;
}
}
/**
* Get innovation statistics by industry
*/
async getInnovationStats(): Promise<any> {
try {
const tables = await this.searchTables('innovasjon næring', 10);
return tables.slice(0, 3).map(t => ({
tableId: t.id,
label: t.label,
period: `${t.firstPeriod}-${t.lastPeriod}`
}));
} catch (error) {
console.error('Error fetching innovation stats:', error);
return [];
}
}
/**
* Get regional industry statistics
*/
async getRegionalStats(region?: string): Promise<any> {
try {
const tables = await this.searchTables('regional næring', 10);
return tables.slice(0, 3).map(t => ({
tableId: t.id,
label: t.label,
period: `${t.firstPeriod}-${t.lastPeriod}`
}));
} catch (error) {
console.error('Error fetching regional stats:', error);
return [];
}
}
/**
* Get comprehensive industry context
*/
async getIndustryContext(naceCode?: string): Promise<any> {
try {
const [highGrowth, employment, production, newEnterprises] = await Promise.all([
this.getHighGrowthEnterprises(naceCode),
this.getEmploymentByIndustry(),
this.getProductionByIndustry(),
this.getNewEnterprises()
]);
return {
highGrowth,
employment,
production,
newEnterprises,
summary: 'SSB provides comprehensive statistics on Norwegian enterprises, industries, and economic indicators'
};
} catch (error) {
console.error('Error fetching industry context:', error);
return null;
}
}
/**
* Get general economic indicators
*/
async getEconomicIndicators(): Promise<any> {
try {
const tables = await this.searchTables('økonomi', 10);
return tables.slice(0, 5).map(t => ({
tableId: t.id,
label: t.label,
period: `${t.firstPeriod}-${t.lastPeriod}`
}));
} catch (error) {
console.error('Error fetching economic indicators:', error);
return [];
}
}
/**
* Get metadata for a specific table
*/
async getTableMetadata(tableId: string): Promise<SSBTableMetadata | null> {
try {
const data = await this.fetch(`/tables/${tableId}/metadata`);
return {
title: data.title,
variables: data.variables?.map((v: any) => ({
code: v.code,
text: v.text,
values: v.values || [],
valueTexts: v.valueTexts || []
})) || []
};
} catch (error) {
console.error(`Error fetching metadata for table ${tableId}:`, error);
return null;
}
}
/**
* Fetch actual data from an SSB table with filtering
*/
async fetchTableData(
tableId: string,
filters?: {
naceCode?: string;
region?: string;
year?: string;
limit?: number;
}
): Promise<any> {
try {
// First get metadata to understand table structure
const metadata = await this.getTableMetadata(tableId);
if (!metadata) return null;
// Build query based on filters
const query: SSBDataQuery = {
query: [],
response: {
format: 'json-stat2'
}
};
// Add filters for available dimensions
metadata.variables.forEach(variable => {
if (filters?.naceCode && variable.code.toLowerCase().includes('næring')) {
query.query.push({
code: variable.code,
selection: {
filter: 'item',
values: [filters.naceCode]
}
});
} else if (filters?.region && variable.code.toLowerCase().includes('region')) {
query.query.push({
code: variable.code,
selection: {
filter: 'item',
values: [filters.region]
}
});
} else if (filters?.year && variable.code.toLowerCase().includes('år')) {
query.query.push({
code: variable.code,
selection: {
filter: 'item',
values: [filters.year]
}
});
} else {
// Include all values for unfiltered dimensions
query.query.push({
code: variable.code,
selection: {
filter: 'item',
values: variable.values.slice(0, filters?.limit || 10)
}
});
}
});
// Fetch data
const data = await this.fetch(`/tables/${tableId}/data`, {
method: 'POST',
body: JSON.stringify(query)
});
return data;
} catch (error) {
console.error(`Error fetching data from table ${tableId}:`, error);
return null;
}
}
/**
* Get time series data for trend analysis
*/
async getTimeSeries(
tableId: string,
filters?: {
naceCode?: string;
region?: string;
}
): Promise<SSBTimeSeries[]> {
try {
const data = await this.fetchTableData(tableId, filters);
if (!data || !data.value) return [];
// Parse JSON-STAT2 format
const dimension = data.dimension || {};
const timeVar = Object.keys(dimension).find(k =>
k.toLowerCase().includes('år') || k.toLowerCase().includes('tid')
);
if (!timeVar) return [];
const timeDimension = dimension[timeVar];
const periods = timeDimension.category?.index || [];
const values = data.value || [];
return Object.keys(periods).map((period, idx) => ({
period,
value: values[idx] || 0
}));
} catch (error) {
console.error('Error getting time series:', error);
return [];
}
}
/**
* Calculate growth trends from time series data
*/
analyzeTrend(timeSeries: SSBTimeSeries[]): SSBTrendAnalysis | null {
if (timeSeries.length < 2) return null;
const sorted = [...timeSeries].sort((a, b) => a.period.localeCompare(b.period));
const startValue = sorted[0].value;
const endValue = sorted[sorted.length - 1].value;
const totalChange = endValue - startValue;
const percentageChange = (totalChange / startValue) * 100;
// Calculate average period-over-period growth
let totalGrowth = 0;
for (let i = 1; i < sorted.length; i++) {
const growth = ((sorted[i].value - sorted[i - 1].value) / sorted[i - 1].value) * 100;
totalGrowth += growth;
}
const averageGrowth = totalGrowth / (sorted.length - 1);
// Determine direction
let direction: 'increasing' | 'decreasing' | 'stable';
if (percentageChange > 5) direction = 'increasing';
else if (percentageChange < -5) direction = 'decreasing';
else direction = 'stable';
return {
direction,
percentageChange: Math.round(percentageChange * 10) / 10,
averageGrowth: Math.round(averageGrowth * 10) / 10,
periods: sorted.length,
startValue: Math.round(startValue),
endValue: Math.round(endValue)
};
}
/**
* Get high-growth enterprise data with actual numbers
*/
async getHighGrowthData(naceCode?: string, region?: string): Promise<any> {
const tableId = '13706'; // High-growth firms by industry
const filters = { naceCode, region };
// Check cache first
if (this.db) {
const cached = this.db.getSSBCache(tableId, filters);
if (cached) {
console.error(`SSB cache hit for table ${tableId}`);
return {
tableId,
description: 'High-growth enterprises and gazelles by industry sector',
period: cached.period,
timeSeries: cached.time_series,
trend: cached.trend_analysis,
latestValue: cached.latest_value,
cached: true
};
}
}
try {
const timeSeries = await this.getTimeSeries(tableId, { naceCode, region });
if (timeSeries.length === 0) {
// Fallback to metadata only
return await this.getHighGrowthEnterprises(naceCode);
}
const trend = this.analyzeTrend(timeSeries);
const result = {
tableId,
description: 'High-growth enterprises and gazelles by industry sector',
period: `${timeSeries[0]?.period}-${timeSeries[timeSeries.length - 1]?.period}`,
timeSeries,
trend,
latestValue: timeSeries[timeSeries.length - 1]?.value
};
// Cache the result
if (this.db) {
this.db.cacheSSBData({
table_id: tableId,
table_name: 'High-growth enterprises',
category: 'enterprise',
filters,
nace_code: naceCode,
region,
data: result,
time_series: timeSeries,
trend_analysis: trend
});
}
return result;
} catch (error) {
console.error('Error fetching high-growth data:', error);
return await this.getHighGrowthEnterprises(naceCode);
}
}
/**
* Get employment data with trends
*/
async getEmploymentData(naceCode?: string, region?: string): Promise<any> {
try {
const tableId = '11656'; // Employment and wages by industry
const timeSeries = await this.getTimeSeries(tableId, { naceCode, region });
if (timeSeries.length === 0) {
return await this.getEmploymentByIndustry();
}
const trend = this.analyzeTrend(timeSeries);
return {
tableId,
description: 'Employment and wages across industry sectors',
period: `${timeSeries[0]?.period}-${timeSeries[timeSeries.length - 1]?.period}`,
timeSeries: timeSeries.slice(-5), // Last 5 periods
trend,
latestValue: timeSeries[timeSeries.length - 1]?.value,
change: trend ? `${trend.direction === 'increasing' ? '+' : ''}${trend.percentageChange}%` : 'N/A'
};
} catch (error) {
console.error('Error fetching employment data:', error);
return await this.getEmploymentByIndustry();
}
}
}