Refactors loading function

* Initialize service component and handler in hooks
* Dynamically watch the config instead of a reload on each page
* Uses derived store for public server config
* Moves service data in its own page data property
* Reverts to a array to store group/services
This commit is contained in:
2023-08-13 13:59:31 +02:00
parent f23f268b60
commit edbbd2f0f6
13 changed files with 280 additions and 248 deletions

View File

@@ -1,99 +0,0 @@
import configData from '../config.yml';
export interface BrandConfig {
logo?: string;
icon?: string;
usemask?: boolean;
}
export interface SectionConfig extends BrandConfig {
title: string;
subtitle?: string;
}
export interface ServiceConfig extends SectionConfig {
url: string;
target?: string;
type?: string;
data?: Record<string, unknown>;
[x: string]: unknown;
}
export interface ServiceGroupConfig extends SectionConfig {
items: Record<string, ServiceConfig>;
[x: string]: unknown;
}
export interface Config extends SectionConfig {
services: Record<string, ServiceGroupConfig>;
[x: string]: unknown;
}
type DeepRequired<T> = T extends object
? {
[P in keyof T]: DeepRequired<T[P]>;
}
: T;
const requiredConfig: DeepRequired<Config> = {
title: '',
subtitle: '',
services: {
default: {
title: '',
items: {
default: {
title: '',
url: ''
}
}
}
}
};
export function mergeConfig(a: Config, b: any): Config {
return { ...a, ...b };
}
export const defaultConfig: Config = {
title: 'Flanders',
services: {}
};
type SPOJO = Record<string, unknown>;
function strip<Type extends SPOJO>(toStrip: Type, reference: Type): Type {
const res: Type = { ...toStrip };
const referenceNames = Object.entries(reference).map(([key, value]) => key);
const allowAny: boolean = referenceNames.length == 1 && referenceNames[0] == 'default';
for (const [key, value] of Object.entries(res)) {
if (referenceNames.includes(key) == false && allowAny == false) {
// remove the object
delete res[key];
continue;
}
if (typeof value != 'object') {
continue;
}
// it is a child object, we strip it further
const stripped: SPOJO = {};
const childReference: SPOJO = reference[allowAny ? 'default' : key] as SPOJO;
stripped[key] = strip(value as SPOJO, childReference);
Object.assign(res, stripped);
}
return res;
}
export function stripPrivateFields(config: Config): Config {
return strip(config, requiredConfig);
}
export const config: Config = mergeConfig(defaultConfig, configData);
export const clientConfig: Config = stripPrivateFields(config);

View File

