@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,6 @@
1
+ import { t as NodeSparqlServer_d_exports } from "./NodeSparqlServer-1vSrPk3J.mjs";
2
+ import { r as Cli_d_exports } from "./Cli-DE0eXMJr.mjs";
3
+ import { g as SparqlApi_d_exports } from "./SparqlApi-BKE9nIf4.mjs";
4
+ import { n as SparqlApiBuilder_d_exports } from "./SparqlApiBuilder-BAMpMy-Z.mjs";
5
+ import { c as SparqlApiClient_d_exports } from "./SparqlApiClient-C82RGMEF.mjs";
6
+ export { Cli_d_exports as Cli, NodeSparqlServer_d_exports as NodeSparqlServer, SparqlApi_d_exports as SparqlApi, SparqlApiBuilder_d_exports as SparqlApiBuilder, SparqlApiClient_d_exports as SparqlApiClient };
package/dist/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import { t as NodeSparqlServer_exports } from "./NodeSparqlServer-CYAd_6BG.mjs";
2
+ import { n as Cli_exports } from "./Cli-DFvng1N8.mjs";
3
+ import { m as SparqlApi_exports } from "./SparqlApi-BuycDkWu.mjs";
4
+ import { t as SparqlApiBuilder_exports } from "./SparqlApiBuilder-GT7EopBq.mjs";
5
+ import { n as SparqlApiClient_exports } from "./SparqlApiClient-CJcoFy5b.mjs";
6
+ export { Cli_exports as Cli, NodeSparqlServer_exports as NodeSparqlServer, SparqlApi_exports as SparqlApi, SparqlApiBuilder_exports as SparqlApiBuilder, SparqlApiClient_exports as SparqlApiClient };
@@ -0,0 +1,12 @@
1
+ // \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ export { __exportAll as t };
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
+ "$schema": "https://www.schemastore.org/package",
2
3
  "name": "@thomasfosterau/effect-sparql-node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.1.0",
5
+ "description": "The Node side of @thomasfosterau/effect-rdf/sparql: a SPARQL 1.1 Protocol / Graph Store server on node:http, its command-line launcher, and the typed SparqlApi HttpApi definition, server and client",
6
+ "keywords": [
7
+ "effect",
8
+ "http-api",
9
+ "rdf",
10
+ "sparql",
11
+ "sparql-protocol",
12
+ "typescript"
13
+ ],
14
+ "homepage": "https://github.com/thomasfosterau/effect-rdf/tree/main/packages/sparql-node#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/thomasfosterau/effect-rdf/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Thomas Foster <me@thomasfoster.au>",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/thomasfosterau/effect-rdf.git",
23
+ "directory": "packages/sparql-node"
24
+ },
25
+ "bin": {
26
+ "effect-sparql-server": "./bin/effect-sparql-server.mjs"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "dist",
31
+ "src",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "type": "module",
36
+ "sideEffects": false,
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.mts",
40
+ "default": "./dist/index.mjs"
41
+ },
42
+ "./*": {
43
+ "types": "./dist/*.d.mts",
44
+ "default": "./dist/*.mjs"
45
+ }
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "dependencies": {
51
+ "@effect/platform-node": "4.1.2",
52
+ "@thomasfosterau/effect-rdf": "0.1.0"
53
+ },
54
+ "devDependencies": {
55
+ "@effect/vitest": "4.1.2",
56
+ "@thomasfosterau/effect-parse": "0.4.1",
57
+ "effect": "4.1.2",
58
+ "vitest": "5.0.5"
59
+ },
60
+ "peerDependencies": {
61
+ "@thomasfosterau/effect-parse": "^0.4.0",
62
+ "effect": "^4.1.2"
63
+ },
64
+ "engines": {
65
+ "node": ">=22"
66
+ },
67
+ "scripts": {
68
+ "start": "node --experimental-transform-types --no-warnings bin/effect-sparql-server.mjs"
69
+ }
6
70
  }
