@lde/sparql-monitor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # SPARQL Monitor
2
+
3
+ Monitor SPARQL endpoints with periodic checks, storing observations in PostgreSQL.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @lde/sparql-monitor
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import {
15
+ MonitorService,
16
+ PostgresObservationStore,
17
+ type MonitorConfig,
18
+ } from '@lde/sparql-monitor';
19
+
20
+ // Define monitors
21
+ const monitors: MonitorConfig[] = [
22
+ {
23
+ identifier: 'dbpedia',
24
+ endpointUrl: new URL('https://dbpedia.org/sparql'),
25
+ query: 'ASK { ?s ?p ?o }',
26
+ },
27
+ {
28
+ identifier: 'wikidata',
29
+ endpointUrl: new URL('https://query.wikidata.org/sparql'),
30
+ query: 'SELECT * WHERE { ?s ?p ?o } LIMIT 1',
31
+ },
32
+ ];
33
+
34
+ // Create store (initializes database schema automatically)
35
+ const store = await PostgresObservationStore.create(
36
+ 'postgres://user:pass@localhost:5432/db'
37
+ );
38
+
39
+ // Create service with polling interval
40
+ const service = new MonitorService({
41
+ store,
42
+ monitors,
43
+ intervalSeconds: 300, // Check all endpoints every 5 minutes
44
+ });
45
+
46
+ // Start periodic monitoring
47
+ service.start();
48
+
49
+ // Or perform immediate checks
50
+ await service.checkAll();
51
+ await service.checkNow('dbpedia');
52
+
53
+ // Get latest observations
54
+ const observations = await store.getLatest();
55
+ for (const [identifier, observation] of observations) {
56
+ console.log(
57
+ `${identifier}: ${observation.success ? 'OK' : 'FAIL'} (${
58
+ observation.responseTimeMs
59
+ }ms)`
60
+ );
61
+ }
62
+
63
+ // Stop monitoring and close the store
64
+ service.stop();
65
+ await store.close();
66
+ ```
67
+
68
+ ## Database Initialisation
69
+
70
+ `PostgresObservationStore.create()` automatically initializes the database schema:
71
+
72
+ - `observations` table for storing check results
73
+ - `latest_observations` materialized view for efficient queries
74
+ - Required indexes
75
+
76
+ This is idempotent and safe to call on every startup.
77
+
78
+ ## Query Types
79
+
80
+ The monitor supports ASK, SELECT, and CONSTRUCT queries. The check is considered successful if the query executes without error.
@@ -0,0 +1,4 @@
1
+ export type { MonitorConfig, Observation, ObservationStore } from './types.js';
2
+ export { PostgresObservationStore } from './store.js';
3
+ export { MonitorService, type MonitorServiceOptions } from './service.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { PostgresObservationStore } from './store.js';
2
+ export { MonitorService } from './service.js';
@@ -0,0 +1,23 @@
1
+ import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
2
+ import type { CheckResult } from './types.js';
3
+ export interface SparqlMonitorOptions {
4
+ /** Optional custom fetcher instance. */
5
+ fetcher?: SparqlEndpointFetcher;
6
+ /** Timeout in milliseconds for the SPARQL request. */
7
+ timeoutMs?: number;
8
+ /** HTTP headers to include in requests (e.g., User-Agent). */
9
+ headers?: Headers;
10
+ }
11
+ /**
12
+ * Executes SPARQL queries against an endpoint and measures response time.
13
+ */
14
+ export declare class SparqlMonitor {
15
+ private readonly fetcher;
16
+ constructor(options?: SparqlMonitorOptions);
17
+ /**
18
+ * Execute a SPARQL query against an endpoint and return the result.
19
+ */
20
+ check(endpointUrl: URL, query: string): Promise<CheckResult>;
21
+ private consumeStream;
22
+ }
23
+ //# sourceMappingURL=monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../src/monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,wCAAwC;IACxC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,OAAO,CAAC,EAAE,oBAAoB;IAS1C;;OAEG;IACG,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;YA8CpD,aAAa;CAS5B"}
@@ -0,0 +1,64 @@
1
+ import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
2
+ /**
3
+ * Executes SPARQL queries against an endpoint and measures response time.
4
+ */
5
+ export class SparqlMonitor {
6
+ fetcher;
7
+ constructor(options) {
8
+ this.fetcher =
9
+ options?.fetcher ??
10
+ new SparqlEndpointFetcher({
11
+ timeout: options?.timeoutMs ?? 30000,
12
+ defaultHeaders: options?.headers,
13
+ });
14
+ }
15
+ /**
16
+ * Execute a SPARQL query against an endpoint and return the result.
17
+ */
18
+ async check(endpointUrl, query) {
19
+ const observedAt = new Date(); // UTC
20
+ const startTime = performance.now();
21
+ try {
22
+ const queryType = this.fetcher.getQueryType(query);
23
+ switch (queryType) {
24
+ case 'ASK':
25
+ await this.fetcher.fetchAsk(endpointUrl.toString(), query);
26
+ break;
27
+ case 'SELECT':
28
+ await this.consumeStream(await this.fetcher.fetchBindings(endpointUrl.toString(), query));
29
+ break;
30
+ case 'CONSTRUCT':
31
+ await this.consumeStream(await this.fetcher.fetchTriples(endpointUrl.toString(), query));
32
+ break;
33
+ default:
34
+ throw new Error(`Unsupported query type: ${queryType}`);
35
+ }
36
+ const responseTimeMs = Math.round(performance.now() - startTime);
37
+ return {
38
+ success: true,
39
+ responseTimeMs,
40
+ errorMessage: null,
41
+ observedAt,
42
+ };
43
+ }
44
+ catch (error) {
45
+ const responseTimeMs = Math.round(performance.now() - startTime);
46
+ const errorMessage = error instanceof Error ? error.message : String(error);
47
+ return {
48
+ success: false,
49
+ responseTimeMs,
50
+ errorMessage,
51
+ observedAt,
52
+ };
53
+ }
54
+ }
55
+ async consumeStream(stream) {
56
+ return new Promise((resolve, reject) => {
57
+ stream.on('data', () => {
58
+ // Just consume the data
59
+ });
60
+ stream.on('end', () => resolve());
61
+ stream.on('error', reject);
62
+ });
63
+ }
64
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Observations table — maps to SOSA Observation concept.
3
+ */
4
+ export declare const observations: import("drizzle-orm/pg-core").PgTableWithColumns<{
5
+ name: "observations";
6
+ schema: undefined;
7
+ columns: {
8
+ id: import("drizzle-orm/pg-core").PgColumn<{
9
+ name: "id";
10
+ tableName: "observations";
11
+ dataType: "string";
12
+ columnType: "PgUUID";
13
+ data: string;
14
+ driverParam: string;
15
+ notNull: true;
16
+ hasDefault: true;
17
+ isPrimaryKey: true;
18
+ isAutoincrement: false;
19
+ hasRuntimeDefault: false;
20
+ enumValues: undefined;
21
+ baseColumn: never;
22
+ identity: undefined;
23
+ generated: undefined;
24
+ }, {}, {}>;
25
+ observedAt: import("drizzle-orm/pg-core").PgColumn<{
26
+ name: "observed_at";
27
+ tableName: "observations";
28
+ dataType: "date";
29
+ columnType: "PgTimestamp";
30
+ data: Date;
31
+ driverParam: string;
32
+ notNull: true;
33
+ hasDefault: true;
34
+ isPrimaryKey: false;
35
+ isAutoincrement: false;
36
+ hasRuntimeDefault: false;
37
+ enumValues: undefined;
38
+ baseColumn: never;
39
+ identity: undefined;
40
+ generated: undefined;
41
+ }, {}, {}>;
42
+ monitor: import("drizzle-orm/pg-core").PgColumn<{
43
+ name: "monitor";
44
+ tableName: "observations";
45
+ dataType: "string";
46
+ columnType: "PgText";
47
+ data: string;
48
+ driverParam: string;
49
+ notNull: true;
50
+ hasDefault: false;
51
+ isPrimaryKey: false;
52
+ isAutoincrement: false;
53
+ hasRuntimeDefault: false;
54
+ enumValues: [string, ...string[]];
55
+ baseColumn: never;
56
+ identity: undefined;
57
+ generated: undefined;
58
+ }, {}, {}>;
59
+ success: import("drizzle-orm/pg-core").PgColumn<{
60
+ name: "success";
61
+ tableName: "observations";
62
+ dataType: "boolean";
63
+ columnType: "PgBoolean";
64
+ data: boolean;
65
+ driverParam: boolean;
66
+ notNull: true;
67
+ hasDefault: false;
68
+ isPrimaryKey: false;
69
+ isAutoincrement: false;
70
+ hasRuntimeDefault: false;
71
+ enumValues: undefined;
72
+ baseColumn: never;
73
+ identity: undefined;
74
+ generated: undefined;
75
+ }, {}, {}>;
76
+ responseTimeMs: import("drizzle-orm/pg-core").PgColumn<{
77
+ name: "response_time_ms";
78
+ tableName: "observations";
79
+ dataType: "number";
80
+ columnType: "PgInteger";
81
+ data: number;
82
+ driverParam: string | number;
83
+ notNull: true;
84
+ hasDefault: false;
85
+ isPrimaryKey: false;
86
+ isAutoincrement: false;
87
+ hasRuntimeDefault: false;
88
+ enumValues: undefined;
89
+ baseColumn: never;
90
+ identity: undefined;
91
+ generated: undefined;
92
+ }, {}, {}>;
93
+ errorMessage: import("drizzle-orm/pg-core").PgColumn<{
94
+ name: "error_message";
95
+ tableName: "observations";
96
+ dataType: "string";
97
+ columnType: "PgText";
98
+ data: string;
99
+ driverParam: string;
100
+ notNull: false;
101
+ hasDefault: false;
102
+ isPrimaryKey: false;
103
+ isAutoincrement: false;
104
+ hasRuntimeDefault: false;
105
+ enumValues: [string, ...string[]];
106
+ baseColumn: never;
107
+ identity: undefined;
108
+ generated: undefined;
109
+ }, {}, {}>;
110
+ };
111
+ dialect: "pg";
112
+ }>;
113
+ /**
114
+ * SQL for refreshing the materialized view.
115
+ */
116
+ export declare const refreshLatestObservationsViewSql: import("drizzle-orm").SQL<unknown>;
117
+ /**
118
+ * Materialized view for the latest observation per monitor.
119
+ */
120
+ export declare const latestObservations: import("drizzle-orm/pg-core").PgMaterializedViewWithSelection<"latest_observations", false, {
121
+ id: import("drizzle-orm/pg-core").PgColumn<{
122
+ name: "id";
123
+ tableName: "latest_observations";
124
+ dataType: "string";
125
+ columnType: "PgUUID";
126
+ data: string;
127
+ driverParam: string;
128
+ notNull: true;
129
+ hasDefault: false;
130
+ isPrimaryKey: false;
131
+ isAutoincrement: false;
132
+ hasRuntimeDefault: false;
133
+ enumValues: undefined;
134
+ baseColumn: never;
135
+ identity: undefined;
136
+ generated: undefined;
137
+ }, {}, {}>;
138
+ monitor: import("drizzle-orm/pg-core").PgColumn<{
139
+ name: "monitor";
140
+ tableName: "latest_observations";
141
+ dataType: "string";
142
+ columnType: "PgText";
143
+ data: string;
144
+ driverParam: string;
145
+ notNull: true;
146
+ hasDefault: false;
147
+ isPrimaryKey: false;
148
+ isAutoincrement: false;
149
+ hasRuntimeDefault: false;
150
+ enumValues: [string, ...string[]];
151
+ baseColumn: never;
152
+ identity: undefined;
153
+ generated: undefined;
154
+ }, {}, {}>;
155
+ observedAt: import("drizzle-orm/pg-core").PgColumn<{
156
+ name: "observed_at";
157
+ tableName: "latest_observations";
158
+ dataType: "date";
159
+ columnType: "PgTimestamp";
160
+ data: Date;
161
+ driverParam: string;
162
+ notNull: true;
163
+ hasDefault: false;
164
+ isPrimaryKey: false;
165
+ isAutoincrement: false;
166
+ hasRuntimeDefault: false;
167
+ enumValues: undefined;
168
+ baseColumn: never;
169
+ identity: undefined;
170
+ generated: undefined;
171
+ }, {}, {}>;
172
+ success: import("drizzle-orm/pg-core").PgColumn<{
173
+ name: "success";
174
+ tableName: "latest_observations";
175
+ dataType: "boolean";
176
+ columnType: "PgBoolean";
177
+ data: boolean;
178
+ driverParam: boolean;
179
+ notNull: true;
180
+ hasDefault: false;
181
+ isPrimaryKey: false;
182
+ isAutoincrement: false;
183
+ hasRuntimeDefault: false;
184
+ enumValues: undefined;
185
+ baseColumn: never;
186
+ identity: undefined;
187
+ generated: undefined;
188
+ }, {}, {}>;
189
+ responseTimeMs: import("drizzle-orm/pg-core").PgColumn<{
190
+ name: "response_time_ms";
191
+ tableName: "latest_observations";
192
+ dataType: "number";
193
+ columnType: "PgInteger";
194
+ data: number;
195
+ driverParam: string | number;
196
+ notNull: true;
197
+ hasDefault: false;
198
+ isPrimaryKey: false;
199
+ isAutoincrement: false;
200
+ hasRuntimeDefault: false;
201
+ enumValues: undefined;
202
+ baseColumn: never;
203
+ identity: undefined;
204
+ generated: undefined;
205
+ }, {}, {}>;
206
+ errorMessage: import("drizzle-orm/pg-core").PgColumn<{
207
+ name: "error_message";
208
+ tableName: "latest_observations";
209
+ dataType: "string";
210
+ columnType: "PgText";
211
+ data: string;
212
+ driverParam: string;
213
+ notNull: false;
214
+ hasDefault: false;
215
+ isPrimaryKey: false;
216
+ isAutoincrement: false;
217
+ hasRuntimeDefault: false;
218
+ enumValues: [string, ...string[]];
219
+ baseColumn: never;
220
+ identity: undefined;
221
+ generated: undefined;
222
+ }, {}, {}>;
223
+ }>;
224
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAqBA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAexB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gCAAgC,oCAE5C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,36 @@
1
+ import { boolean, index, integer, pgMaterializedView, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
2
+ import { sql } from 'drizzle-orm';
3
+ const columns = {
4
+ id: uuid('id').notNull(),
5
+ monitor: text('monitor').notNull(),
6
+ observedAt: timestamp('observed_at', { mode: 'date' }).notNull(),
7
+ success: boolean('success').notNull(),
8
+ responseTimeMs: integer('response_time_ms').notNull(),
9
+ errorMessage: text('error_message'),
10
+ };
11
+ /**
12
+ * Observations table — maps to SOSA Observation concept.
13
+ */
14
+ export const observations = pgTable('observations', {
15
+ ...columns,
16
+ id: columns.id.primaryKey().defaultRandom(),
17
+ observedAt: columns.observedAt.defaultNow(),
18
+ }, (table) => [
19
+ index('observations_monitor_idx').on(table.monitor),
20
+ index('observations_observed_at_idx').on(table.observedAt),
21
+ index('observations_monitor_observed_at_idx').on(table.monitor, sql `${table.observedAt} DESC`),
22
+ ]);
23
+ /**
24
+ * SQL for refreshing the materialized view.
25
+ */
26
+ export const refreshLatestObservationsViewSql = sql `
27
+ REFRESH MATERIALIZED VIEW CONCURRENTLY latest_observations
28
+ `;
29
+ /**
30
+ * Materialized view for the latest observation per monitor.
31
+ */
32
+ export const latestObservations = pgMaterializedView('latest_observations', columns).as(sql `
33
+ SELECT DISTINCT ON (monitor) *
34
+ FROM ${observations}
35
+ ORDER BY monitor, observed_at DESC
36
+ `);
@@ -0,0 +1,50 @@
1
+ import { SparqlMonitor } from './monitor.js';
2
+ import type { ObservationStore, MonitorConfig } from './types.js';
3
+ export interface MonitorServiceOptions {
4
+ /** Store for persisting observations. */
5
+ store: ObservationStore;
6
+ /** Monitor configurations. */
7
+ monitors: MonitorConfig[];
8
+ /** Polling interval in seconds (default: 300). */
9
+ intervalSeconds?: number;
10
+ /** Optional custom monitor instance. */
11
+ sparqlMonitor?: SparqlMonitor;
12
+ }
13
+ /**
14
+ * Orchestrates monitoring of multiple SPARQL endpoints.
15
+ */
16
+ export declare class MonitorService {
17
+ private readonly store;
18
+ private readonly sparqlMonitor;
19
+ private readonly configs;
20
+ private readonly intervalSeconds;
21
+ private job;
22
+ constructor(options: MonitorServiceOptions);
23
+ /**
24
+ * Perform an immediate check for a monitor.
25
+ */
26
+ checkNow(identifier: string): Promise<void>;
27
+ /**
28
+ * Perform an immediate check for all monitors.
29
+ */
30
+ checkAll(): Promise<void>;
31
+ /**
32
+ * Start monitoring all configured endpoints.
33
+ */
34
+ start(): void;
35
+ /**
36
+ * Stop monitoring.
37
+ */
38
+ stop(): void;
39
+ /**
40
+ * Check whether monitoring is running.
41
+ */
42
+ isRunning(): boolean;
43
+ /**
44
+ * Convert seconds to a cron expression.
45
+ */
46
+ private secondsToCron;
47
+ private performCheck;
48
+ private refreshView;
49
+ }
50
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,yCAAyC;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,8BAA8B;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,GAAG,CAAwB;gBAEvB,OAAO,EAAE,qBAAqB;IAO1C;;OAEG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,OAAO,CAAC,aAAa;YAYP,YAAY;YAWZ,WAAW;CAO1B"}
@@ -0,0 +1,90 @@
1
+ import { CronJob } from 'cron';
2
+ import { SparqlMonitor } from './monitor.js';
3
+ /**
4
+ * Orchestrates monitoring of multiple SPARQL endpoints.
5
+ */
6
+ export class MonitorService {
7
+ store;
8
+ sparqlMonitor;
9
+ configs;
10
+ intervalSeconds;
11
+ job = null;
12
+ constructor(options) {
13
+ this.store = options.store;
14
+ this.sparqlMonitor = options.sparqlMonitor ?? new SparqlMonitor();
15
+ this.configs = options.monitors;
16
+ this.intervalSeconds = options.intervalSeconds ?? 300;
17
+ }
18
+ /**
19
+ * Perform an immediate check for a monitor.
20
+ */
21
+ async checkNow(identifier) {
22
+ const config = this.configs.find((c) => c.identifier === identifier);
23
+ if (!config) {
24
+ throw new Error(`Monitor not found: ${identifier}`);
25
+ }
26
+ await this.performCheck(config);
27
+ await this.refreshView();
28
+ }
29
+ /**
30
+ * Perform an immediate check for all monitors.
31
+ */
32
+ async checkAll() {
33
+ await Promise.all(this.configs.map((config) => this.performCheck(config)));
34
+ await this.refreshView();
35
+ }
36
+ /**
37
+ * Start monitoring all configured endpoints.
38
+ */
39
+ start() {
40
+ if (!this.job) {
41
+ const cronExpression = this.secondsToCron(this.intervalSeconds);
42
+ this.job = new CronJob(cronExpression, () => this.checkAll());
43
+ this.job.start();
44
+ }
45
+ }
46
+ /**
47
+ * Stop monitoring.
48
+ */
49
+ stop() {
50
+ if (this.job) {
51
+ this.job.stop();
52
+ this.job = null;
53
+ }
54
+ }
55
+ /**
56
+ * Check whether monitoring is running.
57
+ */
58
+ isRunning() {
59
+ return this.job !== null;
60
+ }
61
+ /**
62
+ * Convert seconds to a cron expression.
63
+ */
64
+ secondsToCron(seconds) {
65
+ if (seconds < 60) {
66
+ return `*/${seconds} * * * * *`;
67
+ }
68
+ const minutes = Math.floor(seconds / 60);
69
+ if (minutes < 60) {
70
+ return `0 */${minutes} * * * *`;
71
+ }
72
+ const hours = Math.floor(minutes / 60);
73
+ return `0 0 */${hours} * * *`;
74
+ }
75
+ async performCheck(config) {
76
+ const result = await this.sparqlMonitor.check(config.endpointUrl, config.query);
77
+ await this.store.store({
78
+ monitor: config.identifier,
79
+ ...result,
80
+ });
81
+ }
82
+ async refreshView() {
83
+ try {
84
+ await this.store.refreshLatestObservationsView();
85
+ }
86
+ catch {
87
+ // View refresh failure is not critical
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,19 @@
1
+ import type { ObservationStore, Observation } from './types.js';
2
+ /**
3
+ * PostgreSQL implementation of the ObservationStore interface.
4
+ */
5
+ export declare class PostgresObservationStore implements ObservationStore {
6
+ private client;
7
+ private db;
8
+ private constructor();
9
+ /**
10
+ * Create a new store and initialize the database schema.
11
+ */
12
+ static create(connectionString: string): Promise<PostgresObservationStore>;
13
+ close(): Promise<void>;
14
+ getLatest(): Promise<Map<string, Observation>>;
15
+ get(id: string): Promise<Observation | null>;
16
+ store(observation: Omit<Observation, 'id'>): Promise<Observation>;
17
+ refreshLatestObservationsView(): Promise<void>;
18
+ }
19
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAKhE;;GAEG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,EAAE,CAAqB;IAE/B,OAAO;IAKP;;OAEG;WACU,MAAM,CACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,wBAAwB,CAAC;IAQ9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAK9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAS5C,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAQjE,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrD"}
package/dist/store.js ADDED
@@ -0,0 +1,51 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import { eq } from 'drizzle-orm';
3
+ import postgres from 'postgres';
4
+ import * as schema from './schema.js';
5
+ const { observations, latestObservations, refreshLatestObservationsViewSql } = schema;
6
+ /**
7
+ * PostgreSQL implementation of the ObservationStore interface.
8
+ */
9
+ export class PostgresObservationStore {
10
+ client;
11
+ db;
12
+ constructor(connectionString) {
13
+ this.client = postgres(connectionString);
14
+ this.db = drizzle(this.client);
15
+ }
16
+ /**
17
+ * Create a new store and initialize the database schema.
18
+ */
19
+ static async create(connectionString) {
20
+ const store = new PostgresObservationStore(connectionString);
21
+ const { pushSchema } = await import('drizzle-kit/api');
22
+ const result = await pushSchema(schema, store.db);
23
+ await result.apply();
24
+ return store;
25
+ }
26
+ async close() {
27
+ await this.client.end();
28
+ }
29
+ async getLatest() {
30
+ const rows = await this.db.select().from(latestObservations);
31
+ return new Map(rows.map((row) => [row.monitor, row]));
32
+ }
33
+ async get(id) {
34
+ const rows = await this.db
35
+ .select()
36
+ .from(observations)
37
+ .where(eq(observations.id, id))
38
+ .limit(1);
39
+ return rows[0] ?? null;
40
+ }
41
+ async store(observation) {
42
+ const rows = await this.db
43
+ .insert(observations)
44
+ .values(observation)
45
+ .returning();
46
+ return rows[0];
47
+ }
48
+ async refreshLatestObservationsView() {
49
+ await this.db.execute(refreshLatestObservationsViewSql);
50
+ }
51
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Configuration for a monitor.
3
+ */
4
+ export interface MonitorConfig {
5
+ /** Unique identifier for this monitor. */
6
+ identifier: string;
7
+ /** URL of the SPARQL endpoint to monitor. */
8
+ endpointUrl: URL;
9
+ /** SPARQL query to execute. */
10
+ query: string;
11
+ }
12
+ /**
13
+ * Result of a single check against a SPARQL endpoint.
14
+ */
15
+ export interface CheckResult {
16
+ /** Whether the endpoint responded successfully. */
17
+ success: boolean;
18
+ /** Response time in milliseconds. */
19
+ responseTimeMs: number;
20
+ /** Error message if the check failed. */
21
+ errorMessage: string | null;
22
+ /** Timestamp when the response was received (UTC). */
23
+ observedAt: Date;
24
+ }
25
+ /**
26
+ * Observation record from the database.
27
+ */
28
+ export interface Observation {
29
+ id: string;
30
+ monitor: string;
31
+ observedAt: Date;
32
+ success: boolean;
33
+ responseTimeMs: number;
34
+ errorMessage: string | null;
35
+ }
36
+ /**
37
+ * Store interface for persisting observations.
38
+ */
39
+ export interface ObservationStore {
40
+ /**
41
+ * Get the latest observation for each identifier.
42
+ * Returns a map keyed by identifier.
43
+ */
44
+ getLatest(): Promise<Map<string, Observation>>;
45
+ /**
46
+ * Get a specific observation by ID.
47
+ */
48
+ get(id: string): Promise<Observation | null>;
49
+ /**
50
+ * Save a new observation.
51
+ */
52
+ store(observation: Omit<Observation, 'id'>): Promise<Observation>;
53
+ /**
54
+ * Refresh the latest_observations materialized view.
55
+ */
56
+ refreshLatestObservationsView(): Promise<void>;
57
+ /**
58
+ * Close the database connection.
59
+ */
60
+ close(): Promise<void>;
61
+ }
62
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,WAAW,EAAE,GAAG,CAAC;IACjB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sDAAsD;IACtD,UAAU,EAAE,IAAI,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE7C;;OAEG;IACH,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAElE;;OAEG;IACH,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@lde/sparql-monitor",
3
+ "version": "0.1.0",
4
+ "description": "Monitor SPARQL endpoints with periodic checks",
5
+ "repository": {
6
+ "url": "https://github.com/ldengine/lde"
7
+ },
8
+ "type": "module",
9
+ "exports": {
10
+ "./package.json": "./package.json",
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "development": "./src/index.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "main": "./dist/index.js",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist",
23
+ "!**/*.tsbuildinfo"
24
+ ],
25
+ "dependencies": {
26
+ "cron": "^4.1.0",
27
+ "drizzle-kit": "^0.30.4",
28
+ "drizzle-orm": "^0.38.4",
29
+ "fetch-sparql-endpoint": "^6.0.0",
30
+ "postgres": "^3.4.5",
31
+ "tslib": "^2.3.0"
32
+ },
33
+ "devDependencies": {
34
+ "vite": "^6.0.0"
35
+ }
36
+ }