@manta-eu/cli 0.0.0-stage → 0.5.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manta AI AS
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,114 @@
1
- # Temporary Holding Version
1
+ # @manta-eu/cli
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 `manta` CLI keeps what a Manta datastore holds in line with files you track in git. It covers your app's
4
+ vocabulary (the classes and properties your app adds to the knowledge graph) and the SHACL shapes for those classes.
5
+
6
+ Documentation: [developers.manta.eu](https://developers.manta.eu)
7
+
8
+ ## Push your vocabulary
9
+
10
+ Keep the vocabulary as a Turtle file in your repo, under a namespace your app owns:
11
+
12
+ ```turtle
13
+ @prefix todo: <https://todomvc.example/vocab#> .
14
+
15
+ todo:Todo a rdfs:Class ; rdfs:label "Todo" ; rdfs:comment "Something to do." .
16
+ todo:title a rdf:Property ; rdfs:domain todo:Todo ; rdfs:range xsd:string ; rdfs:label "Title" .
17
+ ```
18
+
19
+ Then make the datastore hold exactly what it declares:
20
+
21
+ ```shell
22
+ pnpm manta ontology push manta/ontology.ttl --datastore your-datastore --name todomvc \
23
+ --namespace https://todomvc.example/vocab#
24
+ ```
25
+
26
+ - **Declarative.** The datastore ends up with the triples the file declares, and nothing it no longer declares.
27
+ Running it again changes nothing, so it fits a CI step after every merge.
28
+ - **Your own graph.** Each `--name` gets its own named graph, described as an `owl:Ontology` whose `dcterms:identifier`
29
+ is the name, so a push never touches the realm's ontology or another app's vocabulary. The realm's ontology is
30
+ redeployed independently, and your vocabulary survives that. Ontology coverage skips these graphs, so your
31
+ vocabulary is never counted as data.
32
+ - **Your own namespace.** `--namespace` names the namespace your app owns. Every term the file declares must be under
33
+ it.
34
+ - **Dry run.** `--dry-run` reports how many triples would be added and removed, and writes nothing.
35
+ - **Blank nodes.** Triples on blank nodes (a list, a nested description) cannot be compared between pushes, so a file
36
+ with any is rewritten on every push. The result is the same graph.
37
+
38
+ `manta ontology delete --datastore your-datastore --name todomvc` removes the vocabulary.
39
+
40
+ ## Push your shapes
41
+
42
+ SHACL shapes describe a class's properties, so `manta-codegen` can generate typed schemas for it. Keep them in a second
43
+ file, pushed under the same `--name` as your vocabulary:
44
+
45
+ ```turtle
46
+ @prefix todo: <https://todomvc.example/vocab#> .
47
+
48
+ todo:TodoShape a sh:NodeShape ;
49
+ sh:targetClass todo:Todo ;
50
+ sh:property [ sh:path todo:title ; sh:datatype xsd:string ; sh:maxCount 1 ] .
51
+ ```
52
+
53
+ ```shell
54
+ pnpm manta shapes push manta/shapes.ttl --datastore your-datastore --name todomvc
55
+ ```
56
+
57
+ - **One shapes graph.** Your shapes land in the datastore's shapes graph next to the realm's, so every reader of shapes
58
+ sees them. Each one is tagged with your `--name`, a push replaces only the shapes tagged with it, and a realm deploy
59
+ keeps them.
60
+ - **Kept outside the datastore too.** Each push also stores a copy next to the datastore's uploads, and a realm deploy
61
+ puts it back, so a rebuilt datastore gets your shapes again. Only the shapes are stored: on a rebuilt datastore, push
62
+ your vocabulary again before you push or delete shapes as a custom app. A deploy puts your shapes back without
63
+ checking them again, so a realm shape added later for one of your classes sits next to yours until you push again.
64
+ - **Classes nobody else shapes.** A shape may target your own classes or the classes your data brings in, such as one
65
+ Manta inferred from an upload; a shape you push for that class takes over from the inferred one the next time Manta
66
+ syncs the upload shapes. A class the realm's own shapes or another app's shapes already cover is refused, so your
67
+ shapes can never make the realm's or another app's data fail validation.
68
+ - **Your own names.** Name your shapes in your app's own namespace. A name in a namespace Manta uses (the realm's own,
69
+ an integration's such as `meta:`, or RDF, RDFS, OWL, XSD or SHACL), or one the realm or another app already uses, is
70
+ refused.
71
+ - **Shape predicates only.** A shapes file may use RDF, RDFS, OWL, XSD, SHACL and Dublin Core predicates, and its own.
72
+ Manta's own predicates, such as the tags that tell app and upload shapes apart, are refused.
73
+ - **Declarative.** Nothing the file no longer declares is left behind, and a second push of the same file writes nothing.
74
+ `--dry-run` reports what would change.
75
+ - **Your app's shapes only.** A custom app pushes or deletes shapes with its machine token, and only for a vocabulary
76
+ name it pushed itself. An operator may manage any app's shapes.
77
+
78
+ `manta shapes delete` removes your shapes. Delete them before your vocabulary: `manta ontology delete` is refused while
79
+ shapes pushed under the same `--name` are still in the datastore.
80
+
81
+ The vocabulary push keeps the prefixes your file declares for your own namespace, so `manta-codegen` names your schemas
82
+ after them (`todo.ts`). Declare your namespace with its prefix in the vocabulary before you push shapes for it.
83
+
84
+ The command targets hiremanta.com production unless `--api-url`, `--keycloak-url` and `--keycloak-realm` name another
85
+ deployment, as for `manta-codegen`.
86
+
87
+ ## What the API checks
88
+
89
+ The whole push is refused, and nothing is written, when the file:
90
+
91
+ - is not valid Turtle, declares nothing, or is larger than 1 MiB;
92
+ - declares a term outside `--namespace`, or names a namespace that overlaps one another app's vocabulary already
93
+ claims;
94
+ - declares a term the realm ontology or another app's vocabulary already describes;
95
+ - writes a predicate the platform derives, such as `:isTrusted`;
96
+ - gives your namespace a prefix name Manta already uses, such as `meta:`.
97
+
98
+ A shapes file is also refused when it declares no `sh:NodeShape`, uses `sh:targetNode`, `sh:targetSubjectsOf` or
99
+ `sh:targetObjectsOf`, or targets a class the realm's own shapes already cover.
100
+
101
+ `--name` is 1 to 64 lowercase letters, digits and dashes, and may not end in `ontology`, which marks the realm
102
+ ontology. `--namespace` is an http(s) IRI ending in `#` or `/`, and may not overlap a namespace Manta already owns: the
103
+ realm's own vocabulary, an integration's (such as `meta:` or `shopify:`), or RDF, RDFS, OWL, XSD, SHACL and the other
104
+ vocabularies the platform uses.
105
+
106
+ ## Log in
107
+
108
+ Login works as it does for `manta-codegen` and `manta-upload`: all three register the same commands from
109
+ `@manta-eu/auth`, and share one stored login. Log in once with `manta login`. In CI, set `MANTA_CLIENT_SECRET` for a
110
+ service account token instead.
111
+
112
+ Writing a vocabulary needs an operator's login, or a custom app's machine token when the app may write data. A token
113
+ a custom app issued to one of its users only reads, so it is refused. A vocabulary a custom app pushes records the app as
114
+ its `dcterms:creator`, and only that app, or an operator, may change or delete it.
@@ -0,0 +1,36 @@
1
+ import { type FetchLike, type TokenSource } from "@manta-eu/auth";
2
+ export type AppFilePush = {
3
+ name: string;
4
+ graphIri: string;
5
+ added: number;
6
+ removed: number;
7
+ blankNodeTriples: number;
8
+ changed: boolean;
9
+ written: boolean;
10
+ dryRun: boolean;
11
+ };
12
+ export type AppFileDeletion = {
13
+ name: string;
14
+ graphIri: string;
15
+ deleted: boolean;
16
+ };
17
+ export type AppFileCollection = "vocabularies" | "shapes";
18
+ export type AppFileLocation = {
19
+ apiUrl: string;
20
+ datastore: string;
21
+ collection: AppFileCollection;
22
+ name: string;
23
+ };
24
+ export type AppFileTarget = AppFileLocation & {
25
+ token: TokenSource;
26
+ fetchImpl?: FetchLike;
27
+ };
28
+ export declare function appFileEndpoint({ apiUrl, datastore, collection, name }: AppFileLocation): string;
29
+ export declare function pushAppFile(target: AppFileTarget & {
30
+ namespace?: string;
31
+ turtle: string;
32
+ dryRun: boolean;
33
+ }): Promise<AppFilePush>;
34
+ export declare function deleteAppFile(target: AppFileTarget): Promise<AppFileDeletion>;
35
+ export declare function describePush(push: AppFilePush): string;
36
+ //# sourceMappingURL=app-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-file.d.ts","sourceRoot":"","sources":["../src/app-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAGd,KAAK,WAAW,EAEjB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE1D,MAAM,MAAM,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjH,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAE5F,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,eAAe,GAAG,MAAM,CAEhG;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC9E,OAAO,CAAC,WAAW,CAAC,CAUtB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAE7E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAStD"}
@@ -0,0 +1,50 @@
1
+ import { describeError, readJson, RefusedRequestError, withRenewal, } from "@manta-eu/auth";
2
+ export function appFileEndpoint({ apiUrl, datastore, collection, name }) {
3
+ return `${apiUrl.replace(/\/$/, "")}/d/${encodeURIComponent(datastore)}/${collection}/${encodeURIComponent(name)}`;
4
+ }
5
+ export function pushAppFile(target) {
6
+ const endpoint = new URL(appFileEndpoint(target));
7
+ if (target.namespace !== undefined)
8
+ endpoint.searchParams.set("namespace", target.namespace);
9
+ if (target.dryRun)
10
+ endpoint.searchParams.set("dryRun", "true");
11
+ return send(target, endpoint.toString(), {
12
+ method: "PUT",
13
+ headers: { "content-type": "text/turtle" },
14
+ body: target.turtle,
15
+ });
16
+ }
17
+ export function deleteAppFile(target) {
18
+ return send(target, appFileEndpoint(target), { method: "DELETE", headers: {} });
19
+ }
20
+ export function describePush(push) {
21
+ const blankNodes = push.blankNodeTriples > 0 ? `, ${push.blankNodeTriples} blank-node triples rewritten` : "";
22
+ const changes = `${push.added} added, ${push.removed} removed${blankNodes}`;
23
+ const dryRun = push.dryRun ? " (dry run, nothing written)" : "";
24
+ if (!push.changed)
25
+ return `${push.name} is already up to date${dryRun}`;
26
+ if (push.dryRun)
27
+ return `${push.name} would change: ${changes}${dryRun}`;
28
+ return `updated ${push.name} in <${push.graphIri}>: ${changes}`;
29
+ }
30
+ async function send({ token, fetchImpl = fetch }, endpoint, init) {
31
+ return withRenewal(token, async (accessToken) => {
32
+ let response;
33
+ try {
34
+ response = await fetchImpl(endpoint, {
35
+ ...init,
36
+ headers: { ...init.headers, authorization: `Bearer ${accessToken}` },
37
+ });
38
+ }
39
+ catch (cause) {
40
+ throw new Error(`cannot reach ${endpoint}. ${describeError(cause)}`, { cause });
41
+ }
42
+ const body = await readJson(response, endpoint);
43
+ if (!response.ok) {
44
+ const reason = typeof body.error === "string" ? `: ${body.error}` : "";
45
+ throw new RefusedRequestError(`${endpoint} answered status ${response.status}${reason}`, response.status);
46
+ }
47
+ return body;
48
+ });
49
+ }
50
+ //# sourceMappingURL=app-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-file.js","sourceRoot":"","sources":["../src/app-file.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import { clientIdHelp, cliTokenSource, defaultDeployment, describeError, registerAuthCommands, } from "@manta-eu/auth";
3
+ import { Command } from "commander";
4
+ import { readFile } from "node:fs/promises";
5
+ import { deleteAppFile, describePush, pushAppFile } from "./app-file.js";
6
+ const label = "manta";
7
+ function withTargetOptions(command) {
8
+ return command
9
+ .option("--api-url <url>", "Manta API base URL", defaultDeployment.apiUrl)
10
+ .requiredOption("--datastore <id>", "datastore to push to")
11
+ .requiredOption("--name <name>", "your app's name: lowercase letters, digits and dashes")
12
+ .option("--keycloak-url <url>", "Keycloak base URL", defaultDeployment.keycloakUrl)
13
+ .option("--keycloak-realm <realm>", "Keycloak realm", defaultDeployment.keycloakRealm)
14
+ .option("--client-id <id>", clientIdHelp)
15
+ .option("--scope <scope>", "scope to ask a service account token for")
16
+ .option("--use-device-code", "if a login is needed, use a one-time code instead of a browser redirect");
17
+ }
18
+ function targetFrom(collection, options) {
19
+ return {
20
+ apiUrl: options.apiUrl,
21
+ datastore: options.datastore,
22
+ collection,
23
+ name: options.name,
24
+ token: cliTokenSource(options, { label }),
25
+ };
26
+ }
27
+ async function run(action) {
28
+ try {
29
+ console.error(`${label}: ${await action()}`);
30
+ }
31
+ catch (error) {
32
+ console.error(`${label}: ${describeError(error)}`);
33
+ process.exitCode = 1;
34
+ }
35
+ }
36
+ const program = new Command()
37
+ .name(label)
38
+ .description("Keep what a Manta datastore holds in line with files you track in git.");
39
+ function registerAppFileCommands({ command, collection, description, noun, addPushOptions }) {
40
+ const group = program.command(command).description(description);
41
+ const push = group
42
+ .command("push")
43
+ .description(`make the datastore hold exactly the ${noun} the Turtle file declares`)
44
+ .argument("<file>", `Turtle file with your app's ${noun}`)
45
+ .option("--dry-run", "report what would change without writing it");
46
+ addPushOptions?.(push);
47
+ withTargetOptions(push).action((file, options) => run(async () => {
48
+ const turtle = await readFile(file, "utf8");
49
+ const pushed = await pushAppFile({
50
+ ...targetFrom(collection, options),
51
+ namespace: options.namespace,
52
+ turtle,
53
+ dryRun: options.dryRun === true,
54
+ });
55
+ return describePush(pushed);
56
+ }));
57
+ withTargetOptions(group.command("delete").description(`remove your app's ${noun} from the datastore`)).action((options) => run(async () => {
58
+ const deletion = await deleteAppFile(targetFrom(collection, options));
59
+ return deletion.deleted ? `deleted ${deletion.name}` : `${deletion.name} was not in the datastore`;
60
+ }));
61
+ }
62
+ registerAppFileCommands({
63
+ command: "ontology",
64
+ collection: "vocabularies",
65
+ description: "your app's vocabulary: its classes and properties",
66
+ noun: "vocabulary",
67
+ addPushOptions: (push) => push.requiredOption("--namespace <iri>", "your app's namespace, ending in # or /: every term the file declares is under it"),
68
+ });
69
+ registerAppFileCommands({
70
+ command: "shapes",
71
+ collection: "shapes",
72
+ description: "your app's SHACL shapes",
73
+ noun: "shapes",
74
+ });
75
+ registerAuthCommands(program, { label });
76
+ await program.parseAsync();
77
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { appFileEndpoint, deleteAppFile, describePush, pushAppFile, type AppFileCollection, type AppFileDeletion, type AppFileLocation, type AppFilePush, type AppFileTarget, } from "./app-file.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,aAAa,EACb,YAAY,EACZ,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { appFileEndpoint, deleteAppFile, describePush, pushAppFile, } from "./app-file.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,aAAa,EACb,YAAY,EACZ,WAAW,GAMZ,MAAM,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@manta-eu/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.5.1",
4
+ "description": "CLI that keeps what a Manta datastore holds in line with files you track in git: your app's vocabulary and shapes",
5
+ "homepage": "https://developers.manta.eu",
6
+ "keywords": [
7
+ "manta",
8
+ "knowledge-graph",
9
+ "ontology",
10
+ "cli"
11
+ ],
12
+ "type": "module",
13
+ "bin": {
14
+ "manta": "./dist/cli.js"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "author": "",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "commander": "^15.0.0",
26
+ "@manta-eu/auth": "0.5.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^24.13.3",
30
+ "eslint": "^9.39.2",
31
+ "rimraf": "^6.1.3",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.1.8",
34
+ "@manta-eu/eslint-config": "0.1.0"
35
+ },
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "!src/**/*.test.*",
42
+ "!src/**/*.stories.*",
43
+ "!src/**/__fixtures__",
44
+ "!src/coverage"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "rimraf dist && tsc -p tsconfig.build.json",
51
+ "test": "vitest --run",
52
+ "types": "tsc --noEmit",
53
+ "lint": "eslint .",
54
+ "lint:fix": "eslint . --fix"
55
+ }
6
56
  }
