Skip to content

Commit

Permalink
implement validators into schema validation service
Browse files Browse the repository at this point in the history
  • Loading branch information
dskvr committed Jan 8, 2025
1 parent 5eccf5b commit c1d0609
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 371 deletions.
3 changes: 2 additions & 1 deletion apps/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
"@nostrwatch/nocap": "workspace:^",
"@nostrwatch/nocap-websocket-adapter-default": "workspace:^",
"@nostrwatch/utils": "workspace:^",
"@nostrwatch/worker-relay": "^1.3.0",
"@nostrwatch/worker-relay": "workspace:^",
"@nostrwatch/schemata-js-ajv": "workspace:^",
"@unovis/svelte": "^1.4.5",
"@unovis/ts": "^1.4.5",
"ajv": "^8.17.1",
Expand Down
31 changes: 21 additions & 10 deletions apps/gui/src/lib/components/lists/table/DataTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,24 @@
}
const createTable = (force: boolean = false) => {
const config: any = {
pageSize: $resultsPerPage,
columns: $filteredTableData.columns,
data: $filteredTableData.data,
}
if(sortState) {
if(sortState?.columnId) {
config.initialSort = sortState.columnId
}
if(sortState?.direction) {
config.initialSortDirection = sortState.direction
}
}
console.log(`Creating DataTable instance with ${$filteredTableData.data.length} rows.`, config);
if ($filteredTableData && $filteredTableData.columns && $filteredTableData.columns.length) {
if(tableInstance === null || force){
tableInstance = new DataTable<any>({
pageSize: $resultsPerPage,
columns: $filteredTableData.columns,
data: $filteredTableData.data,
});
tableInstance = new DataTable<any>(config);
}
} else {
if (tableInstance) {
Expand Down Expand Up @@ -237,13 +248,13 @@
<DataTablePaginator {tableInstance} />
<Popover.Root>
<Popover.Trigger class="text-lg inline-block ml-2 relative -top-1">⚙</Popover.Trigger>
<Popover.Content class="z-[5999]">
<Tabs.Root value="visiblity" class="w-full">
<Popover.Content class="z-[5999] mt-3 min-w-[600px] backdrop-blur-md bg-black/50">
<Tabs.Root value="visiblity" class="">
<Tabs.List>
<Tabs.Trigger value="visiblity">Visiblity</Tabs.Trigger>
<Tabs.Trigger value="order">Order</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="visiblity">
<Tabs.Content value="visiblity" class="py-4 px-8">
<TableOptions {config} {tableKey} />
</Tabs.Content>
<Tabs.Content value="order" class=" text-white/20">
Expand All @@ -266,8 +277,8 @@
class="flex items-center"
on:click={() => {
if(tableInstance) {
const sortState = tableInstance.toggleSort(column.id)
StateManager.set('sortState:relays', sortState)
tableInstance.toggleSort(column.id)
StateManager.set('sortState:relays', tableInstance.sortState)
}
}}
disabled={!tableInstance?.isSortable(column.id)}
Expand Down
105 changes: 60 additions & 45 deletions apps/gui/src/lib/components/lists/table/TableOptions.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<script lang="ts">
import { Nip66Event } from "@nostrwatch/nip66/models";
import Checkbox from "../../ui/checkbox/checkbox.svelte";
import { StateManager } from "@nostrwatch/nip66";
import type { Writable } from "svelte/store";
import { capitalize } from "@nostrwatch/utils";
import type { Formatters } from "src/lib/config/dataTable/monitors";
import Checkbox from "../../ui/checkbox/checkbox.svelte";
import { StateManager } from "@nostrwatch/nip66";
import type { Writable } from "svelte/store";
import { capitalize } from "@nostrwatch/utils";
import type { Formatters } from "src/lib/config/dataTable/monitors";
export let tableKey: string;
export let config: Writable<DataTableConfig | null>;
type DataTableConfig = {
columnsDisable: string[]
columnsDisable: string[]
columnsShow: string[]
filtersDisable: string[]
filtersShow: string[]
Expand All @@ -19,52 +19,67 @@
tableFormatters: Formatters
filterFormatters: Formatters
tableRowStyler: (row: any) => string
}
}
$: availableKeys = [
...($config?.columnsDisable? Nip66Event.keys.filter(key => !$config.columnsDisable.includes(key)): Nip66Event.keys),
'seenBy',
'lastSeen',
'seenTimes'
];
...($config?.columnsDisable
? Nip66Event.keys.filter(key => !$config.columnsDisable.includes(key))
: Nip66Event.keys),
"seenBy",
"lastSeen",
"seenTimes"
];
const toggleColumnShow = (key: string) => {
config.update( (currentConfig: DataTableConfig) => {
if (!currentConfig || !Array.isArray(currentConfig.columnsShow)) {
console.error('columnsShow is not an array');
return currentConfig;
}
let newColumnsShow: string[];
if (currentConfig.columnsShow.includes(key)) {
newColumnsShow = currentConfig.columnsShow.filter(k => k !== key);
} else {
newColumnsShow = [...currentConfig.columnsShow, key];
}
const sortedColumnsShow = availableKeys.filter(k => newColumnsShow.includes(k));
const newConfig = { ...currentConfig, columnsShow: sortedColumnsShow }
config.update((currentConfig: DataTableConfig) => {
if (!currentConfig || !Array.isArray(currentConfig.columnsShow)) {
console.error("columnsShow is not an array");
return currentConfig;
}
let newColumnsShow: string[];
if (currentConfig.columnsShow.includes(key)) {
newColumnsShow = currentConfig.columnsShow.filter(k => k !== key);
} else {
newColumnsShow = [...currentConfig.columnsShow, key];
}
const sortedColumnsShow = availableKeys.filter(k => newColumnsShow.includes(k));
const newConfig = { ...currentConfig, columnsShow: sortedColumnsShow };
const tableConfigCache = StateManager.get(`preferences:${tableKey}:tableConfig`);
StateManager.set(`preferences:${tableKey}:tableConfig`, { ...tableConfigCache, columnsShow: sortedColumnsShow });
return { ...currentConfig, columnsShow: sortedColumnsShow };
});
}
StateManager.set(`preferences:${tableKey}:tableConfig`, {
...tableConfigCache,
columnsShow: sortedColumnsShow
});
return newConfig;
});
};
</script>

