@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Foster
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,221 @@
1
- # Temporary Holding Version
1
+ # @thomasfosterau/effect-sparql-node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Node side of [`@thomasfosterau/effect-rdf/sparql`](https://github.com/thomasfosterau/effect-rdf/tree/main/packages/core#sparql-thomasfosteraueffect-rdfsparql):
4
+
5
+ - **`NodeSparqlServer`** — the platform-neutral SPARQL 1.1 Protocol and Graph
6
+ Store HTTP Protocol server (`SparqlServer` in effect-rdf) on a `node:http`
7
+ socket, through `@effect/platform-node`'s `NodeHttpServer`: `layer(options)`
8
+ and `layerMemory(options)` (over an in-memory store).
9
+ - **`Cli`** and the `effect-sparql-server` executable — serve RDF files from
10
+ the command line.
11
+ - **`SparqlApi`**, **`SparqlApiBuilder`**, **`SparqlApiClient`** — the two
12
+ protocols as an Effect `HttpApi` definition with an OpenAPI document, the
13
+ routes that serve it with `SparqlServer`'s behaviour, and the client derived
14
+ from it.
15
+
16
+ ```sh
17
+ pnpm add effect @thomasfosterau/effect-parse @thomasfosterau/effect-rdf @thomasfosterau/effect-sparql-node
18
+ ```
19
+
20
+ Peers: `effect` (`^4.1.2`: its runtime dependency `@effect/platform-node`
21
+ needs it, and `SparqlApi` declares a list of response content types per
22
+ stream, a 4.1 form of `HttpApiSchema.StreamUint8Array`) and
23
+ `@thomasfosterau/effect-parse` (the SPARQL parser behind effect-rdf's
24
+ `/sparql`). `@thomasfosterau/effect-rdf` and `@effect/platform-node` are
25
+ dependencies.
26
+
27
+ ## Serving data
28
+
29
+ ```ts
30
+ import { Layer } from 'effect';
31
+ import { NodeRuntime } from '@effect/platform-node';
32
+ import { Dataset } from '@thomasfosterau/effect-rdf';
33
+ import { NodeSparqlServer } from '@thomasfosterau/effect-sparql-node';
34
+
35
+ const data: Dataset.Dataset = /* … */ Dataset.empty;
36
+
37
+ // SPARQL endpoint on /sparql, graph store on /rdf-graph-store.
38
+ const Server = NodeSparqlServer.layerMemory({
39
+ port: 3030,
40
+ data,
41
+ queryTimeout: '30 seconds',
42
+ maxResults: 100_000,
43
+ cors: true,
44
+ });
45
+
46
+ Layer.launch(Server).pipe(NodeRuntime.runMain);
47
+ ```
48
+
49
+ With your own store or engine (e.g. a SQL backend), provide the
50
+ `SparqlEngine`, `QuadStore` and `Serialization.Formats` services:
51
+
52
+ ```ts
53
+ import { Layer } from 'effect';
54
+ import { Engine, ServerFormats, Store } from '@thomasfosterau/effect-rdf/sparql';
55
+ import { NodeSparqlServer } from '@thomasfosterau/effect-sparql-node';
56
+
57
+ const Server = NodeSparqlServer.layer({ port: 3030, readOnly: true }).pipe(
58
+ Layer.provide(Engine.layer({ unionDefaultGraph: true })),
59
+ Layer.provide(Store.layerMemory()),
60
+ Layer.provide(ServerFormats.layer),
61
+ );
62
+ ```
63
+
64
+ `NodeSparqlServer` takes the options of `SparqlServer` (see effect-rdf's
65
+ README) plus `port` and `host`. To mount the routes into a server of your own,
66
+ or to serve them on another platform, use `SparqlServer.layerRoutes`,
67
+ `SparqlServer.handler` or `SparqlServer.layerServe` from
68
+ `@thomasfosterau/effect-rdf/sparql`.
69
+
70
+ ## HttpApi definition and OpenAPI
71
+
72
+ `SparqlApi` describes both protocols as an Effect `HttpApi`, so the
73
+ endpoints, their parameters, request and response media types and error
74
+ statuses are typed and documented in one place:
75
+
76
+ - group `sparql`: `query` (`GET` with `query`, `default-graph-uri`,
77
+ `named-graph-uri`; without `query` it answers the service description) and
78
+ `post` (`POST` with an `application/x-www-form-urlencoded`,
79
+ `application/sparql-query` or `application/sparql-update` body; `200` with
80
+ results or `204` after an update);
81
+ - group `graphStore`: `get`, `head`, `put`, `post` and `delete` with
82
+ `graph=<iri>` or `default`, bodies under every RDF media type of
83
+ `ServerFormats.codecs`;
84
+ - responses are byte streams listing every format
85
+ (`SparqlApi.queryMediaTypes`, `SparqlApi.rdfMediaTypes`); errors are
86
+ `text/plain` messages decoded into `ProtocolErrors.ProtocolError` at `400`, `403`,
87
+ `404`, `405`, `406`, `415`, `500` and `503`.
88
+
89
+ `SparqlApi.make({ path, graphStorePath })` builds it at other paths and
90
+ `SparqlApi.openApi(api)` returns its OpenAPI document.
91
+
92
+ `SparqlApiBuilder.layerRoutes(options)` serves it with `HttpApiBuilder`. Its
93
+ handlers are raw (`handleRaw`) and run the same code as `SparqlServer`:
94
+ `Accept` is negotiated and bodies are read by the protocol internals, so
95
+ statuses, headers and messages are identical. Methods the API does not
96
+ declare (`405` with `Allow`), CORS preflights and graphs identified directly
97
+ by a URL below the graph store go to plain `HttpRouter` fallback routes, and
98
+ the `SparqlApi.SchemaFallback` middleware hands a request whose parameters
99
+ fail the API's schemas (e.g. a repeated `query`) to the protocol handler, so
100
+ it gets the protocol's own `400`. The conformance suites run against both
101
+ servers. `effect/http-api` is `@stability unstable` in Effect 4.
102
+
103
+ ```ts
104
+ import { Layer } from 'effect';
105
+ import { HttpRouter } from 'effect/http';
106
+ import { NodeHttpServer } from '@effect/platform-node';
107
+ import { createServer } from 'node:http';
108
+ import { SparqlServer } from '@thomasfosterau/effect-rdf/sparql';
109
+ import { SparqlApiBuilder } from '@thomasfosterau/effect-sparql-node';
110
+
111
+ const Main = HttpRouter.serve(SparqlApiBuilder.layerRoutes({ openapiPath: '/openapi.json' })).pipe(
112
+ Layer.provide(SparqlServer.layerMemoryServices()),
113
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 8080 })),
114
+ );
115
+ ```
116
+
117
+ `SparqlApiBuilder.layerServe(options)` is the same with request logging, and
118
+ `layerHandlers` / `layerSchemaFallback` / `layerFallback` are the pieces, for
119
+ mounting the API next to other `HttpApiBuilder` groups. The API takes the
120
+ options of `SparqlServer` except `updatePath` (updates go to the query
121
+ endpoint) and `graphStorePath: false`. `SparqlApiBuilder.makeTestClient(options)` is an
122
+ in-process `HttpClient` for the API routes.
123
+
124
+ ## Command line
125
+
126
+ This package installs an `effect-sparql-server` executable
127
+ (`bin/effect-sparql-server.mjs`):
128
+
129
+ ```sh
130
+ effect-sparql-server --port 3030 --cors data.ttl more.trig
131
+ effect-sparql-server --read-only --timeout 10000 --graph-per-file a.ttl b.nt
132
+ ```
133
+
134
+ Files are parsed by extension with the default formats; run
135
+ `effect-sparql-server --help` for every option (the help, `--version` and
136
+ `--completions <shell>` come from `effect/cli`). (From the repository:
137
+ `pnpm --filter @thomasfosterau/effect-sparql-node start -- data.ttl`.)
138
+
139
+ Every flag can also be set by an environment variable — `SPARQL_SERVER_`
140
+ followed by the flag name in upper snake case — which suits containers. A flag
141
+ on the command line wins over the environment, which wins over the default:
142
+
143
+ | Flag | Environment variable | Default |
144
+ | ----------------------- | ----------------------------------- | ------------------ |
145
+ | `-p`, `--port` | `SPARQL_SERVER_PORT` | `3030` |
146
+ | `--host` | `SPARQL_SERVER_HOST` | all interfaces |
147
+ | `--path` | `SPARQL_SERVER_PATH` | `/sparql` |
148
+ | `--update-path` | `SPARQL_SERVER_UPDATE_PATH` | the query path |
149
+ | `--graph-store` | `SPARQL_SERVER_GRAPH_STORE` | `/rdf-graph-store` |
150
+ | `--no-graph-store` | `SPARQL_SERVER_NO_GRAPH_STORE` | `false` |
151
+ | `--read-only` | `SPARQL_SERVER_READ_ONLY` | `false` |
152
+ | `--timeout` | `SPARQL_SERVER_TIMEOUT` | none |
153
+ | `--max-results` | `SPARQL_SERVER_MAX_RESULTS` | none |
154
+ | `--cors` | `SPARQL_SERVER_CORS` | `false` |
155
+ | `--union-default-graph` | `SPARQL_SERVER_UNION_DEFAULT_GRAPH` | `false` |
156
+ | `--graph-per-file` | `SPARQL_SERVER_GRAPH_PER_FILE` | `false` |
157
+ | `--base-url` | `SPARQL_SERVER_BASE_URL` | the request origin |
158
+
159
+ ```sh
160
+ SPARQL_SERVER_PORT=8080 SPARQL_SERVER_READ_ONLY=true effect-sparql-server data.ttl
161
+ ```
162
+
163
+ Programmatically, `Cli.command` is the `effect/cli` command, `Cli.parse(argv)`
164
+ validates arguments (and the environment) into `Cli.Arguments`, and
165
+ `Cli.run(argv, { version })` runs the server.
166
+
167
+ ## Derived client
168
+
169
+ `SparqlApiClient.make(options)` derives a client with `HttpApiClient` from
170
+ the server's `SparqlApi` definition (above): one typed method
171
+ per endpoint (`client.sparql.query`, `client.sparql.post`,
172
+ `client.graphStore.get` / `head` / `put` / `post` / `delete`), returning
173
+ response bodies as byte streams and failing with the server's
174
+ `ProtocolErrors.ProtocolError` (with the status and message) on error statuses. The
175
+ helpers send the common requests and parse the bodies with `ResultsStream`
176
+ and `ClientFormats.defaults`:
177
+
178
+ ```ts
179
+ import { Effect, Stream } from 'effect';
180
+ import { FetchHttpClient } from 'effect/http';
181
+ import { SparqlApiClient } from '@thomasfosterau/effect-sparql-node';
182
+
183
+ const program = Effect.gen(function* () {
184
+ const client = yield* SparqlApiClient.SparqlApiClient;
185
+ // Raw: the generated method, with `responseMode` to see the response.
186
+ const [body, response] = yield* client.sparql.query({
187
+ query: { query: 'ASK {}' },
188
+ headers: { accept: 'application/sparql-results+json' },
189
+ responseMode: 'decoded-and-response',
190
+ });
191
+ // Parsed: SELECT / ASK events, CONSTRUCT graphs, updates, Graph Store.
192
+ const events = yield* Stream.runCollect(SparqlApiClient.results(client, 'SELECT * { ?s ?p ?o }'));
193
+ yield* SparqlApiClient.update(client, 'INSERT DATA { <urn:a> <urn:b> <urn:c> }');
194
+ yield* SparqlApiClient.writeGraph(client, { graph: 'urn:g' }, '<urn:a> <urn:b> "c" .');
195
+ return yield* SparqlApiClient.readGraph(client, { graph: 'urn:g' });
196
+ }).pipe(
197
+ Effect.provide(SparqlApiClient.layer({ baseUrl: 'https://example.org' })),
198
+ Effect.provide(FetchHttpClient.layer),
199
+ );
200
+ ```
201
+
202
+ `options.paths` targets a server at other paths and `transformClient` adds
203
+ retries or authentication. Direct bodies are tagged payloads
204
+ (`{ _tag: 'DirectQuery', query }`, `{ _tag: 'DirectUpdate', update }`),
205
+ forms are plain records, and Graph Store bodies are `{ mediaType, document }`.
206
+ It works against `SparqlServer.handler` too: the definition describes the
207
+ same protocol. `effect/http-api` is `@stability unstable` in Effect 4.
208
+
209
+ ## Conformance
210
+
211
+ The SPARQL 1.1 Protocol suites (`.test-suites/rdf-tests/sparql/sparql11`, or
212
+ `$RDF_TEST_SUITES`) run in-process (`test/ProtocolConformance.test.ts`), once
213
+ against `SparqlServer.handler` and once against the `SparqlApi` routes, with
214
+ the same results: Protocol 34 / 34, Graph Store Protocol 14 / 14, Service
215
+ Description 3 / 3 (the deprecated `http-rdf-update` suite is skipped). The
216
+ tests of `SparqlApi`, `SparqlApiClient` (against both servers), the CLI and
217
+ the Node server sit beside it in `test/`.
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env -S node --no-warnings
2
+ // Serves RDF files over the SPARQL 1.1 Protocol and Graph Store Protocol.
3
+ // Uses the built `dist/` when available, otherwise the TypeScript sources
4
+ // (run directly with Node's built-in type stripping).
5
+ import { existsSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
7
+
8
+ const dist = new URL('../dist/Cli.mjs', import.meta.url);
9
+ const Cli = await import(
10
+ existsSync(dist) ? dist.href : new URL('../src/Cli.ts', import.meta.url).href
11
+ );
12
+ const { NodeRuntime } = await import('@effect/platform-node');
13
+ const { Effect } = await import('effect');
14
+ const { version } = createRequire(import.meta.url)('../package.json');
15
+
16
+ NodeRuntime.runMain(
17
+ Cli.run(process.argv.slice(2), { version }).pipe(
18
+ // Usage errors: `effect/cli` has printed the help; say what was wrong.
19
+ Effect.catchTag('CliError', (e) =>
20
+ Effect.sync(() => {
21
+ if (e.message !== '') process.stderr.write(`${e.message}\n`);
22
+ process.exitCode = 2;
23
+ }),
24
+ ),
25
+ ),
26
+ );
@@ -0,0 +1,143 @@
1
+ import { n as ServeOptions } from "./NodeSparqlServer-1vSrPk3J.mjs";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import * as Option from "effect/Option";
5
+ import * as Path from "effect/Path";
6
+ import * as Schema from "effect/Schema";
7
+ import { Argument, Command, Flag } from "effect/cli";
8
+ import * as Dataset from "@thomasfosterau/effect-rdf/Dataset";
9
+ import * as Serialization from "@thomasfosterau/effect-rdf/Serialization";
10
+ import * as HttpServerError from "effect/http/HttpServerError";
11
+ declare namespace Cli_d_exports {
12
+ export { Arguments, CliError, Flags, command, config, envPrefix, load, parse, run, toArguments, usage };
13
+ }
14
+ declare const CliError_base: Schema.Class<CliError, Schema.TaggedStruct<"CliError", {
15
+ readonly message: Schema.String;
16
+ }>, import("effect/Cause").YieldableError>;
17
+ /**
18
+ * A command line usage error.
19
+ *
20
+ * @since 0.1.0
21
+ * @category errors
22
+ */
23
+ declare class CliError extends CliError_base {}
24
+ /**
25
+ * The prefix of the environment variables that back the flags:
26
+ * `SPARQL_SERVER_PORT`, `SPARQL_SERVER_READ_ONLY`, …
27
+ *
28
+ * @since 0.1.0
29
+ * @category cli
30
+ */
31
+ declare const envPrefix = "SPARQL_SERVER_";
32
+ /**
33
+ * A summary of the flags and their environment variables; `--help` prints
34
+ * the full, generated help.
35
+ *
36
+ * @since 0.1.0
37
+ * @category cli
38
+ */
39
+ declare const usage = "Usage: effect-sparql-server [flags] [files...]\n\nServes the given RDF files from an in-memory store over the SPARQL 1.1\nProtocol and the Graph Store HTTP Protocol.\n\nFlags (environment variable in brackets; a flag wins over the environment):\n -p, --port <port> port to listen on (default 3030) [SPARQL_SERVER_PORT]\n --host <host> host to bind (default all interfaces) [SPARQL_SERVER_HOST]\n --path <path> SPARQL endpoint path (default /sparql) [SPARQL_SERVER_PATH]\n --update-path <path> separate update endpoint path [SPARQL_SERVER_UPDATE_PATH]\n --graph-store <path> Graph Store Protocol path (default /rdf-graph-store) [SPARQL_SERVER_GRAPH_STORE]\n --no-graph-store disable the Graph Store Protocol [SPARQL_SERVER_NO_GRAPH_STORE]\n --read-only reject updates and graph store writes [SPARQL_SERVER_READ_ONLY]\n --timeout <ms> query timeout in milliseconds [SPARQL_SERVER_TIMEOUT]\n --max-results <n> maximum number of results per query [SPARQL_SERVER_MAX_RESULTS]\n --cors enable permissive CORS [SPARQL_SERVER_CORS]\n --union-default-graph query the union of all graphs by default [SPARQL_SERVER_UNION_DEFAULT_GRAPH]\n --graph-per-file load each file into a named graph (its file URL) [SPARQL_SERVER_GRAPH_PER_FILE]\n --base-url <url> public origin of the server [SPARQL_SERVER_BASE_URL]\n -h, --help show the full help\n";
40
+ /**
41
+ * Parsed command line arguments.
42
+ *
43
+ * @since 0.1.0
44
+ * @category models
45
+ */
46
+ interface Arguments {
47
+ readonly help: boolean;
48
+ readonly files: ReadonlyArray<string>;
49
+ readonly graphPerFile: boolean;
50
+ readonly unionDefaultGraph: boolean;
51
+ readonly serve: ServeOptions;
52
+ }
53
+ /**
54
+ * The command's flags and arguments.
55
+ *
56
+ * @since 0.1.0
57
+ * @category cli
58
+ */
59
+ declare const config: {
60
+ readonly port: Flag.Flag<Option.Option<number>>;
61
+ readonly host: Flag.Flag<Option.Option<string>>;
62
+ readonly path: Flag.Flag<Option.Option<string>>;
63
+ readonly updatePath: Flag.Flag<Option.Option<string>>;
64
+ readonly graphStore: Flag.Flag<Option.Option<string>>;
65
+ readonly noGraphStore: Flag.Flag<boolean>;
66
+ readonly readOnly: Flag.Flag<boolean>;
67
+ readonly timeout: Flag.Flag<Option.Option<number>>;
68
+ readonly maxResults: Flag.Flag<Option.Option<number>>;
69
+ readonly cors: Flag.Flag<boolean>;
70
+ readonly unionDefaultGraph: Flag.Flag<boolean>;
71
+ readonly graphPerFile: Flag.Flag<boolean>;
72
+ readonly baseUrl: Flag.Flag<Option.Option<string>>;
73
+ readonly files: Argument.Argument<readonly string[]>;
74
+ };
75
+ /**
76
+ * The values of the flags and arguments, as `effect/cli` parses them.
77
+ *
78
+ * @since 0.1.0
79
+ * @category models
80
+ */
81
+ type Flags = Command.Command.Config.Infer<typeof config>;
82
+ /**
83
+ * Validates parsed flags into `Arguments`: integers must be non-negative,
84
+ * paths must start with `/`, and `--base-url` must be an absolute IRI.
85
+ *
86
+ * @since 0.1.0
87
+ * @category cli
88
+ */
89
+ declare const toArguments: (flags: Flags) => Effect.Effect<Arguments, CliError>;
90
+ /**
91
+ * Parses command line arguments, reading unset flags from the environment
92
+ * (`SPARQL_SERVER_*`). `-h` / `--help` sets `help` instead.
93
+ *
94
+ * @since 0.1.0
95
+ * @category cli
96
+ */
97
+ declare const parse: (argv: ReadonlyArray<string>) => Effect.Effect<Arguments, CliError>;
98
+ /**
99
+ * Loads RDF files into a dataset through Effect's `FileSystem`, choosing
100
+ * parsers by file extension from the `Formats` service. Relative IRIs
101
+ * resolve against each file's `file:` URL; with `graphPerFile`, a file's
102
+ * default-graph triples go into a graph named by that URL.
103
+ *
104
+ * @since 0.1.0
105
+ * @category cli
106
+ */
107
+ declare const load: (files: ReadonlyArray<string>, graphPerFile: boolean) => Effect.Effect<Dataset.Dataset, CliError | Serialization.RdfParseError, Serialization.Formats | FileSystem.FileSystem | Path.Path>;
108
+ /**
109
+ * The `effect/cli` command: parses the flags (and their environment
110
+ * variables), loads the files and serves them until interrupted.
111
+ *
112
+ * @since 0.1.0
113
+ * @category cli
114
+ */
115
+ declare const command: Command.Command<"effect-sparql-server", {
116
+ readonly port: Option.Option<number>;
117
+ readonly host: Option.Option<string>;
118
+ readonly path: Option.Option<string>;
119
+ readonly updatePath: Option.Option<string>;
120
+ readonly graphStore: Option.Option<string>;
121
+ readonly noGraphStore: boolean;
122
+ readonly readOnly: boolean;
123
+ readonly timeout: Option.Option<number>;
124
+ readonly maxResults: Option.Option<number>;
125
+ readonly cors: boolean;
126
+ readonly unionDefaultGraph: boolean;
127
+ readonly graphPerFile: boolean;
128
+ readonly baseUrl: Option.Option<string>;
129
+ readonly files: readonly string[];
130
+ }, {}, CliError | Serialization.RdfParseError | HttpServerError.ServeError, never>;
131
+ /**
132
+ * Runs the command line program: parses arguments (printing the generated
133
+ * help for `--help` or a usage error), loads the files and serves them
134
+ * until interrupted. `version` is what `--version` prints.
135
+ *
136
+ * @since 0.1.0
137
+ * @category cli
138
+ */
139
+ declare const run: (argv: ReadonlyArray<string>, options?: {
140
+ readonly version?: string | undefined;
141
+ }) => Effect.Effect<void, CliError | Serialization.RdfParseError | HttpServerError.ServeError>;
142
+ export { command as a, load as c, toArguments as d, usage as f, Flags as i, parse as l, CliError as n, config as o, Cli_d_exports as r, envPrefix as s, Arguments as t, run as u };
143
+ //# sourceMappingURL=Cli-DE0eXMJr.d.mts.map
@@ -0,0 +1,259 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
+ import { r as layerMemory } from "./NodeSparqlServer-CYAd_6BG.mjs";
3
+ import * as NodeServices from "@effect/platform-node/NodeServices";
4
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
5
+ import * as NodePath from "@effect/platform-node/NodePath";
6
+ import * as Config from "effect/Config";
7
+ import * as Console from "effect/Console";
8
+ import * as Effect from "effect/Effect";
9
+ import "effect/FileSystem";
10
+ import * as Layer from "effect/Layer";
11
+ import * as Match from "effect/Match";
12
+ import * as Option from "effect/Option";
13
+ import * as Path from "effect/Path";
14
+ import * as PlatformError from "effect/PlatformError";
15
+ import * as Ref from "effect/Ref";
16
+ import * as Schema from "effect/Schema";
17
+ import { Argument, Command, Flag } from "effect/cli";
18
+ import * as CliErrors from "effect/cli/CliError";
19
+ import * as Iri from "@thomasfosterau/effect-rdf/Iri";
20
+ import * as Dataset from "@thomasfosterau/effect-rdf/Dataset";
21
+ import * as Serialization from "@thomasfosterau/effect-rdf/Serialization";
22
+ import * as Term from "@thomasfosterau/effect-rdf/Term";
23
+ import * as ServerFormats from "@thomasfosterau/effect-rdf/sparql/ServerFormats";
24
+ // src/Cli.ts
25
+ /**
26
+ * The command line entry point: serves RDF files loaded into an in-memory
27
+ * store.
28
+ *
29
+ * ```sh
30
+ * effect-sparql-server --port 3030 data.ttl more.trig
31
+ * ```
32
+ *
33
+ * Files are parsed with the default formats, chosen by file extension.
34
+ * Triples go to the default graph, or with `--graph-per-file` to a named
35
+ * graph per file (named by the file URL); quads keep their graph.
36
+ *
37
+ * The command is built with `effect/cli` (generated `--help`, `--version`
38
+ * and shell completions), and every flag also reads an environment variable
39
+ * through `Config`: `SPARQL_SERVER_` followed by the flag name in upper
40
+ * snake case (`--max-results` is `SPARQL_SERVER_MAX_RESULTS`). A flag on the
41
+ * command line wins over the environment, which wins over the default.
42
+ *
43
+ * @since 0.1.0
44
+ */
45
+ var Cli_exports = /* @__PURE__ */ __exportAll({
46
+ CliError: () => CliError,
47
+ command: () => command,
48
+ config: () => config,
49
+ envPrefix: () => envPrefix,
50
+ load: () => load,
51
+ parse: () => parse,
52
+ run: () => run,
53
+ toArguments: () => toArguments,
54
+ usage: () => usage
55
+ });
56
+ /**
57
+ * A command line usage error.
58
+ *
59
+ * @since 0.1.0
60
+ * @category errors
61
+ */
62
+ var CliError = class extends Schema.TaggedError()("CliError", { message: Schema.String }) {};
63
+ /**
64
+ * The prefix of the environment variables that back the flags:
65
+ * `SPARQL_SERVER_PORT`, `SPARQL_SERVER_READ_ONLY`, …
66
+ *
67
+ * @since 0.1.0
68
+ * @category cli
69
+ */
70
+ const envPrefix = "SPARQL_SERVER_";
71
+ const envName = (flag) => `${envPrefix}${flag.toUpperCase().replaceAll("-", "_")}`;
72
+ /**
73
+ * A summary of the flags and their environment variables; `--help` prints
74
+ * the full, generated help.
75
+ *
76
+ * @since 0.1.0
77
+ * @category cli
78
+ */
79
+ const usage = `Usage: effect-sparql-server [flags] [files...]
80
+
81
+ Serves the given RDF files from an in-memory store over the SPARQL 1.1
82
+ Protocol and the Graph Store HTTP Protocol.
83
+
84
+ Flags (environment variable in brackets; a flag wins over the environment):
85
+ -p, --port <port> port to listen on (default 3030) [SPARQL_SERVER_PORT]
86
+ --host <host> host to bind (default all interfaces) [SPARQL_SERVER_HOST]
87
+ --path <path> SPARQL endpoint path (default /sparql) [SPARQL_SERVER_PATH]
88
+ --update-path <path> separate update endpoint path [SPARQL_SERVER_UPDATE_PATH]
89
+ --graph-store <path> Graph Store Protocol path (default /rdf-graph-store) [SPARQL_SERVER_GRAPH_STORE]
90
+ --no-graph-store disable the Graph Store Protocol [SPARQL_SERVER_NO_GRAPH_STORE]
91
+ --read-only reject updates and graph store writes [SPARQL_SERVER_READ_ONLY]
92
+ --timeout <ms> query timeout in milliseconds [SPARQL_SERVER_TIMEOUT]
93
+ --max-results <n> maximum number of results per query [SPARQL_SERVER_MAX_RESULTS]
94
+ --cors enable permissive CORS [SPARQL_SERVER_CORS]
95
+ --union-default-graph query the union of all graphs by default [SPARQL_SERVER_UNION_DEFAULT_GRAPH]
96
+ --graph-per-file load each file into a named graph (its file URL) [SPARQL_SERVER_GRAPH_PER_FILE]
97
+ --base-url <url> public origin of the server [SPARQL_SERVER_BASE_URL]
98
+ -h, --help show the full help
99
+ `;
100
+ const stringFlag = (name, description) => Flag.String(name).pipe(Flag.withDescription(`${description} [${envName(name)}]`), Flag.withFallbackConfig(Config.String(envName(name))), Flag.optional);
101
+ const intFlag = (name, description) => Flag.Int(name).pipe(Flag.withDescription(`${description} [${envName(name)}]`), Flag.withFallbackConfig(Config.Int(envName(name))), Flag.optional);
102
+ const booleanFlag = (name, description) => Flag.Boolean(name).pipe(Flag.withDescription(`${description} [${envName(name)}]`), Flag.withFallbackConfig(Config.Boolean(envName(name))), Flag.withDefault(false));
103
+ /**
104
+ * The command's flags and arguments.
105
+ *
106
+ * @since 0.1.0
107
+ * @category cli
108
+ */
109
+ const config = {
110
+ port: intFlag("port", "port to listen on (default 3030)").pipe(Flag.withAlias("p")),
111
+ host: stringFlag("host", "host to bind (default all interfaces)"),
112
+ path: stringFlag("path", "SPARQL endpoint path (default /sparql)"),
113
+ updatePath: stringFlag("update-path", "separate update endpoint path"),
114
+ graphStore: stringFlag("graph-store", "Graph Store Protocol path (default /rdf-graph-store)"),
115
+ noGraphStore: booleanFlag("no-graph-store", "disable the Graph Store Protocol"),
116
+ readOnly: booleanFlag("read-only", "reject updates and graph store writes"),
117
+ timeout: intFlag("timeout", "query timeout in milliseconds"),
118
+ maxResults: intFlag("max-results", "maximum number of results per query"),
119
+ cors: booleanFlag("cors", "enable permissive CORS"),
120
+ unionDefaultGraph: booleanFlag("union-default-graph", "query the union of all graphs by default"),
121
+ graphPerFile: booleanFlag("graph-per-file", "load each file into a named graph (its file URL)"),
122
+ baseUrl: stringFlag("base-url", "public origin of the server"),
123
+ files: Argument.String("files").pipe(Argument.withDescription("RDF files to load, in any syntax the server reads"), Argument.variadic())
124
+ };
125
+ const nonNegative = (name, value) => Option.match(value, {
126
+ onNone: () => Effect.succeed(void 0),
127
+ onSome: (n) => Number.isInteger(n) && n >= 0 ? Effect.succeed(n) : Effect.fail(new CliError({ message: `--${name} expects a non-negative integer` }))
128
+ });
129
+ const endpointPath = (name, value) => Option.match(value, {
130
+ onNone: () => Effect.succeed(void 0),
131
+ onSome: (path) => Schema.is(EndpointPath)(path) ? Effect.succeed(path) : Effect.fail(new CliError({ message: `--${name} must start with '/'` }))
132
+ });
133
+ const EndpointPath = Schema.TemplateLiteral(["/", Schema.String]);
134
+ /**
135
+ * Validates parsed flags into `Arguments`: integers must be non-negative,
136
+ * paths must start with `/`, and `--base-url` must be an absolute IRI.
137
+ *
138
+ * @since 0.1.0
139
+ * @category cli
140
+ */
141
+ const toArguments = (flags) => Effect.gen(function* () {
142
+ const baseUrl = Option.getOrUndefined(flags.baseUrl);
143
+ if (baseUrl !== void 0 && !Iri.isValid(baseUrl)) return yield* new CliError({ message: `--base-url must be an absolute IRI: ${baseUrl}` });
144
+ const graphStore = yield* endpointPath("graph-store", flags.graphStore);
145
+ const options = {
146
+ path: yield* endpointPath("path", flags.path),
147
+ updatePath: yield* endpointPath("update-path", flags.updatePath),
148
+ graphStorePath: flags.noGraphStore ? false : graphStore,
149
+ readOnly: flags.readOnly,
150
+ queryTimeout: yield* nonNegative("timeout", flags.timeout),
151
+ maxResults: yield* nonNegative("max-results", flags.maxResults),
152
+ cors: flags.cors,
153
+ logging: true,
154
+ baseUrl,
155
+ unionDefaultGraph: flags.unionDefaultGraph
156
+ };
157
+ return {
158
+ help: false,
159
+ files: flags.files,
160
+ graphPerFile: flags.graphPerFile,
161
+ unionDefaultGraph: flags.unionDefaultGraph,
162
+ serve: {
163
+ ...options,
164
+ port: (yield* nonNegative("port", flags.port)) ?? 3030,
165
+ host: Option.getOrUndefined(flags.host)
166
+ }
167
+ };
168
+ });
169
+ const fromCliError = (error) => new CliError({ message: Match.value(error).pipe(Match.tag("ShowHelp", (help) => help.errors.map((e) => e.message).join("\n")), Match.orElse((e) => e.message)) });
170
+ const quiet = () => void 0;
171
+ const silentConsole = Object.assign(Object.create(globalThis.console), {
172
+ log: quiet,
173
+ error: quiet,
174
+ info: quiet,
175
+ warn: quiet
176
+ });
177
+ const isHelp = (argv) => argv.includes("--help") || argv.includes("-h");
178
+ /**
179
+ * Parses command line arguments, reading unset flags from the environment
180
+ * (`SPARQL_SERVER_*`). `-h` / `--help` sets `help` instead.
181
+ *
182
+ * @since 0.1.0
183
+ * @category cli
184
+ */
185
+ const parse = (argv) => isHelp(argv) ? Effect.succeed({
186
+ help: true,
187
+ files: [],
188
+ graphPerFile: false,
189
+ unionDefaultGraph: false,
190
+ serve: { port: 3030 }
191
+ }) : Effect.gen(function* () {
192
+ const parsed = yield* Ref.make(Option.none());
193
+ const capture = Command.make("effect-sparql-server", config, (flags) => Effect.flatMap(toArguments(flags), (args) => Ref.set(parsed, Option.some(args))));
194
+ yield* Command.runWith(capture, {
195
+ version: "",
196
+ renderErrors: false
197
+ })(argv).pipe(Effect.catchIf(CliErrors.isCliError, (e) => Effect.fail(fromCliError(e))), Effect.provideService(Console.Console, silentConsole), Effect.provide(NodeServices.layer));
198
+ return yield* Effect.flatMap(Ref.get(parsed), Effect.fromOption).pipe(Effect.mapError(() => new CliError({ message: "no arguments were parsed" })));
199
+ });
200
+ /**
201
+ * Loads RDF files into a dataset through Effect's `FileSystem`, choosing
202
+ * parsers by file extension from the `Formats` service. Relative IRIs
203
+ * resolve against each file's `file:` URL; with `graphPerFile`, a file's
204
+ * default-graph triples go into a graph named by that URL.
205
+ *
206
+ * @since 0.1.0
207
+ * @category cli
208
+ */
209
+ const load = (files, graphPerFile) => Effect.gen(function* () {
210
+ const formats = yield* Serialization.Formats;
211
+ const paths = yield* Path.Path;
212
+ const perFile = yield* Effect.forEach(files, Effect.fnUntraced(function* (file) {
213
+ const parser = Serialization.parserForExtension(formats, paths.extname(file));
214
+ if (Option.isNone(parser)) return yield* new CliError({ message: `Unknown RDF file extension: ${file}` });
215
+ const cannotRead = (e) => new CliError({ message: `Cannot read ${file}: ${e.message}` });
216
+ const url = (yield* paths.toFileUrl(paths.resolve(file)).pipe(Effect.mapError((e) => cannotRead(new PlatformError.PlatformError(e))))).href;
217
+ const result = yield* Serialization.readFile(file, {
218
+ parser: parser.value,
219
+ baseIri: url
220
+ }).pipe(Effect.catchTag("PlatformError", (e) => Effect.fail(cannotRead(e))));
221
+ const graph = graphPerFile ? Term.namedNode(url) : void 0;
222
+ const quads = Array.from(result.dataset, (q) => graph !== void 0 && q.graph._tag === "DefaultGraph" ? new Term.Quad(q.subject, q.predicate, q.object, graph) : q);
223
+ yield* Effect.logInfo(`Loaded ${result.dataset.size} quads from ${file}`);
224
+ return quads;
225
+ }), { concurrency: 1 });
226
+ return Dataset.fromIterable(perFile.flat());
227
+ });
228
+ /**
229
+ * The `effect/cli` command: parses the flags (and their environment
230
+ * variables), loads the files and serves them until interrupted.
231
+ *
232
+ * @since 0.1.0
233
+ * @category cli
234
+ */
235
+ const command = Command.make("effect-sparql-server", config, (flags) => Effect.gen(function* () {
236
+ const args = yield* toArguments(flags);
237
+ const data = yield* Effect.provide(load(args.files, args.graphPerFile), Layer.mergeAll(ServerFormats.layer, NodeFileSystem.layer, NodePath.layer));
238
+ const server = layerMemory({
239
+ ...args.serve,
240
+ data,
241
+ engine: { unionDefaultGraph: args.unionDefaultGraph }
242
+ });
243
+ return yield* Layer.launch(server);
244
+ })).pipe(Command.withDescription("Serves the given RDF files from an in-memory store over the SPARQL 1.1 Protocol and the Graph Store HTTP Protocol."));
245
+ /**
246
+ * Runs the command line program: parses arguments (printing the generated
247
+ * help for `--help` or a usage error), loads the files and serves them
248
+ * until interrupted. `version` is what `--version` prints.
249
+ *
250
+ * @since 0.1.0
251
+ * @category cli
252
+ */
253
+ const run = (argv, options) => Command.runWith(command, {
254
+ version: options?.version ?? "unknown",
255
+ renderErrors: false
256
+ })(argv).pipe(Effect.catchIf(CliErrors.isCliError, (e) => Effect.fail(fromCliError(e))), Effect.provide(NodeServices.layer));
257
+ export { envPrefix as a, run as c, config as i, toArguments as l, Cli_exports as n, load as o, command as r, parse as s, CliError as t, usage as u };
258
+
259
+ //# sourceMappingURL=Cli-DFvng1N8.mjs.map