package/src/Cli.ts ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * The command line entry point: serves RDF files loaded into an in-memory
3
+ * store.
4
+ *
5
+ * ```sh
6
+ * effect-sparql-server --port 3030 data.ttl more.trig
7
+ * ```
8
+ *
9
+ * Files are parsed with the default formats, chosen by file extension.
10
+ * Triples go to the default graph, or with `--graph-per-file` to a named
11
+ * graph per file (named by the file URL); quads keep their graph.
12
+ *
13
+ * The command is built with `effect/cli` (generated `--help`, `--version`
14
+ * and shell completions), and every flag also reads an environment variable
15
+ * through `Config`: `SPARQL_SERVER_` followed by the flag name in upper
16
+ * snake case (`--max-results` is `SPARQL_SERVER_MAX_RESULTS`). A flag on the
17
+ * command line wins over the environment, which wins over the default.
18
+ *
19
+ * @since 0.1.0
20
+ */
21
+ import * as NodeServices from '@effect/platform-node/NodeServices';
22
+ import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem';
23
+ import * as NodePath from '@effect/platform-node/NodePath';
24
+ import * as Config from 'effect/Config';
25
+ import * as Console from 'effect/Console';
26
+ import * as Effect from 'effect/Effect';
27
+ import * as FileSystem from 'effect/FileSystem';
28
+ import * as Layer from 'effect/Layer';
29
+ import * as Match from 'effect/Match';
30
+ import * as Option from 'effect/Option';
31
+ import * as Path from 'effect/Path';
32
+ import * as PlatformError from 'effect/PlatformError';
33
+ import * as Ref from 'effect/Ref';
34
+ import * as Schema from 'effect/Schema';
35
+ import { Argument, Command, Flag } from 'effect/cli';
36
+ import * as CliErrors from 'effect/cli/CliError';
37
+ import type * as HttpServerError from 'effect/http/HttpServerError';
38
+ import * as Iri from '@thomasfosterau/effect-rdf/Iri';
39
+ import * as Dataset from '@thomasfosterau/effect-rdf/Dataset';
40
+ import * as Serialization from '@thomasfosterau/effect-rdf/Serialization';
41
+ import * as Term from '@thomasfosterau/effect-rdf/Term';
42
+ import * as ServerFormats from '@thomasfosterau/effect-rdf/sparql/ServerFormats';
43
+ import type * as Options from '@thomasfosterau/effect-rdf/sparql/Options';
44
+ import * as NodeSparqlServer from './NodeSparqlServer.ts';
45
+
46
+ /**
47
+ * A command line usage error.
48
+ *
49
+ * @since 0.1.0
50
+ * @category errors
51
+ */
52
+ export class CliError extends Schema.TaggedError<CliError>()('CliError', {
53
+ message: Schema.String,
54
+ }) {}
55
+
56
+ /**
57
+ * The prefix of the environment variables that back the flags:
58
+ * `SPARQL_SERVER_PORT`, `SPARQL_SERVER_READ_ONLY`, …
59
+ *
60
+ * @since 0.1.0
61
+ * @category cli
62
+ */
63
+ export const envPrefix = 'SPARQL_SERVER_';
64
+
65
+ const envName = (flag: string): string => `${envPrefix}${flag.toUpperCase().replaceAll('-', '_')}`;
66
+
67
+ /**
68
+ * A summary of the flags and their environment variables; `--help` prints
69
+ * the full, generated help.
70
+ *
71
+ * @since 0.1.0
72
+ * @category cli
73
+ */
74
+ export const usage = `Usage: effect-sparql-server [flags] [files...]
75
+
76
+ Serves the given RDF files from an in-memory store over the SPARQL 1.1
77
+ Protocol and the Graph Store HTTP Protocol.
78
+
79
+ Flags (environment variable in brackets; a flag wins over the environment):
80
+ -p, --port <port> port to listen on (default 3030) [SPARQL_SERVER_PORT]
81
+ --host <host> host to bind (default all interfaces) [SPARQL_SERVER_HOST]
82
+ --path <path> SPARQL endpoint path (default /sparql) [SPARQL_SERVER_PATH]
83
+ --update-path <path> separate update endpoint path [SPARQL_SERVER_UPDATE_PATH]
84
+ --graph-store <path> Graph Store Protocol path (default /rdf-graph-store) [SPARQL_SERVER_GRAPH_STORE]
85
+ --no-graph-store disable the Graph Store Protocol [SPARQL_SERVER_NO_GRAPH_STORE]
86
+ --read-only reject updates and graph store writes [SPARQL_SERVER_READ_ONLY]
87
+ --timeout <ms> query timeout in milliseconds [SPARQL_SERVER_TIMEOUT]
88
+ --max-results <n> maximum number of results per query [SPARQL_SERVER_MAX_RESULTS]
89
+ --cors enable permissive CORS [SPARQL_SERVER_CORS]
90
+ --union-default-graph query the union of all graphs by default [SPARQL_SERVER_UNION_DEFAULT_GRAPH]
91
+ --graph-per-file load each file into a named graph (its file URL) [SPARQL_SERVER_GRAPH_PER_FILE]
92
+ --base-url <url> public origin of the server [SPARQL_SERVER_BASE_URL]
93
+ -h, --help show the full help
94
+ `;
95
+
96
+ /**
97
+ * Parsed command line arguments.
98
+ *
99
+ * @since 0.1.0
100
+ * @category models
101
+ */
102
+ export interface Arguments {
103
+ readonly help: boolean;
104
+ readonly files: ReadonlyArray<string>;
105
+ readonly graphPerFile: boolean;
106
+ readonly unionDefaultGraph: boolean;
107
+ readonly serve: NodeSparqlServer.ServeOptions;
108
+ }
109
+
110
+ // A string flag, falling back to its environment variable, absent when neither is set.
111
+ const stringFlag = (name: string, description: string) =>
112
+ Flag.String(name).pipe(
113
+ Flag.withDescription(`${description} [${envName(name)}]`),
114
+ Flag.withFallbackConfig(Config.String(envName(name))),
115
+ Flag.optional,
116
+ );
117
+
118
+ // An integer flag, falling back to its environment variable.
119
+ const intFlag = (name: string, description: string) =>
120
+ Flag.Int(name).pipe(
121
+ Flag.withDescription(`${description} [${envName(name)}]`),
122
+ Flag.withFallbackConfig(Config.Int(envName(name))),
123
+ Flag.optional,
124
+ );
125
+
126
+ // A boolean flag, falling back to its environment variable, then to false.
127
+ const booleanFlag = (name: string, description: string) =>
128
+ Flag.Boolean(name).pipe(
129
+ Flag.withDescription(`${description} [${envName(name)}]`),
130
+ Flag.withFallbackConfig(Config.Boolean(envName(name))),
131
+ Flag.withDefault(false),
132
+ );
133
+
134
+ /**
135
+ * The command's flags and arguments.
136
+ *
137
+ * @since 0.1.0
138
+ * @category cli
139
+ */
140
+ export const config = {
141
+ port: intFlag('port', 'port to listen on (default 3030)').pipe(Flag.withAlias('p')),
142
+ host: stringFlag('host', 'host to bind (default all interfaces)'),
143
+ path: stringFlag('path', 'SPARQL endpoint path (default /sparql)'),
144
+ updatePath: stringFlag('update-path', 'separate update endpoint path'),
145
+ graphStore: stringFlag('graph-store', 'Graph Store Protocol path (default /rdf-graph-store)'),
146
+ noGraphStore: booleanFlag('no-graph-store', 'disable the Graph Store Protocol'),
147
+ readOnly: booleanFlag('read-only', 'reject updates and graph store writes'),
148
+ timeout: intFlag('timeout', 'query timeout in milliseconds'),
149
+ maxResults: intFlag('max-results', 'maximum number of results per query'),
150
+ cors: booleanFlag('cors', 'enable permissive CORS'),
151
+ unionDefaultGraph: booleanFlag('union-default-graph', 'query the union of all graphs by default'),
152
+ graphPerFile: booleanFlag('graph-per-file', 'load each file into a named graph (its file URL)'),
153
+ baseUrl: stringFlag('base-url', 'public origin of the server'),
154
+ files: Argument.String('files').pipe(
155
+ Argument.withDescription('RDF files to load, in any syntax the server reads'),
156
+ Argument.variadic(),
157
+ ),
158
+ } as const;
159
+
160
+ /**
161
+ * The values of the flags and arguments, as `effect/cli` parses them.
162
+ *
163
+ * @since 0.1.0
164
+ * @category models
165
+ */
166
+ export type Flags = Command.Command.Config.Infer<typeof config>;
167
+
168
+ const nonNegative = (name: string, value: Option.Option<number>) =>
169
+ Option.match(value, {
170
+ onNone: () => Effect.succeed(undefined),
171
+ onSome: (n) =>
172
+ Number.isInteger(n) && n >= 0
173
+ ? Effect.succeed(n)
174
+ : Effect.fail(new CliError({ message: `--${name} expects a non-negative integer` })),
175
+ });
176
+
177
+ const endpointPath = (name: string, value: Option.Option<string>) =>
178
+ Option.match(value, {
179
+ onNone: () => Effect.succeed(undefined),
180
+ onSome: (path) =>
181
+ Schema.is(EndpointPath)(path)
182
+ ? Effect.succeed(path)
183
+ : Effect.fail(new CliError({ message: `--${name} must start with '/'` })),
184
+ });
185
+
186
+ const EndpointPath = Schema.TemplateLiteral(['/', Schema.String]);
187
+
188
+ /**
189
+ * Validates parsed flags into `Arguments`: integers must be non-negative,
190
+ * paths must start with `/`, and `--base-url` must be an absolute IRI.
191
+ *
192
+ * @since 0.1.0
193
+ * @category cli
194
+ */
195
+ export const toArguments = (flags: Flags): Effect.Effect<Arguments, CliError> =>
196
+ Effect.gen(function* () {
197
+ const baseUrl = Option.getOrUndefined(flags.baseUrl);
198
+ if (baseUrl !== undefined && !Iri.isValid(baseUrl)) {
199
+ return yield* new CliError({ message: `--base-url must be an absolute IRI: ${baseUrl}` });
200
+ }
201
+ const graphStore = yield* endpointPath('graph-store', flags.graphStore);
202
+ const options: Options.Options = {
203
+ path: yield* endpointPath('path', flags.path),
204
+ updatePath: yield* endpointPath('update-path', flags.updatePath),
205
+ graphStorePath: flags.noGraphStore ? false : graphStore,
206
+ readOnly: flags.readOnly,
207
+ queryTimeout: yield* nonNegative('timeout', flags.timeout),
208
+ maxResults: yield* nonNegative('max-results', flags.maxResults),
209
+ cors: flags.cors,
210
+ logging: true,
211
+ baseUrl,
212
+ unionDefaultGraph: flags.unionDefaultGraph,
213
+ };
214
+ return {
215
+ help: false,
216
+ files: flags.files,
217
+ graphPerFile: flags.graphPerFile,
218
+ unionDefaultGraph: flags.unionDefaultGraph,
219
+ serve: {
220
+ ...options,
221
+ port: (yield* nonNegative('port', flags.port)) ?? 3030,
222
+ host: Option.getOrUndefined(flags.host),
223
+ },
224
+ };
225
+ });
226
+
227
+ // The `effect/cli` errors as this module's `CliError` (a help request has no message).
228
+ const fromCliError = (error: CliErrors.CliError): CliError =>
229
+ new CliError({
230
+ message: Match.value(error).pipe(
231
+ Match.tag('ShowHelp', (help) => help.errors.map((e) => e.message).join('\n')),
232
+ Match.orElse((e) => e.message),
233
+ ),
234
+ });
235
+
236
+ const quiet = (): void => undefined;
237
+ const silentConsole: Console.Console = Object.assign(Object.create(globalThis.console), {
238
+ log: quiet,
239
+ error: quiet,
240
+ info: quiet,
241
+ warn: quiet,
242
+ });
243
+
244
+ const isHelp = (argv: ReadonlyArray<string>): boolean =>
245
+ argv.includes('--help') || argv.includes('-h');
246
+
247
+ /**
248
+ * Parses command line arguments, reading unset flags from the environment
249
+ * (`SPARQL_SERVER_*`). `-h` / `--help` sets `help` instead.
250
+ *
251
+ * @since 0.1.0
252
+ * @category cli
253
+ */
254
+ export const parse = (argv: ReadonlyArray<string>): Effect.Effect<Arguments, CliError> =>
255
+ isHelp(argv)
256
+ ? Effect.succeed({
257
+ help: true,
258
+ files: [],
259
+ graphPerFile: false,
260
+ unionDefaultGraph: false,
261
+ serve: { port: 3030 },
262
+ })
263
+ : Effect.gen(function* () {
264
+ const parsed = yield* Ref.make(Option.none<Arguments>());
265
+ const capture = Command.make('effect-sparql-server', config, (flags) =>
266
+ Effect.flatMap(toArguments(flags), (args) => Ref.set(parsed, Option.some(args))),
267
+ );
268
+ yield* Command.runWith(capture, { version: '', renderErrors: false })(argv).pipe(
269
+ Effect.catchIf(CliErrors.isCliError, (e) => Effect.fail(fromCliError(e))),
270
+ // Parsing is programmatic: the help `effect/cli` prints for a usage error stays quiet.
271
+ Effect.provideService(Console.Console, silentConsole),
272
+ Effect.provide(NodeServices.layer),
273
+ );
274
+ return yield* Effect.flatMap(Ref.get(parsed), Effect.fromOption).pipe(
275
+ Effect.mapError(() => new CliError({ message: 'no arguments were parsed' })),
276
+ );
277
+ });
278
+
279
+ /**
280
+ * Loads RDF files into a dataset through Effect's `FileSystem`, choosing
281
+ * parsers by file extension from the `Formats` service. Relative IRIs
282
+ * resolve against each file's `file:` URL; with `graphPerFile`, a file's
283
+ * default-graph triples go into a graph named by that URL.
284
+ *
285
+ * @since 0.1.0
286
+ * @category cli
287
+ */
288
+ export const load = (
289
+ files: ReadonlyArray<string>,
290
+ graphPerFile: boolean,
291
+ ): Effect.Effect<
292
+ Dataset.Dataset,
293
+ CliError | Serialization.RdfParseError,
294
+ Serialization.Formats | FileSystem.FileSystem | Path.Path
295
+ > =>
296
+ Effect.gen(function* () {
297
+ const formats = yield* Serialization.Formats;
298
+ const paths = yield* Path.Path;
299
+ const perFile = yield* Effect.forEach(
300
+ files,
301
+ Effect.fnUntraced(function* (file) {
302
+ const parser = Serialization.parserForExtension(formats, paths.extname(file));
303
+ if (Option.isNone(parser)) {
304
+ return yield* new CliError({ message: `Unknown RDF file extension: ${file}` });
305
+ }
306
+ const cannotRead = (e: PlatformError.PlatformError) =>
307
+ new CliError({ message: `Cannot read ${file}: ${e.message}` });
308
+ const url = (yield* paths
309
+ .toFileUrl(paths.resolve(file))
310
+ .pipe(Effect.mapError((e) => cannotRead(new PlatformError.PlatformError(e))))).href;
311
+ const result = yield* Serialization.readFile(file, {
312
+ parser: parser.value,
313
+ baseIri: url,
314
+ }).pipe(Effect.catchTag('PlatformError', (e) => Effect.fail(cannotRead(e))));
315
+ const graph = graphPerFile ? Term.namedNode(url) : undefined;
316
+ const quads = Array.from(result.dataset, (q) =>
317
+ graph !== undefined && q.graph._tag === 'DefaultGraph'
318
+ ? new Term.Quad(q.subject, q.predicate, q.object, graph)
319
+ : q,
320
+ );
321
+ yield* Effect.logInfo(`Loaded ${result.dataset.size} quads from ${file}`);
322
+ return quads;
323
+ }),
324
+ { concurrency: 1 },
325
+ );
326
+ return Dataset.fromIterable(perFile.flat());
327
+ });
328
+
329
+ /**
330
+ * The `effect/cli` command: parses the flags (and their environment
331
+ * variables), loads the files and serves them until interrupted.
332
+ *
333
+ * @since 0.1.0
334
+ * @category cli
335
+ */
336
+ export const command = Command.make('effect-sparql-server', config, (flags) =>
337
+ Effect.gen(function* () {
338
+ const args = yield* toArguments(flags);
339
+ const data = yield* Effect.provide(
340
+ load(args.files, args.graphPerFile),
341
+ Layer.mergeAll(ServerFormats.layer, NodeFileSystem.layer, NodePath.layer),
342
+ );
343
+ const server = NodeSparqlServer.layerMemory({
344
+ ...args.serve,
345
+ data,
346
+ engine: { unionDefaultGraph: args.unionDefaultGraph },
347
+ });
348
+ return yield* Layer.launch(server);
349
+ }),
350
+ ).pipe(
351
+ Command.withDescription(
352
+ 'Serves the given RDF files from an in-memory store over the SPARQL 1.1 Protocol and the Graph Store HTTP Protocol.',
353
+ ),
354
+ );
355
+
356
+ /**
357
+ * Runs the command line program: parses arguments (printing the generated
358
+ * help for `--help` or a usage error), loads the files and serves them
359
+ * until interrupted. `version` is what `--version` prints.
360
+ *
361
+ * @since 0.1.0
362
+ * @category cli
363
+ */
364
+ export const run = (
365
+ argv: ReadonlyArray<string>,
366
+ options?: { readonly version?: string | undefined },
367
+ ): Effect.Effect<void, CliError | Serialization.RdfParseError | HttpServerError.ServeError> =>
368
+ Command.runWith(command, { version: options?.version ?? 'unknown', renderErrors: false })(
369
+ argv,
370
+ ).pipe(
371
+ Effect.catchIf(CliErrors.isCliError, (e) => Effect.fail(fromCliError(e))),
372
+ Effect.provide(NodeServices.layer),
373
+ );
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The SPARQL 1.1 Protocol and Graph Store HTTP Protocol server of
3
+ * `@thomasfosterau/effect-rdf/sparql` (`SparqlServer`) on a Node
4
+ * `node:http` server, through `@effect/platform-node`'s `NodeHttpServer`.
5
+ *
6
+ * `SparqlServer` itself is platform-neutral (`handler`, `layerRoutes`,
7
+ * `layerServe`); this module adds the listening socket.
8
+ *
9
+ * @since 0.1.0
10
+ */
11
+ import { createServer } from 'node:http';
12
+ import * as Layer from 'effect/Layer';
13
+ import type * as HttpServer from 'effect/http/HttpServer';
14
+ import type * as HttpServerError from 'effect/http/HttpServerError';
15
+ import * as NodeHttpServer from '@effect/platform-node/NodeHttpServer';
16
+ import type * as Dataset from '@thomasfosterau/effect-rdf/Dataset';
17
+ import type * as Engine from '@thomasfosterau/effect-rdf/sparql/Engine';
18
+ import type * as Options from '@thomasfosterau/effect-rdf/sparql/Options';
19
+ import * as SparqlServer from '@thomasfosterau/effect-rdf/sparql/SparqlServer';
20
+
21
+ /**
22
+ * Options of the Node server.
23
+ *
24
+ * @since 0.1.0
25
+ * @category models
26
+ */
27
+ export interface ServeOptions extends Options.Options {
28
+ /** The port to listen on (`0` for an ephemeral port). */
29
+ readonly port: number;
30
+ /** The host to bind (default: all interfaces). */
31
+ readonly host?: string | undefined;
32
+ }
33
+
34
+ /**
35
+ * A Node HTTP server serving the SPARQL endpoint and graph store.
36
+ *
37
+ * @since 0.1.0
38
+ * @category layers
39
+ */
40
+ export const layer = (
41
+ options: ServeOptions,
42
+ ): Layer.Layer<HttpServer.HttpServer, HttpServerError.ServeError, SparqlServer.Requirements> => {
43
+ const { port, host, ...rest } = options;
44
+ // SAFETY: providing the Node HTTP server leaves `HttpServer` and `SparqlServer.Requirements`; the cast
45
+ // restates the layer type.
46
+ return SparqlServer.layerServe(rest).pipe(
47
+ Layer.provideMerge(
48
+ NodeHttpServer.layer(createServer, host === undefined ? { port } : { port, host }),
49
+ ),
50
+ );
51
+ };
52
+
53
+ /**
54
+ * A Node server over an in-memory store.
55
+ *
56
+ * @since 0.1.0
57
+ * @category layers
58
+ */
59
+ export const layerMemory = (
60
+ options: ServeOptions & {
61
+ readonly data?: Dataset.Dataset | undefined;
62
+ readonly engine?: Engine.EngineOptions | undefined;
63
+ },
64
+ ): Layer.Layer<HttpServer.HttpServer, HttpServerError.ServeError> => {
65
+ const { data, engine, ...rest } = options;
66
+ return layer({
67
+ ...rest,
68
+ unionDefaultGraph: rest.unionDefaultGraph ?? engine?.unionDefaultGraph,
69
+ federation: rest.federation ?? engine?.service !== undefined,
70
+ }).pipe(Layer.provide(SparqlServer.layerMemoryServices(data, engine)));
71
+ };