{#each availableKeys as key}
<li>
<Checkbox
checked={$config?.columnsShow.includes(key)}
onCheckedChange={() => toggleColumnShow(key)}
value={key}
class="mr-2"
/>
{$config?.humanReadableNames?.[key] || capitalize(key)}
</li>
{/each}
<ul class="columns">
{#each availableKeys as key}
<li>
<Checkbox
checked={$config?.columnsShow.includes(key)}
onCheckedChange={() => toggleColumnShow(key)}
value={key}
class="mr-2"
/>
{$config?.humanReadableNames?.[key] || capitalize(key)}
</li>
{/each}
</ul>

<style lang="postcss">
li {
@apply flex text-center py-2 px-3;
}
</style>
.columns {
column-count: 3;
column-gap: 1.5rem;
max-width: 960px;
margin: 0 auto;
}
.columns li {
break-inside: avoid;
display: block;
margin-bottom: 0.5rem;
}
</style>
94 changes: 93 additions & 1 deletion apps/gui/src/lib/services/SchemaValidationService/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,97 @@
import { deterministicHash } from '@nostrwatch/nip66/utils/hash';
import type { NostrEvent } from 'nostr-tools';
import { get } from 'svelte/store';
import { EventEmitter } from 'tseep'

export type SchemaValidationServiceRequest = {
type: 'nip11' | 'message' | 'note',
json: string,
subject?: string,
slug?: string,
hash?: string
}

export type SchemaValidationServiceResponse = {
status: 'success' | 'error',
result: string,
hash: string;
error?: any
}

export class SchemaValidationService {
worker: Worker;
private _subIds: Set<string> = new Set();
private emitter = new EventEmitter();

constructor(){
this.worker = new Worker(new URL('./schemavalidation.worker.ts', import.meta.url), { type: 'module' });
this.worker.onmessage = this.onmessage.bind(this);
}

get subIds(): Set<string> {
return this._subIds;
}

emitterKey(hash: string) {
return `schemaValidation:${hash}`
}

async respond(hash: string): Promise<SchemaValidationServiceResponse> {
return new Promise((resolve, reject) => {
const timeout = setTimeout( () => reject({ status: 'error', hash, error: 'Request timed out, worker may have been terminated.' }), 5000)
this.emitter.once(this.emitterKey(hash), (response: SchemaValidationServiceResponse) => {
clearTimeout(timeout)
const { result, error } = response;
if(error) {
reject(response)
}
else {
resolve(response)
}
})
})
}

async validate(request: SchemaValidationServiceRequest, hash?: string): Promise<SchemaValidationServiceResponse> {
hash = hash ?? deterministicHash(request.json)
this._subIds.add(hash)
this.worker.postMessage(request)
return this.respond(hash)
}

constructor(){}
async validateNip11(nip11: string): Promise<SchemaValidationServiceResponse> {
const request: SchemaValidationServiceRequest = {
type: 'nip11',
json: nip11
}
return this.validate(request)
}

async validateMessage(json: string, subject: string, slug: string): Promise<SchemaValidationServiceResponse> {
const request: SchemaValidationServiceRequest = {
type: 'message',
subject,
slug,
json
}
return this.validate(request)
}

async validateNote(json: any): Promise<SchemaValidationServiceResponse> {
let hash: string | undefined;
if(json?.id) {
hash = json.id;
}
const request: SchemaValidationServiceRequest = {
type: 'note',
json,
hash
}
return this.validate(request, hash)
}

private onmessage(message: MessageEvent<SchemaValidationServiceResponse>){
const { hash } = message.data;
this.emitter.emit(this.emitterKey(hash), message.data)
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import Ajv from 'ajv'

const validate = async (relay: string, checks: string[]): Promise<any> => {
const nocap = new Nocap(relay)
return nocap.check(checks)
}
import { deterministicHash } from '@nostrwatch/nip66/utils/hash';
import { type SchemaValidatorResult } from '@nostrwatch/schemata-js-ajv'

self.onmessage = ({ data }) => {
const { relay, checks } = data as NocapRequestMessage;
check(relay, checks)
.then( (results: any) => {
const message: NocapResultMessage = {relay, results}
self.postMessage(message)
})
.catch( (error: any) => {
const message: NocapResultMessage = {relay, error}
self.postMessage(message)
})
import('@nostrwatch/schemata-js-ajv').then( ({validateNip11, validateMessage, validateNote}) => {
const { json, type, subject, slug } = data as any;
let { hash } = data as any;
let result: SchemaValidatorResult;
let error: string = '';
if(!hash) {
hash = deterministicHash(json)
}
if(type === 'nip11') {
result = validateNip11(json)
}
else if(type === 'message') {
if(subject && slug) {
result = validateMessage(json, subject, slug)
}
else {
error = 'Both subject and slug are required for message validation (for example subject "relay" and slug "ok"'
}
}
else if(type === 'note') {
result = validateNote(json)
}
if(result) {
self.postMessage({
status: 'success',
hash,
result
})
}
else if(error){
self.postMessage({
status: 'error',
hash,
error
})
}
});
}
Loading

0 comments on commit c1d0609

Please sign in to comment.