@@ -1,9 +1,17 @@
import { describe, expect, it } from 'vitest';
import { config, defaultConfig, mergeConfig, type Config, stripPrivateFields } from './config';
import {
defaultConfig,
mergeConfig,
type Config,
stripPrivateFields,
serverConfig,
clientConfig
} from './config';
describe('Config', () => {
it('should be export a build time config', () => {
expect(config).toBeTruthy();
it('should be export a build time server and client config store', () => {
expect(serverConfig).toBeTruthy();
expect(clientConfig).toBeTruthy();
});
it('should be able to merge with POJO', () => {
@@ -34,25 +42,33 @@ describe('Config', () => {
const custom: Config = {
title: 'custom',
secret: { secret: 'some secret' },
services: {
top: {
services: [
{
title: 'top services',
secret: 'secret',
items: {
top: {
items: [
{
title: 'top service',
url: 'somewhere',
secret: 'secret'
secret: 'secret',
type: 'foo'
}
}
]
}
}
]
};
const stripped: Config = stripPrivateFields(custom);
expect(stripped.secret).toBeUndefined();
expect(stripped.services.top.secret).toBeUndefined();
expect(stripped.services.top.items.top.secret).toBeUndefined();
expect(custom.secret).toEqual({ secret: 'some secret' });
expect(stripped.services[0].secret).toBeUndefined();
expect(custom.services[0].secret).toEqual('secret');
expect(stripped.services[0].title).toEqual('top services');
expect(stripped.services[0].items[0].secret).toBeUndefined();
expect(custom.services[0].items[0].secret).toEqual('secret');
expect(stripped.services[0].items[0].title).toEqual('top service');
expect(stripped.services[0].items[0].url).toEqual('somewhere');
expect(stripped.services[0].items[0].type).toEqual('foo');
});
});

146
src/lib/server/config.ts Normal file
View File

@@ -0,0 +1,146 @@
import { writable, type Readable, type Writable, derived } from 'svelte/store';
import configData from '../../config.yml';
import { readFile, watch } from 'fs/promises';
import * as yml from 'js-yaml';
export interface BrandConfig {
logo?: string;
icon?: string;
usemask?: boolean;
}
export interface SectionConfig extends BrandConfig {
title: string;
subtitle?: string;
}
export interface ServiceConfig extends SectionConfig {
url: string;
target?: string;
type?: string;
[x: string]: unknown;
}
export interface ServiceGroupConfig extends SectionConfig {
items: Array<ServiceConfig>;
[x: string]: unknown;
}
export interface Config extends SectionConfig {
services: Array<ServiceGroupConfig>;
[x: string]: unknown;
}
const requiredService: Required<ServiceConfig> = {
title: '',
subtitle: '',
logo: '',
icon: '',
usemask: false,
url: '',
target: '',
type: ''
};
const requiredServiceGroup: Required<ServiceGroupConfig> = {
title: '',
subtitle: '',
logo: '',
icon: '',
usemask: false,
items: [requiredService]
};
export const requiredConfig: Required<Config> = {
title: '',
subtitle: '',
logo: '',
icon: '',
usemask: false,
services: [requiredServiceGroup]
};
export function mergeConfig(a: Config, b: any): Config {
return { ...a, ...b };
}
export const defaultConfig: Config = {
title: 'Flanders',
services: []
};
type SPOJO = Record<string, unknown>;
function strip<Type extends SPOJO>(toStrip: Type, reference: Type): Type {
const res: Type = { ...toStrip };
const referenceNames = Object.entries(reference).map(([key, value]) => key);
for (const [key, value] of Object.entries(res)) {
if (referenceNames.includes(key) == false) {
// remove the object
delete res[key];
continue;
}
if (typeof value != 'object') {
continue;
}
if (value instanceof Array) {
const newValue = [];
const childReference: SPOJO = (reference[key] as Array<SPOJO>)[0];
for (const child of value) {
newValue.push(strip(child, childReference));
}
(res as SPOJO)[key] = newValue;
continue;
}
// it is a child object, we strip it further
const stripped: SPOJO = {};
const childReference: SPOJO = (reference[key] as Array<SPOJO>)[0];
stripped[key] = strip(value as SPOJO, childReference);
(res as SPOJO)[key] = stripped;
}
return res;
}
export function stripPrivateFields(config: Config): Config {
return strip(config, requiredConfig);
}
export const serverConfig: Writable<Config> = writable(mergeConfig(defaultConfig, configData));
export const clientConfig: Readable<Config> = derived(serverConfig, ($config) =>
stripPrivateFields($config)
);
export function watchDymamicConfig() {
const __filepath = '/dynamic/config.yml';
const reloadConfig = async () => {
try {
const dynamicConfig = yml.load(await readFile(__filepath, 'utf8'));
serverConfig.set(mergeConfig(defaultConfig, dynamicConfig));
} catch (err) {
console.error('could not read or parse config: ' + err);
}
};
reloadConfig();
(async () => {
try {
const watcher = watch(__filepath);
for await (const event of watcher) {
reloadConfig();
}
} catch (err) {
console.error('could not watch config: ' + err);
return;
}
})();
}

View File

@@ -1,9 +1,7 @@
import { type ServiceHandler } from '../service';
import { type ServiceComponentPath, type ServiceHandler } from '../service';
export const handle: ServiceHandler = ({ config }) => {
const data = {};
if (config.apikey != undefined) {
//TODO: fetch data
}
return { data, componentPath: 'pihole/PiHoleContent.svelte' };
export const handle: ServiceHandler = () => {
return { piholedata: 'coucou' };
};
export const path: ServiceComponentPath = 'pihole/PiHoleContent.svelte';

View File

@@ -1,5 +1,7 @@
import type { ServiceHandler } from '../service';
import type { ServiceComponentPath, ServiceHandler } from '../service';
export const handle: ServiceHandler = () => {
return { data: {}, componentPath: 'prowlarr/ProwlarrContent.svelte' };
return { prowlardata: 'coucou' };
};
export const path: ServiceComponentPath = 'prowlarr/ProwlarrContent.svelte';

View File

@@ -5,7 +5,6 @@ interface ServiceHandlerArgs {
config: ServiceConfig;
}
export type ServiceHandler = (input: ServiceHandlerArgs) => {
data: Record<string, unknown>;
componentPath: string;
};
export type ServiceHandler = (input: ServiceHandlerArgs) => Record<string, unknown>;
export type ServiceComponentPath = string;

View File

@@ -1,33 +1,43 @@
import type { ServiceHandler } from './service';
const services: Record<string, [ServiceHandler, string]> = {};
type ServiceRecord = {
handler: ServiceHandler;
component: any;
};
function registerService(type: string, handler: ServiceHandler) {
services[type] = [handler, type];
const services: Record<string, ServiceRecord> = {};
function registerService(type: string, handler: ServiceHandler, component: any) {
services[type] = { handler, component };
}
export function getService(type: string): [ServiceHandler, string] {
const handler = services[type];
if (handler == undefined) {
return [
() => {
return { data: {}, componentPath: '' };
},
''
];
}
return handler;
export function getServiceHandler(type: string): ServiceHandler | undefined {
return services[type]?.handler;
}
export function getServiceComponent(type: string): any {
return services[type]?.component;
}
export async function initServices() {
const services = import.meta.glob('/src/lib/services/**/+service.ts');
for (const [path, load] of Object.entries(services)) {
const { handle } = (await load()) as any;
if (handle == undefined) {
continue;
for (const [modulePath, load] of Object.entries(services)) {
try {
const { handle, path } = (await load()) as any;
if (handle == undefined) {
throw new Error(`${modulePath} does not export 'handle'`);
}
if (path == undefined) {
throw new Error(`${modulePath} does not export 'path'`);
}
const module = await import(/* @vite-ignore */ '/src/lib/services/' + path);
const typeName = modulePath.slice(18, -12);
registerService(typeName, handle, module.default);
} catch (err) {
console.error(`Could not load service definition from '${modulePath}': ${err}`);
}
const typeName = path.slice(18, -12);
registerService(typeName, handle);
}
}