@thomasfosterau/effect-sparql-node 0.0.0 → 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/LICENSE +21 -0
- package/README.md +219 -1
- package/bin/effect-sparql-server.mjs +26 -0
- package/dist/Cli-DE0eXMJr.d.mts +143 -0
- package/dist/Cli-DFvng1N8.mjs +259 -0
- package/dist/Cli-DFvng1N8.mjs.map +1 -0
- package/dist/Cli.d.mts +2 -0
- package/dist/Cli.mjs +1 -0
- package/dist/NodeSparqlServer-1vSrPk3J.d.mts +41 -0
- package/dist/NodeSparqlServer-CYAd_6BG.mjs +50 -0
- package/dist/NodeSparqlServer-CYAd_6BG.mjs.map +1 -0
- package/dist/NodeSparqlServer.d.mts +2 -0
- package/dist/NodeSparqlServer.mjs +1 -0
- package/dist/SparqlApi-BKE9nIf4.d.mts +488 -0
- package/dist/SparqlApi-BuycDkWu.mjs +371 -0
- package/dist/SparqlApi-BuycDkWu.mjs.map +1 -0
- package/dist/SparqlApi.d.mts +2 -0
- package/dist/SparqlApi.mjs +1 -0
- package/dist/SparqlApiBuilder-BAMpMy-Z.d.mts +83 -0
- package/dist/SparqlApiBuilder-GT7EopBq.mjs +136 -0
- package/dist/SparqlApiBuilder-GT7EopBq.mjs.map +1 -0
- package/dist/SparqlApiBuilder.d.mts +2 -0
- package/dist/SparqlApiBuilder.mjs +1 -0
- package/dist/SparqlApiClient-C82RGMEF.d.mts +386 -0
- package/dist/SparqlApiClient-CJcoFy5b.mjs +203 -0
- package/dist/SparqlApiClient-CJcoFy5b.mjs.map +1 -0
- package/dist/SparqlApiClient.d.mts +2 -0
- package/dist/SparqlApiClient.mjs +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +6 -0
- package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
- package/package.json +59 -3
- package/src/Cli.ts +373 -0
- package/src/NodeSparqlServer.ts +71 -0
- package/src/SparqlApi.ts +506 -0
- package/src/SparqlApiBuilder.ts +196 -0
- package/src/SparqlApiClient.ts +383 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A client derived with `HttpApiClient` from the `SparqlApi` definition,
|
|
3
|
+
* alongside the hand-written `SparqlClient`.
|
|
4
|
+
*
|
|
5
|
+
* `make` returns the generated client: one method per endpoint
|
|
6
|
+
* (`client.sparql.query`, `client.sparql.post`, `client.graphStore.get`, …)
|
|
7
|
+
* taking the endpoint's typed `query`, `headers` and `payload` and returning
|
|
8
|
+
* response bodies as byte streams. Error statuses decode into the server's
|
|
9
|
+
* `ProtocolErrors.ProtocolError`. The helpers below (`query`, `results`,
|
|
10
|
+
* `construct`, `update`, `readGraph`, `writeGraph`) send the common
|
|
11
|
+
* requests and parse the bodies with the results parsers (`ResultsStream`)
|
|
12
|
+
* and the default `Formats`.
|
|
13
|
+
*
|
|
14
|
+
* `effect/http-api` is `@stability unstable` in Effect 4, and so is this
|
|
15
|
+
* client.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
*/
|
|
19
|
+
import * as Context from 'effect/Context';
|
|
20
|
+
import * as Effect from 'effect/Effect';
|
|
21
|
+
import * as Layer from 'effect/Layer';
|
|
22
|
+
import * as Option from 'effect/Option';
|
|
23
|
+
import * as Stream from 'effect/Stream';
|
|
24
|
+
import type * as Schema from 'effect/Schema';
|
|
25
|
+
import type * as HttpClient from 'effect/http/HttpClient';
|
|
26
|
+
import type * as HttpClientError from 'effect/http/HttpClientError';
|
|
27
|
+
import type * as HttpClientResponse from 'effect/http/HttpClientResponse';
|
|
28
|
+
import * as HttpApiClient from 'effect/http-api/HttpApiClient';
|
|
29
|
+
import type * as Dataset from '@thomasfosterau/effect-rdf/Dataset';
|
|
30
|
+
import * as Serialization from '@thomasfosterau/effect-rdf/Serialization';
|
|
31
|
+
import type * as ProtocolErrors from '@thomasfosterau/effect-rdf/sparql/ProtocolErrors';
|
|
32
|
+
import * as SparqlApi from './SparqlApi.ts';
|
|
33
|
+
import * as ClientErrors from '@thomasfosterau/effect-rdf/sparql/ClientErrors';
|
|
34
|
+
import * as ClientFormats from '@thomasfosterau/effect-rdf/sparql/ClientFormats';
|
|
35
|
+
import * as ResultsStream from '@thomasfosterau/effect-rdf/sparql/ResultsStream';
|
|
36
|
+
import * as SparqlClient from '@thomasfosterau/effect-rdf/sparql/SparqlClient';
|
|
37
|
+
|
|
38
|
+
// -----------------------------------------------------------------------------
|
|
39
|
+
// The client
|
|
40
|
+
// -----------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The generated client of the `SparqlApi`.
|
|
44
|
+
*
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
* @category models
|
|
47
|
+
*/
|
|
48
|
+
export type Client = HttpApiClient.ForApi<SparqlApi.Api>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options of the derived client.
|
|
52
|
+
*
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
* @category models
|
|
55
|
+
*/
|
|
56
|
+
export interface Options {
|
|
57
|
+
/** The server's base URL (e.g. `https://example.org`); paths are appended to it. */
|
|
58
|
+
readonly baseUrl?: string | URL | undefined;
|
|
59
|
+
/** The endpoint paths, when the server does not use the defaults. */
|
|
60
|
+
readonly paths?: SparqlApi.Paths | undefined;
|
|
61
|
+
/** Transforms the `HttpClient` (retries, authentication, …). */
|
|
62
|
+
readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Derives the client from the API definition, using the `HttpClient` from
|
|
67
|
+
* the context.
|
|
68
|
+
*
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
* @category constructors
|
|
71
|
+
*/
|
|
72
|
+
export const make = (options?: Options): Effect.Effect<Client, never, HttpClient.HttpClient> =>
|
|
73
|
+
HttpApiClient.make(SparqlApi.make(options?.paths), {
|
|
74
|
+
baseUrl: options?.baseUrl,
|
|
75
|
+
transformClient: options?.transformClient,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The derived client as a service.
|
|
80
|
+
*
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
* @category services
|
|
83
|
+
*/
|
|
84
|
+
export class SparqlApiClient extends Context.Service<SparqlApiClient, Client>()(
|
|
85
|
+
'@thomasfosterau/effect-sparql-node/SparqlApiClient',
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A layer providing the derived client.
|
|
90
|
+
*
|
|
91
|
+
* @since 0.1.0
|
|
92
|
+
* @category layers
|
|
93
|
+
*/
|
|
94
|
+
export const layer = (
|
|
95
|
+
options?: Options,
|
|
96
|
+
): Layer.Layer<SparqlApiClient, never, HttpClient.HttpClient> =>
|
|
97
|
+
Layer.effect(SparqlApiClient, make(options));
|
|
98
|
+
|
|
99
|
+
// -----------------------------------------------------------------------------
|
|
100
|
+
// Helpers
|
|
101
|
+
// -----------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Any failure of a derived client call: a protocol error status, a transport
|
|
105
|
+
* or decoding failure of the generated client, or an unreadable body.
|
|
106
|
+
*
|
|
107
|
+
* @since 0.1.0
|
|
108
|
+
* @category errors
|
|
109
|
+
*/
|
|
110
|
+
export type ApiError =
|
|
111
|
+
| ProtocolErrors.ProtocolError
|
|
112
|
+
| HttpClientError.HttpClientError
|
|
113
|
+
| Schema.SchemaError
|
|
114
|
+
| ClientErrors.MalformedResponseError
|
|
115
|
+
| ClientErrors.UnsupportedMediaTypeError;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A response body and its media type.
|
|
119
|
+
*
|
|
120
|
+
* @since 0.1.0
|
|
121
|
+
* @category models
|
|
122
|
+
*/
|
|
123
|
+
export interface Body {
|
|
124
|
+
/** The media type of the body, without parameters. */
|
|
125
|
+
readonly mediaType: string;
|
|
126
|
+
readonly bytes: Stream.Stream<Uint8Array, ApiError>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The protocol dataset of a query (`default-graph-uri` / `named-graph-uri`).
|
|
131
|
+
*
|
|
132
|
+
* @since 0.1.0
|
|
133
|
+
* @category models
|
|
134
|
+
*/
|
|
135
|
+
export interface QueryOptions {
|
|
136
|
+
readonly defaultGraphs?: ReadonlyArray<string> | undefined;
|
|
137
|
+
readonly namedGraphs?: ReadonlyArray<string> | undefined;
|
|
138
|
+
/** The `Accept` header (default: every results and RDF format). */
|
|
139
|
+
readonly accept?: string | undefined;
|
|
140
|
+
/** `get` (default) sends `?query=`, `post` an `application/sparql-query` body. */
|
|
141
|
+
readonly method?: 'get' | 'post' | undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The protocol dataset of an update (`using-graph-uri` /
|
|
146
|
+
* `using-named-graph-uri`).
|
|
147
|
+
*
|
|
148
|
+
* @since 0.1.0
|
|
149
|
+
* @category models
|
|
150
|
+
*/
|
|
151
|
+
export interface UpdateOptions {
|
|
152
|
+
readonly usingGraphs?: ReadonlyArray<string> | undefined;
|
|
153
|
+
readonly usingNamedGraphs?: ReadonlyArray<string> | undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The graph of a Graph Store request: the default graph or a graph IRI.
|
|
158
|
+
*
|
|
159
|
+
* @since 0.1.0
|
|
160
|
+
* @category models
|
|
161
|
+
*/
|
|
162
|
+
export type GraphTarget = 'default' | { readonly graph: string };
|
|
163
|
+
|
|
164
|
+
const mediaTypeOf = (response: HttpClientResponse.HttpClientResponse): string =>
|
|
165
|
+
(response.headers['content-type'] ?? '').split(';')[0]?.trim().toLowerCase() ?? '';
|
|
166
|
+
|
|
167
|
+
const graphParams = (target: GraphTarget) =>
|
|
168
|
+
target === 'default' ? { default: '' } : { graph: target.graph };
|
|
169
|
+
|
|
170
|
+
// An empty list sends no parameter.
|
|
171
|
+
const queryDataset = (options?: QueryOptions) => ({
|
|
172
|
+
'default-graph-uri': options?.defaultGraphs ?? [],
|
|
173
|
+
'named-graph-uri': options?.namedGraphs ?? [],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const updateDataset = (options?: UpdateOptions) => ({
|
|
177
|
+
'using-graph-uri': options?.usingGraphs ?? [],
|
|
178
|
+
'using-named-graph-uri': options?.usingNamedGraphs ?? [],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const queryHeaders = (accept?: string) => ({
|
|
182
|
+
accept: accept ?? SparqlApi.queryMediaTypes.join(', '),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Sends a query and returns the response body with its media type, without
|
|
187
|
+
* parsing it.
|
|
188
|
+
*
|
|
189
|
+
* @since 0.1.0
|
|
190
|
+
* @category queries
|
|
191
|
+
*/
|
|
192
|
+
export const query = (
|
|
193
|
+
client: Client,
|
|
194
|
+
sparql: string,
|
|
195
|
+
options?: QueryOptions,
|
|
196
|
+
): Effect.Effect<Body, ApiError> => {
|
|
197
|
+
const dataset = queryDataset(options);
|
|
198
|
+
const headers = queryHeaders(options?.accept);
|
|
199
|
+
const sent =
|
|
200
|
+
options?.method === 'post'
|
|
201
|
+
? client.sparql.post({
|
|
202
|
+
query: dataset,
|
|
203
|
+
headers,
|
|
204
|
+
payload: { _tag: 'DirectQuery', query: sparql },
|
|
205
|
+
responseMode: 'decoded-and-response',
|
|
206
|
+
})
|
|
207
|
+
: client.sparql.query({
|
|
208
|
+
query: { query: sparql, ...dataset },
|
|
209
|
+
headers,
|
|
210
|
+
responseMode: 'decoded-and-response',
|
|
211
|
+
});
|
|
212
|
+
return Effect.flatMap(sent, ([bytes, response]) =>
|
|
213
|
+
bytes === undefined
|
|
214
|
+
? Effect.fail(
|
|
215
|
+
new ClientErrors.MalformedResponseError({
|
|
216
|
+
message: `Expected a query result, got status ${response.status}`,
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
: Effect.succeed({ mediaType: mediaTypeOf(response), bytes }),
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Sends a `SELECT` or `ASK` query and parses the results incrementally with
|
|
225
|
+
* `ResultsStream`.
|
|
226
|
+
*
|
|
227
|
+
* @since 0.1.0
|
|
228
|
+
* @category queries
|
|
229
|
+
*/
|
|
230
|
+
export const results = (
|
|
231
|
+
client: Client,
|
|
232
|
+
sparql: string,
|
|
233
|
+
options?: QueryOptions,
|
|
234
|
+
): Stream.Stream<ResultsStream.ResultEvent, ApiError> =>
|
|
235
|
+
Stream.unwrap(
|
|
236
|
+
Effect.flatMap(
|
|
237
|
+
query(client, sparql, {
|
|
238
|
+
...options,
|
|
239
|
+
accept: options?.accept ?? SparqlClient.DEFAULT_RESULTS_ACCEPT,
|
|
240
|
+
}),
|
|
241
|
+
(body) =>
|
|
242
|
+
Option.match(ResultsStream.parse(body.mediaType, Stream.decodeText(body.bytes)), {
|
|
243
|
+
onNone: () =>
|
|
244
|
+
Effect.fail(
|
|
245
|
+
new ClientErrors.UnsupportedMediaTypeError({
|
|
246
|
+
contentType: body.mediaType,
|
|
247
|
+
expected: SparqlApi.resultsMediaTypes,
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
onSome: (events) =>
|
|
251
|
+
Effect.succeed(
|
|
252
|
+
Stream.mapError(events, (e) =>
|
|
253
|
+
ClientErrors.isSparqlClientError(e)
|
|
254
|
+
? e
|
|
255
|
+
: new ClientErrors.MalformedResponseError({
|
|
256
|
+
message: e.message,
|
|
257
|
+
contentType: body.mediaType,
|
|
258
|
+
cause: e,
|
|
259
|
+
}),
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const parseRdf = (body: Body, baseIri: string): Effect.Effect<Dataset.Dataset, ApiError> =>
|
|
267
|
+
Option.match(Serialization.parserFor(ClientFormats.defaults, body.mediaType), {
|
|
268
|
+
onNone: () =>
|
|
269
|
+
Effect.fail(
|
|
270
|
+
new ClientErrors.UnsupportedMediaTypeError({
|
|
271
|
+
contentType: body.mediaType,
|
|
272
|
+
expected: SparqlApi.rdfMediaTypes,
|
|
273
|
+
}),
|
|
274
|
+
),
|
|
275
|
+
onSome: (parser) =>
|
|
276
|
+
Stream.mkString(Stream.decodeText(body.bytes)).pipe(
|
|
277
|
+
Effect.flatMap((text) =>
|
|
278
|
+
parser.parse(text, { baseIri }).pipe(
|
|
279
|
+
Effect.mapError(
|
|
280
|
+
(e) =>
|
|
281
|
+
new ClientErrors.MalformedResponseError({
|
|
282
|
+
message: Serialization.formatError(e),
|
|
283
|
+
contentType: body.mediaType,
|
|
284
|
+
cause: e,
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
Effect.map((parsed) => parsed.dataset),
|
|
290
|
+
),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Sends a `CONSTRUCT` or `DESCRIBE` query and parses the graph with the
|
|
295
|
+
* default `Formats` (relative IRIs resolve against `baseIri`).
|
|
296
|
+
*
|
|
297
|
+
* @since 0.1.0
|
|
298
|
+
* @category queries
|
|
299
|
+
*/
|
|
300
|
+
export const construct = (
|
|
301
|
+
client: Client,
|
|
302
|
+
sparql: string,
|
|
303
|
+
options?: QueryOptions & { readonly baseIri?: string | undefined },
|
|
304
|
+
): Effect.Effect<Dataset.Dataset, ApiError> =>
|
|
305
|
+
Effect.flatMap(
|
|
306
|
+
query(client, sparql, {
|
|
307
|
+
...options,
|
|
308
|
+
accept: options?.accept ?? ClientFormats.acceptHeader(ClientFormats.defaults),
|
|
309
|
+
}),
|
|
310
|
+
(body) => parseRdf(body, options?.baseIri ?? 'urn:x-base:'),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Sends an update as an `application/sparql-update` body.
|
|
315
|
+
*
|
|
316
|
+
* @since 0.1.0
|
|
317
|
+
* @category updates
|
|
318
|
+
*/
|
|
319
|
+
export const update = (
|
|
320
|
+
client: Client,
|
|
321
|
+
sparql: string,
|
|
322
|
+
options?: UpdateOptions,
|
|
323
|
+
): Effect.Effect<void, ApiError> =>
|
|
324
|
+
Effect.asVoid(
|
|
325
|
+
client.sparql.post({
|
|
326
|
+
query: updateDataset(options),
|
|
327
|
+
headers: {},
|
|
328
|
+
payload: { _tag: 'DirectUpdate', update: sparql },
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Reads a graph from the graph store and parses it with the default
|
|
334
|
+
* `Formats`.
|
|
335
|
+
*
|
|
336
|
+
* @since 0.1.0
|
|
337
|
+
* @category graph store
|
|
338
|
+
*/
|
|
339
|
+
export const readGraph = (
|
|
340
|
+
client: Client,
|
|
341
|
+
target: GraphTarget,
|
|
342
|
+
options?: { readonly accept?: string | undefined; readonly baseIri?: string | undefined },
|
|
343
|
+
): Effect.Effect<Dataset.Dataset, ApiError> =>
|
|
344
|
+
client.graphStore
|
|
345
|
+
.get({
|
|
346
|
+
query: graphParams(target),
|
|
347
|
+
headers: { accept: options?.accept ?? ClientFormats.acceptHeader(ClientFormats.defaults) },
|
|
348
|
+
responseMode: 'decoded-and-response',
|
|
349
|
+
})
|
|
350
|
+
.pipe(
|
|
351
|
+
Effect.flatMap(([bytes, response]) =>
|
|
352
|
+
parseRdf(
|
|
353
|
+
{ mediaType: mediaTypeOf(response), bytes },
|
|
354
|
+
options?.baseIri ?? (target === 'default' ? 'urn:x-base:' : target.graph),
|
|
355
|
+
),
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Writes an RDF document to a graph: `put` (default) replaces the graph,
|
|
361
|
+
* `post` adds to it. The document's media type must be one of
|
|
362
|
+
* `SparqlApi.rdfMediaTypes` (default `text/turtle`).
|
|
363
|
+
*
|
|
364
|
+
* @since 0.1.0
|
|
365
|
+
* @category graph store
|
|
366
|
+
*/
|
|
367
|
+
export const writeGraph = (
|
|
368
|
+
client: Client,
|
|
369
|
+
target: GraphTarget,
|
|
370
|
+
document: string,
|
|
371
|
+
options?: {
|
|
372
|
+
readonly mediaType?: string | undefined;
|
|
373
|
+
readonly method?: 'put' | 'post' | undefined;
|
|
374
|
+
},
|
|
375
|
+
): Effect.Effect<void, ApiError> => {
|
|
376
|
+
const request = {
|
|
377
|
+
query: graphParams(target),
|
|
378
|
+
payload: { mediaType: options?.mediaType ?? 'text/turtle', document },
|
|
379
|
+
};
|
|
380
|
+
return Effect.asVoid(
|
|
381
|
+
options?.method === 'post' ? client.graphStore.post(request) : client.graphStore.put(request),
|
|
382
|
+
);
|
|
383
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node side of `@thomasfosterau/effect-rdf/sparql`: the SPARQL 1.1
|
|
3
|
+
* Protocol / Graph Store server on a `node:http` socket (`NodeSparqlServer`),
|
|
4
|
+
* the `effect-sparql-server` command (`Cli`), and the typed `SparqlApi`
|
|
5
|
+
* `HttpApi` definition with its server (`SparqlApiBuilder`) and client
|
|
6
|
+
* (`SparqlApiClient`).
|
|
7
|
+
*
|
|
8
|
+
* @since 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @since 0.1.0 */
|
|
12
|
+
export * as Cli from './Cli.ts';
|
|
13
|
+
/** @since 0.1.0 */
|
|
14
|
+
export * as NodeSparqlServer from './NodeSparqlServer.ts';
|
|
15
|
+
/** @since 0.1.0 */
|
|
16
|
+
export * as SparqlApi from './SparqlApi.ts';
|
|
17
|
+
/** @since 0.1.0 */
|
|
18
|
+
export * as SparqlApiBuilder from './SparqlApiBuilder.ts';
|
|
19
|
+
/** @since 0.1.0 */
|
|
20
|
+
export * as SparqlApiClient from './SparqlApiClient.ts';
|