@astrale-domains/shell 0.1.0-beta.46 → 0.1.0-beta.49
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/README.md +57 -5
- package/dist/domain.d.ts +49 -0
- package/dist/domain.js +12 -1
- package/dist/functions/namespace/index.js +3 -0
- package/dist/functions/namespace/link.js +30 -0
- package/dist/functions/namespace/namespace-errors.js +28 -0
- package/dist/functions/namespace/namespace-name.js +13 -0
- package/dist/functions/namespace/rename.js +31 -0
- package/dist/functions/namespace/unlink.js +21 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mutations/namespace/alias-write.js +22 -0
- package/dist/mutations/namespace/index.js +3 -0
- package/dist/mutations/namespace/link-alias.js +15 -0
- package/dist/mutations/namespace/rename-alias.js +13 -0
- package/dist/mutations/namespace/unlink-alias.js +13 -0
- package/dist/queries/namespace/index.js +1 -0
- package/dist/queries/namespace/read-designated-node.js +12 -0
- package/dist/runtime.d.ts +731 -1
- package/dist/runtime.js +10 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/modules/group/classes/group.d.ts +1 -0
- package/dist/schema/modules/group/classes/group.js +2 -0
- package/dist/schema/modules/namespace/classes/alias.d.ts +21 -0
- package/dist/schema/modules/namespace/classes/alias.js +13 -0
- package/dist/schema/modules/namespace/classes/index.d.ts +2 -0
- package/dist/schema/modules/namespace/classes/index.js +2 -0
- package/dist/schema/modules/namespace/classes/namespace.d.ts +31 -0
- package/dist/schema/modules/namespace/classes/namespace.js +10 -0
- package/dist/schema/modules/namespace/core/index.d.ts +1 -0
- package/dist/schema/modules/namespace/core/index.js +1 -0
- package/dist/schema/modules/namespace/core/views-namespace.d.ts +2 -0
- package/dist/schema/modules/namespace/core/views-namespace.js +7 -0
- package/dist/schema/modules/namespace/functions/index.js +3 -0
- package/dist/schema/modules/namespace/functions/link.js +11 -0
- package/dist/schema/modules/namespace/functions/rename.js +11 -0
- package/dist/schema/modules/namespace/functions/unlink.js +11 -0
- package/dist/schema/modules/namespace/index.d.ts +2 -0
- package/dist/schema/modules/namespace/index.js +2 -0
- package/dist/schema/modules/namespace/types/index.js +1 -0
- package/dist/schema/modules/namespace/types/namespace-name.js +7 -0
- package/dist/schema/modules/user/classes/user.d.ts +1 -0
- package/dist/schema/modules/user/classes/user.js +2 -0
- package/dist/schema/schema.d.ts +195 -0
- package/dist/schema/schema.js +8 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Shell Domain
|
|
2
2
|
|
|
3
3
|
Shell owns two concrete Identity classes, User and Group, the member_of_group relation,
|
|
4
|
-
the admin/member/applications Core groups,
|
|
4
|
+
the admin/member/applications Core groups, the Namespace class and its alias relation, the `views`
|
|
5
|
+
Core Namespace, three static methods, three Namespace methods, one Domain function, and one
|
|
5
6
|
application View, which is its entrypoint. The Schema names the Domain `Shell`. There is no Space, setup function, personal home, or public repair method.
|
|
6
7
|
|
|
7
8
|
## Contracts
|
|
@@ -12,6 +13,12 @@ application View, which is its entrypoint. The Schema names the Domain `Shell`.
|
|
|
12
13
|
| Group.assignMember | `{ member: UserPath \| GroupPath, group: GroupPath }` | `{}` |
|
|
13
14
|
| Group.unassignMember | Same | `{}` |
|
|
14
15
|
|
|
16
|
+
| Namespace method | Input | Output |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Namespace.link | `{ name: string, target: NodePath }` | `{}` |
|
|
19
|
+
| Namespace.unlink | `{ name: string }` | `{}` |
|
|
20
|
+
| Namespace.rename | `{ name: string, to: string }` | `{}` |
|
|
21
|
+
|
|
15
22
|
| Domain function | Input | Output |
|
|
16
23
|
| --- | --- | --- |
|
|
17
24
|
| listEntrypoints | `{}` | `{ entrypoints: { origin, name, view }[] }` |
|
|
@@ -27,7 +34,9 @@ The workspace opens only once listEntrypoints answers, so every profile that adm
|
|
|
27
34
|
the workspace holds it. Administrators and members hold both listEntrypoints and View.resolve
|
|
28
35
|
directly (a Core Edge cannot author Kernel `extends_with`): an invited person, whom Admin assigns
|
|
29
36
|
to Core member, can open the workspace. The Core `applications` Group carries the same two
|
|
30
|
-
capabilities for Identities outside Members
|
|
37
|
+
capabilities for Identities outside Members, and `query`: the `/v/<name>` page reads its Path with
|
|
38
|
+
a Query under the visitor's own rights, so read rules still decide what it sees. Share them
|
|
39
|
+
dynamically with
|
|
31
40
|
`Group.assignMember({ member: identity, group: applications })`, which composes the profile through
|
|
32
41
|
`extends_with`; `unassignMember` withdraws it.
|
|
33
42
|
|
|
@@ -52,6 +61,48 @@ With Kernel Host beta94 or newer, an external caller needs callable authority th
|
|
|
52
61
|
identity composition AND a passing callable Policy. Declare exact foreign methods in the calling
|
|
53
62
|
Domain's requirements; can_use no longer bypasses the Policy. Root and exact owner exceptions remain.
|
|
54
63
|
|
|
64
|
+
## Namespaces and aliases
|
|
65
|
+
|
|
66
|
+
**An alias names a Node. It grants nothing.** A name designates at most one Node in its
|
|
67
|
+
Namespace, for every author: `alias` declares the Kernel key `unique: [['source', 'slug']]` in its
|
|
68
|
+
first release, so the Kernel refuses a taken name on the final state of any Mutation, Shell's or an
|
|
69
|
+
administrator's own `mutate`. Edge identity gives a Node at most one alias per Namespace. An alias
|
|
70
|
+
is a Path, `/:shell.astrale.ai:core.views~"shell.astrale.ai:class.alias"(roadmap)`, and reaches its
|
|
71
|
+
Node only for a reader who may read that Node. Its target endpoint accepts any Node, a Class Shell
|
|
72
|
+
does not own, so the alias detaches when its Node is deleted; the Namespace stays.
|
|
73
|
+
|
|
74
|
+
The Core Namespace `views` holds the names opened at `/v/<name>`. Namespace and alias declare
|
|
75
|
+
`read: true` and `traverse: true`: a reader lists and follows the aliases whose Node it may read,
|
|
76
|
+
and an alias toward any other Node does not exist for it. Shell, through its own alias Edges,
|
|
77
|
+
finds the Node of every name, even one it does not read.
|
|
78
|
+
|
|
79
|
+
`link` names any Node its target Path reaches, whatever Domain owns it: an alias grants nothing,
|
|
80
|
+
and each reader sees only the aliases toward Nodes it may read. Offering only Nodes the
|
|
81
|
+
administrator sees is the interface's courtesy, not a server guarantee; any writer already learns
|
|
82
|
+
from a Mutation's locator whether a Path reaches a Node.
|
|
83
|
+
|
|
84
|
+
| Method | Calls | Refusals |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| link | One Mutation with Shell's authority; its locator resolves `target` | `INVALID_NAME`, `TARGET_NOT_FOUND`, `NAME_TAKEN`, `TARGET_ALREADY_ALIASED` |
|
|
87
|
+
| unlink | One read of the alias Path with Shell's authority, then one Mutation | `INVALID_NAME`, `NAME_NOT_FOUND` |
|
|
88
|
+
| rename | Same; renaming a name to itself writes nothing | `INVALID_NAME`, `NAME_NOT_FOUND`, `NAME_TAKEN` |
|
|
89
|
+
|
|
90
|
+
A name is an Edge slug in lower case (`INVALID_NAME`). That rule is Shell's: an administrator
|
|
91
|
+
writing through `mutate` bypasses it, never the key. Nothing guards a name's uniqueness but the key.
|
|
92
|
+
`unlink` and `rename` change the alias only while it still carries the name they read, so a
|
|
93
|
+
concurrent rename answers `NAME_NOT_FOUND` instead of changing another name. Shell maps the Kernel's
|
|
94
|
+
sealed refusals:
|
|
95
|
+
|
|
96
|
+
| Kernel refusal of the Mutation | link | unlink | rename |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `MUTATION_POST_STATE_INVALID`, rule `duplicate-key` | `NAME_TAKEN` | propagates | `NAME_TAKEN` |
|
|
99
|
+
| `MUTATION_PRECONDITION_FAILED` (the alias Edge exists, is gone, or carries another name) | `TARGET_ALREADY_ALIASED` | `NAME_NOT_FOUND` | `NAME_NOT_FOUND` |
|
|
100
|
+
| `MUTATION_LOCATOR_INVALID` (the target Path reaches no Node, or the target was deleted) | `TARGET_NOT_FOUND` | `NAME_NOT_FOUND` | `NAME_NOT_FOUND` |
|
|
101
|
+
|
|
102
|
+
Every other failure propagates. Core admin holds the three methods, `read` and `traverse` on
|
|
103
|
+
Namespace, and every operation on alias. Administration follows `administersShellClass` on the
|
|
104
|
+
Namespace, as for Groups.
|
|
105
|
+
|
|
55
106
|
## Flows
|
|
56
107
|
|
|
57
108
|
```text
|
|
@@ -91,9 +142,10 @@ Kernel E2E evidence, not a handler test, qualifies this behavior.
|
|
|
91
142
|
|
|
92
143
|
The Shell principal requests all Kernel root functions, the required extends_with operations,
|
|
93
144
|
and the declared grant/revoke authority. Core admin receives the full declared Shell profile,
|
|
94
|
-
including invite, assignMember and
|
|
95
|
-
Core member receives Download, Introspect, Query, Mutate, View.resolve and
|
|
96
|
-
blanket class-read grants.
|
|
145
|
+
including invite, assignMember, unassignMember, link, unlink and rename so humans inheriting admin
|
|
146
|
+
can call those methods. Core member receives Download, Introspect, Query, Mutate, View.resolve and
|
|
147
|
+
listEntrypoints, not blanket class-read grants. Core applications receives listEntrypoints,
|
|
148
|
+
View.resolve and Query.
|
|
97
149
|
The Domain definition's requirements, including Core Identity capabilities in requirements.core, are owned by domain.ts and materialized by installation,
|
|
98
150
|
never by invitation handlers.
|
|
99
151
|
|
package/dist/domain.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
|
|
|
7
7
|
readonly classes: {
|
|
8
8
|
readonly User: import("@astrale-os/sdk/schema").AuthoredNodeClass<{
|
|
9
9
|
readonly icon: import("@astrale-os/sdk/schema").ClassIcon;
|
|
10
|
+
readonly inheritance: "open";
|
|
10
11
|
readonly policies: {
|
|
11
12
|
readonly read: true;
|
|
12
13
|
};
|
|
@@ -30,6 +31,7 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
|
|
|
30
31
|
}>;
|
|
31
32
|
readonly Group: import("@astrale-os/sdk/schema").AuthoredNodeClass<{
|
|
32
33
|
readonly icon: import("@astrale-os/sdk/schema").ClassIcon;
|
|
34
|
+
readonly inheritance: "open";
|
|
33
35
|
readonly policies: {
|
|
34
36
|
readonly read: true;
|
|
35
37
|
};
|
|
@@ -67,6 +69,52 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
|
|
|
67
69
|
readonly constraints: Record<never, never>;
|
|
68
70
|
readonly effectiveProperties: never;
|
|
69
71
|
}>;
|
|
72
|
+
readonly Namespace: import("@astrale-os/sdk/schema").AuthoredNodeClass<{
|
|
73
|
+
readonly icon: import("@astrale-os/sdk/schema").ClassIcon;
|
|
74
|
+
readonly policies: {
|
|
75
|
+
readonly read: true;
|
|
76
|
+
readonly traverse: true;
|
|
77
|
+
};
|
|
78
|
+
readonly extends: readonly [import("@astrale-os/sdk/schema").KernelClass<"Named">, import("@astrale-os/sdk/schema").KernelClass<"Descriptable">];
|
|
79
|
+
readonly methods: {
|
|
80
|
+
readonly link: import("@astrale-os/sdk/schema").Method<import("@astrale-os/sdk/schema").CallableProjection<{
|
|
81
|
+
name: string;
|
|
82
|
+
target: string;
|
|
83
|
+
}, {
|
|
84
|
+
readonly mode: 'value';
|
|
85
|
+
readonly value: Record<string, never>;
|
|
86
|
+
}, "authorized", false, false, undefined, undefined>>;
|
|
87
|
+
readonly unlink: import("@astrale-os/sdk/schema").Method<import("@astrale-os/sdk/schema").CallableProjection<{
|
|
88
|
+
name: string;
|
|
89
|
+
}, {
|
|
90
|
+
readonly mode: 'value';
|
|
91
|
+
readonly value: Record<string, never>;
|
|
92
|
+
}, "authorized", false, false, undefined, undefined>>;
|
|
93
|
+
readonly rename: import("@astrale-os/sdk/schema").Method<import("@astrale-os/sdk/schema").CallableProjection<{
|
|
94
|
+
name: string;
|
|
95
|
+
to: string;
|
|
96
|
+
}, {
|
|
97
|
+
readonly mode: 'value';
|
|
98
|
+
readonly value: Record<string, never>;
|
|
99
|
+
}, "authorized", false, false, undefined, undefined>>;
|
|
100
|
+
};
|
|
101
|
+
}>;
|
|
102
|
+
readonly alias: import("@astrale-os/sdk/schema").DirectedEdgeClass<{
|
|
103
|
+
readonly depth: 0;
|
|
104
|
+
readonly abstract: false;
|
|
105
|
+
readonly extends: readonly [];
|
|
106
|
+
readonly properties: never;
|
|
107
|
+
readonly source: {
|
|
108
|
+
readonly as: "namespace";
|
|
109
|
+
readonly outgoing: "0..*";
|
|
110
|
+
};
|
|
111
|
+
readonly target: {
|
|
112
|
+
readonly as: "node";
|
|
113
|
+
readonly incoming: "0..*";
|
|
114
|
+
};
|
|
115
|
+
readonly constraints: Record<never, never>;
|
|
116
|
+
readonly effectiveProperties: never;
|
|
117
|
+
}>;
|
|
70
118
|
};
|
|
71
119
|
readonly functions: {
|
|
72
120
|
readonly listEntrypoints: import("@astrale-os/sdk/schema").Function<import("@astrale-os/sdk/schema").CallableProjection<Record<string, never>, {
|
|
@@ -94,6 +142,7 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
|
|
|
94
142
|
readonly admin: import("@astrale-os/sdk/schema").CoreNode;
|
|
95
143
|
readonly applications: import("@astrale-os/sdk/schema").CoreNode;
|
|
96
144
|
readonly member: import("@astrale-os/sdk/schema").CoreNode;
|
|
145
|
+
readonly views: import("@astrale-os/sdk/schema").CoreNode;
|
|
97
146
|
};
|
|
98
147
|
};
|
|
99
148
|
}, {
|
package/dist/domain.js
CHANGED
|
@@ -31,6 +31,9 @@ const domainRequirements = requirements({
|
|
|
31
31
|
S.classes.User.static.methods.invite,
|
|
32
32
|
S.classes.Group.static.methods.assignMember,
|
|
33
33
|
S.classes.Group.static.methods.unassignMember,
|
|
34
|
+
S.classes.Namespace.methods.link,
|
|
35
|
+
S.classes.Namespace.methods.unlink,
|
|
36
|
+
S.classes.Namespace.methods.rename,
|
|
34
37
|
// Core Edges cannot author Kernel extends_with, so administrators hold it directly.
|
|
35
38
|
S.functions.listEntrypoints,
|
|
36
39
|
],
|
|
@@ -44,6 +47,8 @@ const domainRequirements = requirements({
|
|
|
44
47
|
class: S.classes.member_of_group,
|
|
45
48
|
operations: ['create', 'read', 'traverse', 'update', 'delete'],
|
|
46
49
|
},
|
|
50
|
+
{ class: S.classes.Namespace, operations: ['read', 'traverse'] },
|
|
51
|
+
{ class: S.classes.alias, operations: ['create', 'read', 'traverse', 'update', 'delete'] },
|
|
47
52
|
{ class: K.classes.extends_with, operations: ['create', 'read', 'traverse', 'delete'] },
|
|
48
53
|
],
|
|
49
54
|
authority: {
|
|
@@ -66,8 +71,14 @@ const domainRequirements = requirements({
|
|
|
66
71
|
},
|
|
67
72
|
{
|
|
68
73
|
// For Identities outside Members: Group.assignMember(identity, applications) composes it.
|
|
74
|
+
// `query` opens `/v/<name>`: the full page reads that Path with a Query under the visitor's
|
|
75
|
+
// own rights, so read rules still decide what it sees.
|
|
69
76
|
subject: S.core.nodes.applications,
|
|
70
|
-
functions: [
|
|
77
|
+
functions: [
|
|
78
|
+
S.functions.listEntrypoints,
|
|
79
|
+
K.classes.View.static.methods.resolve,
|
|
80
|
+
K.functions.query,
|
|
81
|
+
],
|
|
71
82
|
},
|
|
72
83
|
],
|
|
73
84
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { Path } from '@astrale-os/sdk/graph/path';
|
|
3
|
+
import { linkAlias } from '../../mutations/namespace/index.js';
|
|
4
|
+
import { namespaceErrors } from './namespace-errors.js';
|
|
5
|
+
import { namespaceSlug } from './namespace-name.js';
|
|
6
|
+
/**
|
|
7
|
+
* One Mutation with Shell's own authority. An alias grants nothing and each reader sees only the
|
|
8
|
+
* aliases toward Nodes it may read, so `link` names any Node its target Path reaches; the `alias`
|
|
9
|
+
* key keeps a name to one Node for every author.
|
|
10
|
+
*/
|
|
11
|
+
export const link = defineAction()('Namespace.link', { errors: namespaceErrors }, async ({ error, mutate, input, self }) => {
|
|
12
|
+
const name = namespaceSlug(input.name);
|
|
13
|
+
if (name === undefined)
|
|
14
|
+
throw error('INVALID_NAME', { name: input.name });
|
|
15
|
+
const result = await mutate(linkAlias, {
|
|
16
|
+
namespace: self,
|
|
17
|
+
name,
|
|
18
|
+
target: Path.parse(input.target),
|
|
19
|
+
});
|
|
20
|
+
if (result.ok)
|
|
21
|
+
return {};
|
|
22
|
+
switch (result.refusal) {
|
|
23
|
+
case 'NAME_TAKEN':
|
|
24
|
+
throw error('NAME_TAKEN', { name });
|
|
25
|
+
case 'TARGET_ALREADY_ALIASED':
|
|
26
|
+
throw error('TARGET_ALREADY_ALIASED', {});
|
|
27
|
+
case 'TARGET_NOT_FOUND':
|
|
28
|
+
throw error('TARGET_NOT_FOUND', {});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const namespaceErrors = {
|
|
3
|
+
INVALID_NAME: {
|
|
4
|
+
family: 'VALIDATION_ERROR',
|
|
5
|
+
message: 'A name uses lower-case letters, digits, ".", "_" and "-", and starts and ends with a letter or digit.',
|
|
6
|
+
details: z.strictObject({ name: z.string() }),
|
|
7
|
+
},
|
|
8
|
+
TARGET_NOT_FOUND: {
|
|
9
|
+
family: 'NOT_FOUND',
|
|
10
|
+
message: 'The target Path reaches no Node.',
|
|
11
|
+
details: z.strictObject({}),
|
|
12
|
+
},
|
|
13
|
+
NAME_TAKEN: {
|
|
14
|
+
family: 'CONFLICT',
|
|
15
|
+
message: 'This name already designates a Node in this Namespace.',
|
|
16
|
+
details: z.strictObject({ name: z.string() }),
|
|
17
|
+
},
|
|
18
|
+
TARGET_ALREADY_ALIASED: {
|
|
19
|
+
family: 'CONFLICT',
|
|
20
|
+
message: 'This Node already has an alias in this Namespace; rename it instead.',
|
|
21
|
+
details: z.strictObject({}),
|
|
22
|
+
},
|
|
23
|
+
NAME_NOT_FOUND: {
|
|
24
|
+
family: 'NOT_FOUND',
|
|
25
|
+
message: 'No alias of this Namespace has this name.',
|
|
26
|
+
details: z.strictObject({ name: z.string() }),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EdgeSlug, isEdgeSlug } from '@astrale-os/sdk/graph/edge';
|
|
2
|
+
import { Path } from '@astrale-os/sdk/graph/path';
|
|
3
|
+
/** The Edge slug a name is: an exact Edge slug, in lower case. Anything else is no name. */
|
|
4
|
+
export function namespaceSlug(name) {
|
|
5
|
+
return isEdgeSlug(name) && name === name.toLowerCase() ? EdgeSlug(name) : undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The Path of one name of a Namespace: `@<namespace>~"<alias>"(<name>)`. The `alias` key comes from
|
|
9
|
+
* the admitted Domain, so the Worker never loads the Schema's builder.
|
|
10
|
+
*/
|
|
11
|
+
export function aliasPath(domain, namespace, name) {
|
|
12
|
+
return Path.id(namespace).out(domain.classes.alias.key, { slug: name });
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { renameAlias } from '../../mutations/namespace/index.js';
|
|
3
|
+
import { readDesignatedNode } from '../../queries/namespace/index.js';
|
|
4
|
+
import { namespaceErrors } from './namespace-errors.js';
|
|
5
|
+
import { aliasPath, namespaceSlug } from './namespace-name.js';
|
|
6
|
+
/**
|
|
7
|
+
* One read of the Node the name designates, then one Mutation renaming its alias while it still
|
|
8
|
+
* carries that name. Renaming a name to itself finds the Node and writes nothing.
|
|
9
|
+
*/
|
|
10
|
+
export const rename = defineAction()('Namespace.rename', { errors: namespaceErrors }, async ({ domain, error, query, mutate, input, self }) => {
|
|
11
|
+
const name = namespaceSlug(input.name);
|
|
12
|
+
if (name === undefined)
|
|
13
|
+
throw error('INVALID_NAME', { name: input.name });
|
|
14
|
+
const to = namespaceSlug(input.to);
|
|
15
|
+
if (to === undefined)
|
|
16
|
+
throw error('INVALID_NAME', { name: input.to });
|
|
17
|
+
const target = await query(readDesignatedNode, { path: aliasPath(domain, self, name) });
|
|
18
|
+
if (target === undefined)
|
|
19
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
20
|
+
if (to === name)
|
|
21
|
+
return {};
|
|
22
|
+
const result = await mutate(renameAlias, { namespace: self, target, name, to });
|
|
23
|
+
if (result.ok)
|
|
24
|
+
return {};
|
|
25
|
+
switch (result.refusal) {
|
|
26
|
+
case 'NAME_TAKEN':
|
|
27
|
+
throw error('NAME_TAKEN', { name: to });
|
|
28
|
+
case 'NAME_NOT_FOUND':
|
|
29
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { unlinkAlias } from '../../mutations/namespace/index.js';
|
|
3
|
+
import { readDesignatedNode } from '../../queries/namespace/index.js';
|
|
4
|
+
import { namespaceErrors } from './namespace-errors.js';
|
|
5
|
+
import { aliasPath, namespaceSlug } from './namespace-name.js';
|
|
6
|
+
/**
|
|
7
|
+
* One read of the Node the name designates, then one Mutation deleting its alias while it still
|
|
8
|
+
* carries that name: a concurrent rename answers NAME_NOT_FOUND instead of removing another name.
|
|
9
|
+
*/
|
|
10
|
+
export const unlink = defineAction()('Namespace.unlink', { errors: namespaceErrors }, async ({ domain, error, query, mutate, input, self }) => {
|
|
11
|
+
const name = namespaceSlug(input.name);
|
|
12
|
+
if (name === undefined)
|
|
13
|
+
throw error('INVALID_NAME', { name: input.name });
|
|
14
|
+
const target = await query(readDesignatedNode, { path: aliasPath(domain, self, name) });
|
|
15
|
+
if (target === undefined)
|
|
16
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
17
|
+
const result = await mutate(unlinkAlias, { namespace: self, target, name });
|
|
18
|
+
if (result.ok)
|
|
19
|
+
return {};
|
|
20
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
21
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ShellSchema, S, type Shell, type ShellClasses, type ShellSchemaInput, type InvitationSummary, User, Group, member_of_group, adminGroup, memberGroup, } from './schema/index.js';
|
|
1
|
+
export { ShellSchema, S, type Shell, type ShellClasses, type ShellSchemaInput, type InvitationSummary, User, Group, member_of_group, adminGroup, memberGroup, Namespace, alias, viewsNamespace, } from './schema/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ShellSchema, S, User, Group, member_of_group, adminGroup, memberGroup, } from './schema/index.js';
|
|
1
|
+
export { ShellSchema, S, User, Group, member_of_group, adminGroup, memberGroup, Namespace, alias, viewsNamespace, } from './schema/index.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const written = Object.freeze({ ok: true });
|
|
2
|
+
/** Turn the Kernel refusals an alias write expects into its Namespace refusal; others propagate. */
|
|
3
|
+
export function rejectAliasWrite(refusals) {
|
|
4
|
+
return (failure) => {
|
|
5
|
+
if (failure.outcomeKnown) {
|
|
6
|
+
if (refusals.name !== undefined &&
|
|
7
|
+
failure.code === 'MUTATION_POST_STATE_INVALID' &&
|
|
8
|
+
'rule' in failure &&
|
|
9
|
+
failure.rule === 'duplicate-key') {
|
|
10
|
+
return refused(refusals.name);
|
|
11
|
+
}
|
|
12
|
+
if (failure.code === 'MUTATION_PRECONDITION_FAILED')
|
|
13
|
+
return refused(refusals.edge);
|
|
14
|
+
if (failure.code === 'MUTATION_LOCATOR_INVALID')
|
|
15
|
+
return refused(refusals.node);
|
|
16
|
+
}
|
|
17
|
+
throw failure;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function refused(refusal) {
|
|
21
|
+
return Object.freeze({ ok: false, refusal });
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineMutation } from '@astrale-os/sdk/mutation';
|
|
2
|
+
import { rejectAliasWrite, written } from './alias-write.js';
|
|
3
|
+
/** Create the alias `name` from a Namespace to the one Node a Path reaches. */
|
|
4
|
+
export const linkAlias = defineMutation()((domain) => ({
|
|
5
|
+
id: 'shell.namespace.alias.link',
|
|
6
|
+
build({ namespace, name, target }, mutation) {
|
|
7
|
+
mutation.createEdge({ class: domain.classes.alias, source: namespace, target, slug: name });
|
|
8
|
+
},
|
|
9
|
+
project: () => written,
|
|
10
|
+
reject: rejectAliasWrite({
|
|
11
|
+
name: 'NAME_TAKEN',
|
|
12
|
+
edge: 'TARGET_ALREADY_ALIASED',
|
|
13
|
+
node: 'TARGET_NOT_FOUND',
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineMutation } from '@astrale-os/sdk/mutation';
|
|
2
|
+
import { rejectAliasWrite, written } from './alias-write.js';
|
|
3
|
+
/** Rename the alias `name` of one Node in a Namespace to `to`; the Node it names stays the same. */
|
|
4
|
+
export const renameAlias = defineMutation()((domain) => ({
|
|
5
|
+
id: 'shell.namespace.alias.rename',
|
|
6
|
+
build({ namespace, target, name, to }, mutation) {
|
|
7
|
+
const alias = { class: domain.classes.alias, source: namespace, target };
|
|
8
|
+
mutation.expect.edge({ ...alias, slug: name });
|
|
9
|
+
mutation.updateEdge({ ...alias, slug: to });
|
|
10
|
+
},
|
|
11
|
+
project: () => written,
|
|
12
|
+
reject: rejectAliasWrite({ name: 'NAME_TAKEN', edge: 'NAME_NOT_FOUND', node: 'NAME_NOT_FOUND' }),
|
|
13
|
+
}));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineMutation } from '@astrale-os/sdk/mutation';
|
|
2
|
+
import { rejectAliasWrite, written } from './alias-write.js';
|
|
3
|
+
/** Delete the alias `name` of one Node in a Namespace: Edge identity gives the Node at most one. */
|
|
4
|
+
export const unlinkAlias = defineMutation()((domain) => ({
|
|
5
|
+
id: 'shell.namespace.alias.unlink',
|
|
6
|
+
build({ namespace, target, name }, mutation) {
|
|
7
|
+
const alias = { class: domain.classes.alias, source: namespace, target };
|
|
8
|
+
mutation.expect.edge({ ...alias, slug: name });
|
|
9
|
+
mutation.deleteEdge(alias);
|
|
10
|
+
},
|
|
11
|
+
project: () => written,
|
|
12
|
+
reject: rejectAliasWrite({ edge: 'NAME_NOT_FOUND', node: 'NAME_NOT_FOUND' }),
|
|
13
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { readDesignatedNode } from './read-designated-node.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineQuery, Query, queryResult } from '@astrale-os/sdk/query';
|
|
2
|
+
/**
|
|
3
|
+
* The one Node a Path designates for the authority that reads it, or nothing: a Node it cannot
|
|
4
|
+
* see, and a name no alias holds, read the same.
|
|
5
|
+
*/
|
|
6
|
+
export const readDesignatedNode = defineQuery()(() => ({
|
|
7
|
+
id: 'shell.namespace.designated-node',
|
|
8
|
+
page: { size: 1 },
|
|
9
|
+
pagination: { maximumPages: 1 },
|
|
10
|
+
build: ({ path }) => Query.from({ nodes: [path] }).select({ kind: 'nodes', projection: { kind: 'reference' } }),
|
|
11
|
+
project: (pages) => queryResult.completeNodeIds(pages, 'Shell designated Node')[0],
|
|
12
|
+
}));
|