@@ -0,0 +1,89 @@
1
+ import {
2
+ describeError,
3
+ type FetchLike,
4
+ readJson,
5
+ RefusedRequestError,
6
+ type TokenSource,
7
+ withRenewal,
8
+ } from "@manta-eu/auth";
9
+
10
+ export type AppFilePush = {
11
+ name: string;
12
+ graphIri: string;
13
+ added: number;
14
+ removed: number;
15
+ blankNodeTriples: number;
16
+ changed: boolean;
17
+ written: boolean;
18
+ dryRun: boolean;
19
+ };
20
+
21
+ export type AppFileDeletion = { name: string; graphIri: string; deleted: boolean };
22
+
23
+ export type AppFileCollection = "vocabularies" | "shapes";
24
+
25
+ export type AppFileLocation = { apiUrl: string; datastore: string; collection: AppFileCollection; name: string };
26
+
27
+ export type AppFileTarget = AppFileLocation & { token: TokenSource; fetchImpl?: FetchLike };
28
+
29
+ export function appFileEndpoint({ apiUrl, datastore, collection, name }: AppFileLocation): string {
30
+ return `${apiUrl.replace(/\/$/, "")}/d/${encodeURIComponent(datastore)}/${collection}/${encodeURIComponent(name)}`;
31
+ }
32
+
33
+ export function pushAppFile(
34
+ target: AppFileTarget & { namespace?: string; turtle: string; dryRun: boolean },
35
+ ): Promise<AppFilePush> {
36
+ const endpoint = new URL(appFileEndpoint(target));
37
+ if (target.namespace !== undefined) endpoint.searchParams.set("namespace", target.namespace);
38
+ if (target.dryRun) endpoint.searchParams.set("dryRun", "true");
39
+
40
+ return send<AppFilePush>(target, endpoint.toString(), {
41
+ method: "PUT",
42
+ headers: { "content-type": "text/turtle" },
43
+ body: target.turtle,
44
+ });
45
+ }
46
+
47
+ export function deleteAppFile(target: AppFileTarget): Promise<AppFileDeletion> {
48
+ return send<AppFileDeletion>(target, appFileEndpoint(target), { method: "DELETE", headers: {} });
49
+ }
50
+
51
+ export function describePush(push: AppFilePush): string {
52
+ const blankNodes = push.blankNodeTriples > 0 ? `, ${push.blankNodeTriples} blank-node triples rewritten` : "";
53
+ const changes = `${push.added} added, ${push.removed} removed${blankNodes}`;
54
+ const dryRun = push.dryRun ? " (dry run, nothing written)" : "";
55
+
56
+ if (!push.changed) return `${push.name} is already up to date${dryRun}`;
57
+ if (push.dryRun) return `${push.name} would change: ${changes}${dryRun}`;
58
+
59
+ return `updated ${push.name} in <${push.graphIri}>: ${changes}`;
60
+ }
61
+
62
+ async function send<T>(
63
+ { token, fetchImpl = fetch }: AppFileTarget,
64
+ endpoint: string,
65
+ init: RequestInit & { headers: Record<string, string> },
66
+ ): Promise<T> {
67
+ return withRenewal(token, async (accessToken) => {
68
+ let response: Response;
69
+
70
+ try {
71
+ response = await fetchImpl(endpoint, {
72
+ ...init,
73
+ headers: { ...init.headers, authorization: `Bearer ${accessToken}` },
74
+ });
75
+ } catch (cause) {
76
+ throw new Error(`cannot reach ${endpoint}. ${describeError(cause)}`, { cause });
77
+ }
78
+
79
+ const body = await readJson<T & { error?: unknown }>(response, endpoint);
80
+
81
+ if (!response.ok) {
82
+ const reason = typeof body.error === "string" ? `: ${body.error}` : "";
83
+
84
+ throw new RefusedRequestError(`${endpoint} answered status ${response.status}${reason}`, response.status);
85
+ }
86
+
87
+ return body;
88
+ });
89
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ type CliTokenOptions,
4
+ clientIdHelp,
5
+ cliTokenSource,
6
+ defaultDeployment,
7
+ describeError,
8
+ registerAuthCommands,
9
+ } from "@manta-eu/auth";
10
+ import { Command } from "commander";
11
+ import { readFile } from "node:fs/promises";
12
+ import { type AppFileCollection, type AppFileTarget, deleteAppFile, describePush, pushAppFile } from "./app-file.js";
13
+
14
+ const label = "manta";
15
+
16
+ type TargetOptions = CliTokenOptions & { apiUrl: string; datastore: string; name: string };
17
+
18
+ function withTargetOptions(command: Command): Command {
19
+ return command
20
+ .option("--api-url <url>", "Manta API base URL", defaultDeployment.apiUrl)
21
+ .requiredOption("--datastore <id>", "datastore to push to")
22
+ .requiredOption("--name <name>", "your app's name: lowercase letters, digits and dashes")
23
+ .option("--keycloak-url <url>", "Keycloak base URL", defaultDeployment.keycloakUrl)
24
+ .option("--keycloak-realm <realm>", "Keycloak realm", defaultDeployment.keycloakRealm)
25
+ .option("--client-id <id>", clientIdHelp)
26
+ .option("--scope <scope>", "scope to ask a service account token for")
27
+ .option("--use-device-code", "if a login is needed, use a one-time code instead of a browser redirect");
28
+ }
29
+
30
+ function targetFrom(collection: AppFileCollection, options: TargetOptions): AppFileTarget {
31
+ return {
32
+ apiUrl: options.apiUrl,
33
+ datastore: options.datastore,
34
+ collection,
35
+ name: options.name,
36
+ token: cliTokenSource(options, { label }),
37
+ };
38
+ }
39
+
40
+ async function run(action: () => Promise<string>): Promise<void> {
41
+ try {
42
+ console.error(`${label}: ${await action()}`);
43
+ } catch (error) {
44
+ console.error(`${label}: ${describeError(error)}`);
45
+ process.exitCode = 1;
46
+ }
47
+ }
48
+
49
+ const program = new Command()
50
+ .name(label)
51
+ .description("Keep what a Manta datastore holds in line with files you track in git.");
52
+
53
+ type AppFileCommand = {
54
+ command: string;
55
+ collection: AppFileCollection;
56
+ description: string;
57
+ noun: string;
58
+ addPushOptions?: (push: Command) => Command;
59
+ };
60
+
61
+ function registerAppFileCommands({ command, collection, description, noun, addPushOptions }: AppFileCommand): void {
62
+ const group = program.command(command).description(description);
63
+ const push = group
64
+ .command("push")
65
+ .description(`make the datastore hold exactly the ${noun} the Turtle file declares`)
66
+ .argument("<file>", `Turtle file with your app's ${noun}`)
67
+ .option("--dry-run", "report what would change without writing it");
68
+ addPushOptions?.(push);
69
+
70
+ withTargetOptions(push).action((file: string, options: TargetOptions & { namespace?: string; dryRun?: boolean }) =>
71
+ run(async () => {
72
+ const turtle = await readFile(file, "utf8");
73
+ const pushed = await pushAppFile({
74
+ ...targetFrom(collection, options),
75
+ namespace: options.namespace,
76
+ turtle,
77
+ dryRun: options.dryRun === true,
78
+ });
79
+
80
+ return describePush(pushed);
81
+ }),
82
+ );
83
+
84
+ withTargetOptions(group.command("delete").description(`remove your app's ${noun} from the datastore`)).action(
85
+ (options: TargetOptions) =>
86
+ run(async () => {
87
+ const deletion = await deleteAppFile(targetFrom(collection, options));
88
+
89
+ return deletion.deleted ? `deleted ${deletion.name}` : `${deletion.name} was not in the datastore`;
90
+ }),
91
+ );
92
+ }
93
+
94
+ registerAppFileCommands({
95
+ command: "ontology",
96
+ collection: "vocabularies",
97
+ description: "your app's vocabulary: its classes and properties",
98
+ noun: "vocabulary",
99
+ addPushOptions: (push) =>
100
+ push.requiredOption(
101
+ "--namespace <iri>",
102
+ "your app's namespace, ending in # or /: every term the file declares is under it",
103
+ ),
104
+ });
105
+
106
+ registerAppFileCommands({
107
+ command: "shapes",
108
+ collection: "shapes",
109
+ description: "your app's SHACL shapes",
110
+ noun: "shapes",
111
+ });
112
+
113
+ registerAuthCommands(program, { label });
114
+
115
+ await program.parseAsync();
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export {
2
+ appFileEndpoint,
3
+ deleteAppFile,
4
+ describePush,
5
+ pushAppFile,
6
+ type AppFileCollection,
7
+ type AppFileDeletion,
8
+ type AppFileLocation,
9
+ type AppFilePush,
10
+ type AppFileTarget,
11
+ } from "./app-file.js";