@thomasfosterau/effect-sparql-node 0.0.0-stage → 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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -2
  3. package/bin/effect-sparql-server.mjs +26 -0
  4. package/dist/Cli-DE0eXMJr.d.mts +143 -0
  5. package/dist/Cli-DFvng1N8.mjs +259 -0
  6. package/dist/Cli-DFvng1N8.mjs.map +1 -0
  7. package/dist/Cli.d.mts +2 -0
  8. package/dist/Cli.mjs +1 -0
  9. package/dist/NodeSparqlServer-1vSrPk3J.d.mts +41 -0
  10. package/dist/NodeSparqlServer-CYAd_6BG.mjs +50 -0
  11. package/dist/NodeSparqlServer-CYAd_6BG.mjs.map +1 -0
  12. package/dist/NodeSparqlServer.d.mts +2 -0
  13. package/dist/NodeSparqlServer.mjs +1 -0
  14. package/dist/SparqlApi-BKE9nIf4.d.mts +488 -0
  15. package/dist/SparqlApi-BuycDkWu.mjs +371 -0
  16. package/dist/SparqlApi-BuycDkWu.mjs.map +1 -0
  17. package/dist/SparqlApi.d.mts +2 -0
  18. package/dist/SparqlApi.mjs +1 -0
  19. package/dist/SparqlApiBuilder-BAMpMy-Z.d.mts +83 -0
  20. package/dist/SparqlApiBuilder-GT7EopBq.mjs +136 -0
  21. package/dist/SparqlApiBuilder-GT7EopBq.mjs.map +1 -0
  22. package/dist/SparqlApiBuilder.d.mts +2 -0
  23. package/dist/SparqlApiBuilder.mjs +1 -0
  24. package/dist/SparqlApiClient-C82RGMEF.d.mts +386 -0
  25. package/dist/SparqlApiClient-CJcoFy5b.mjs +203 -0
  26. package/dist/SparqlApiClient-CJcoFy5b.mjs.map +1 -0
  27. package/dist/SparqlApiClient.d.mts +2 -0
  28. package/dist/SparqlApiClient.mjs +1 -0
  29. package/dist/index.d.mts +6 -0
  30. package/dist/index.mjs +6 -0
  31. package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
  32. package/package.json +67 -3
  33. package/src/Cli.ts +373 -0
  34. package/src/NodeSparqlServer.ts +71 -0
  35. package/src/SparqlApi.ts +506 -0
  36. package/src/SparqlApiBuilder.ts +196 -0
  37. package/src/SparqlApiClient.ts +383 -0
  38. 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';