@astrale-domains/shell 0.1.7 → 0.1.8

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 (41) hide show
  1. package/dist/domain.d.ts +24 -2
  2. package/dist/domain.js +13 -1
  3. package/dist/functions/authority/roles.js +45 -30
  4. package/dist/functions/group/assign-member.js +6 -3
  5. package/dist/functions/group/change-group-membership.js +86 -39
  6. package/dist/functions/group/group-membership-errors.js +1 -1
  7. package/dist/functions/group/index.js +2 -2
  8. package/dist/functions/group/legacy/index.js +1 -1
  9. package/dist/functions/group/legacy/membership.js +21 -17
  10. package/dist/functions/group/membership-laws.js +5 -8
  11. package/dist/functions/group/unassign-member.js +6 -3
  12. package/dist/functions/invitation/send-invitation.js +57 -7
  13. package/dist/mutations/group/index.js +1 -1
  14. package/dist/mutations/group/set-group-membership.js +45 -13
  15. package/dist/mutations/invitation/write-invitation.js +19 -2
  16. package/dist/queries/composition/index.js +1 -1
  17. package/dist/queries/composition/read-role-nodes.js +48 -13
  18. package/dist/queries/group/index.js +1 -0
  19. package/dist/queries/group/legacy/index.js +1 -0
  20. package/dist/queries/group/legacy/read-members-of-group.js +27 -0
  21. package/dist/queries/group/read-group-members.js +73 -0
  22. package/dist/queries/invitation/index.js +1 -1
  23. package/dist/queries/invitation/read-invitation.js +33 -8
  24. package/dist/queries/role/index.js +1 -1
  25. package/dist/queries/role/read-role-node.js +8 -0
  26. package/dist/runtime.d.ts +2172 -64
  27. package/dist/runtime.js +3 -1
  28. package/dist/schema/modules/application/legacy/functions/list-entrypoints.js +5 -4
  29. package/dist/schema/modules/group/classes/group-profile-link.d.ts +1 -1
  30. package/dist/schema/modules/group/classes/group.d.ts +14 -0
  31. package/dist/schema/modules/group/classes/group.js +12 -2
  32. package/dist/schema/modules/group/functions/assign-members.js +17 -0
  33. package/dist/schema/modules/group/functions/index.js +2 -0
  34. package/dist/schema/modules/group/functions/unassign-members.js +17 -0
  35. package/dist/schema/modules/group/types/group-membership-input.js +8 -0
  36. package/dist/schema/modules/group/types/index.js +1 -1
  37. package/dist/schema/modules/invitation/classes/invitation.js +4 -2
  38. package/dist/schema/modules/role/core/roles.d.ts +2 -0
  39. package/dist/schema/modules/role/core/roles.js +1 -0
  40. package/dist/schema/schema.d.ts +95 -8
  41. package/package.json +2 -2
@@ -1,6 +1,46 @@
1
1
  import { defineMutation } from '@astrale-os/sdk/mutation';
2
2
  // Seam L-SHELL-8: the legacy membership Edge a change may converge beside `extends_with`.
3
3
  import { legacyMembershipClass } from './legacy/index.js';
4
+ const rejectChange = (failure) => {
5
+ if (failure.outcomeKnown &&
6
+ failure.code !== 'MUTATION_PRECONDITION_FAILED' &&
7
+ failure.code !== 'MUTATION_CONFLICT' &&
8
+ failure.code !== 'MUTATION_POST_STATE_INVALID')
9
+ throw failure;
10
+ return {
11
+ ok: false,
12
+ code: failure.code,
13
+ ...(failure.rule !== undefined ? { rule: failure.rule } : {}),
14
+ outcomeKnown: failure.outcomeKnown,
15
+ };
16
+ };
17
+ /**
18
+ * Create or delete membership Edges of several members of one group in one commit, under the few
19
+ * guards that cover all of them: every change is written, or none.
20
+ */
21
+ export const setGroupMemberships = defineMutation()((domain) => ({
22
+ id: 'shell.group.memberships.set',
23
+ build(input, mutation) {
24
+ for (const guard of input.guards)
25
+ mutation.expect.query(guard);
26
+ for (const change of input.changes) {
27
+ const endpoints = { source: { id: change.member }, target: { id: input.group } };
28
+ const selected = change.edge === 'authority'
29
+ ? domain.dependencies.kernel.classes.extends_with
30
+ : legacyMembershipClass(domain);
31
+ if (change.present)
32
+ mutation.createEdge({ class: selected, ...endpoints });
33
+ else
34
+ mutation.deleteEdge({ class: selected, ...endpoints });
35
+ }
36
+ },
37
+ project: () => ({ ok: true }),
38
+ reject: (failure) => rejectChange({
39
+ outcomeKnown: failure.outcomeKnown,
40
+ code: failure.code,
41
+ ...('rule' in failure && failure.rule !== undefined ? { rule: failure.rule } : {}),
42
+ }),
43
+ }));
4
44
  /** Converge the membership Edges of one pair, guarded by what was observed of them. */
5
45
  export const setGroupMembership = defineMutation()((domain) => ({
6
46
  id: 'shell.group.membership.set',
@@ -22,17 +62,9 @@ export const setGroupMembership = defineMutation()((domain) => ({
22
62
  }
23
63
  },
24
64
  project: () => ({ ok: true }),
25
- reject: (failure) => {
26
- if (failure.outcomeKnown &&
27
- failure.code !== 'MUTATION_PRECONDITION_FAILED' &&
28
- failure.code !== 'MUTATION_CONFLICT' &&
29
- failure.code !== 'MUTATION_POST_STATE_INVALID')
30
- throw failure;
31
- return {
32
- ok: false,
33
- code: failure.code,
34
- ...('rule' in failure && failure.rule !== undefined ? { rule: failure.rule } : {}),
35
- outcomeKnown: failure.outcomeKnown,
36
- };
37
- },
65
+ reject: (failure) => rejectChange({
66
+ outcomeKnown: failure.outcomeKnown,
67
+ code: failure.code,
68
+ ...('rule' in failure && failure.rule !== undefined ? { rule: failure.rule } : {}),
69
+ }),
38
70
  }));
@@ -1,8 +1,23 @@
1
1
  import { defineMutation } from '@astrale-os/sdk/mutation';
2
- /** Record a pending invitation of one account, kept by one durable run. */
2
+ /**
3
+ * Record a pending invitation of one account, kept by one durable run, and cancel in the same
4
+ * commit the pending invitations it replaces: an account has at most one.
5
+ */
3
6
  export const createInvitation = defineMutation()((domain) => ({
4
7
  id: 'shell.invitation.create',
5
8
  build(input, mutation) {
9
+ for (const previous of input.replaces ?? []) {
10
+ mutation.expect.node({
11
+ node: { id: previous },
12
+ class: domain.classes.Invitation,
13
+ props: { equals: { status: 'pending' } },
14
+ });
15
+ mutation.updateNode({
16
+ node: { id: previous },
17
+ class: domain.classes.Invitation,
18
+ props: { set: { status: 'cancelled' } },
19
+ });
20
+ }
6
21
  const invitation = mutation.createNode({
7
22
  as: 'invitation',
8
23
  class: domain.classes.Invitation,
@@ -11,6 +26,7 @@ export const createInvitation = defineMutation()((domain) => ({
11
26
  expiresAt: input.expiresAt,
12
27
  execution: input.execution,
13
28
  round: 0,
29
+ email: input.email,
14
30
  },
15
31
  });
16
32
  mutation.createEdge({
@@ -26,7 +42,7 @@ export const createInvitation = defineMutation()((domain) => ({
26
42
  return { invitation };
27
43
  },
28
44
  }));
29
- /** Change an invitation's status or its round, while it is still pending. */
45
+ /** Change an invitation's status, or its round and expiry, while it is still pending. */
30
46
  export const updateInvitation = defineMutation()((domain) => ({
31
47
  id: 'shell.invitation.update',
32
48
  build(input, mutation) {
@@ -42,6 +58,7 @@ export const updateInvitation = defineMutation()((domain) => ({
42
58
  set: {
43
59
  ...(input.status === undefined ? {} : { status: input.status }),
44
60
  ...(input.round === undefined ? {} : { round: input.round }),
61
+ ...(input.expiresAt === undefined ? {} : { expiresAt: input.expiresAt }),
45
62
  },
46
63
  },
47
64
  });
@@ -1,2 +1,2 @@
1
1
  export { readExtensions } from './read-extensions.js';
2
- export { readRoleNodes } from './read-role-nodes.js';
2
+ export { readRoleKeys, readRoleNodes } from './read-role-nodes.js';
@@ -1,3 +1,4 @@
1
+ import { Path } from '@astrale-os/sdk/graph/path';
1
2
  import { defineQuery, Query, queryResult, } from '@astrale-os/sdk/query';
2
3
  const CORE = 'core.';
3
4
  /** Each Core Node of a Kernel Role Class, named by its `of_domain` locator. */
@@ -22,20 +23,54 @@ export const readRoleNodes = defineQuery()((domain) => {
22
23
  },
23
24
  project(pages) {
24
25
  const byKey = new Map();
25
- for (const page of pages) {
26
- for (const item of queryResult.edgeItems(page, 'Shell Roles')) {
27
- if (item.edge.kind !== 'value' || item.source.kind !== 'reference')
28
- continue;
29
- if (item.target.kind !== 'value')
30
- continue;
31
- const key = item.edge.value.props[member];
32
- const owner = item.target.value.props[origin];
33
- if (typeof key !== 'string' || !key.startsWith(CORE) || typeof owner !== 'string')
34
- continue;
35
- byKey.set(`${owner}:${key.slice(CORE.length)}`, item.source.value.id);
36
- }
37
- }
26
+ for (const [id, key] of roleKeys(pages, member, origin))
27
+ byKey.set(key, id);
38
28
  return { byKey, ids: new Set(byKey.values()) };
39
29
  },
40
30
  };
41
31
  });
32
+ /** Each Core Role in pages of `of_domain` Edges: its Node, and `<origin>:<Core key>`. */
33
+ function roleKeys(pages, member, origin) {
34
+ const keys = new Map();
35
+ for (const page of pages) {
36
+ for (const item of queryResult.edgeItems(page, 'Shell Roles')) {
37
+ if (item.edge.kind !== 'value' || item.source.kind !== 'reference')
38
+ continue;
39
+ if (item.target.kind !== 'value')
40
+ continue;
41
+ const key = item.edge.value.props[member];
42
+ const owner = item.target.value.props[origin];
43
+ if (typeof key !== 'string' || !key.startsWith(CORE) || typeof owner !== 'string')
44
+ continue;
45
+ keys.set(item.source.value.id, `${owner}:${key.slice(CORE.length)}`);
46
+ }
47
+ }
48
+ return keys;
49
+ }
50
+ /**
51
+ * The Core Roles among some Identities, by `<origin>:<Core key>`: what one composition holds, read
52
+ * without the catalogue of every installed Role.
53
+ */
54
+ export const readRoleKeys = defineQuery()((domain) => {
55
+ const kernel = domain.dependencies.kernel.classes;
56
+ const member = kernel.of_domain.properties.member.key;
57
+ const origin = kernel.Domain.properties.origin.key;
58
+ return {
59
+ id: 'shell.composition.role-keys',
60
+ page: { size: 256 },
61
+ pagination: { maximumPages: 16 },
62
+ build: ({ identities }) => {
63
+ const locator = Query.from({
64
+ nodes: [Path.id(identities[0]), ...identities.slice(1).map((id) => Path.id(id))],
65
+ })
66
+ .filter({ class: kernel.Role })
67
+ .expand({ via: [kernel.of_domain], direction: 'outgoing' });
68
+ return locator.select({
69
+ kind: 'edges',
70
+ binding: locator.edge,
71
+ projection: { edge: 'value', source: 'reference', target: 'value' },
72
+ });
73
+ },
74
+ project: (pages) => roleKeys(pages, member, origin),
75
+ };
76
+ });
@@ -1,2 +1,3 @@
1
1
  export { readGroupMembership, } from './read-group-membership.js';
2
2
  export { readGroupMembershipEndpoints, } from './read-group-membership-endpoints.js';
3
+ export { readGroupMembers, readGroupMembersEndpoints, } from './read-group-members.js';
@@ -1,2 +1,3 @@
1
1
  export { legacyReadMemberOfGroup } from './read-member-of-group.js';
2
2
  export { legacyReadCoreGroups } from './read-core-groups.js';
3
+ export { legacyReadMembersOfGroup } from './read-members-of-group.js';
@@ -0,0 +1,27 @@
1
+ import { Path } from '@astrale-os/sdk/graph/path';
2
+ import { Query, defineQuery, } from '@astrale-os/sdk/query';
3
+ import { canonical, sources, } from '../read-group-members.js';
4
+ /**
5
+ * @deprecated The legacy `member_of_group` Edges from the members to the group, in one read and
6
+ * kept whole as one guard (L-SHELL-8). Replaced by `readGroupMembers` (`extends_with`). Removal:
7
+ * Shell major (delivery step 5) after 1Pact step 4.
8
+ */
9
+ export const legacyReadMembersOfGroup = defineQuery()((domain) => ({
10
+ id: 'shell.group.members.legacy',
11
+ page: { size: 256 },
12
+ pagination: { maximumPages: 2 },
13
+ build({ members, group }) {
14
+ const walk = Query.from({
15
+ nodes: [Path.id(members[0]), ...members.slice(1).map((id) => Path.id(id))],
16
+ }).expand({ via: [domain.classes.member_of_group], direction: 'outgoing' });
17
+ return walk.filter({ predicate: { kind: 'identity.equal', identity: group } }).select({
18
+ kind: 'edges',
19
+ binding: walk.edge,
20
+ projection: { edge: 'reference', source: 'reference', target: 'reference' },
21
+ });
22
+ },
23
+ project: (pages, _input, { ast, complete }) => ({
24
+ present: sources(pages, 'Shell legacy group members'),
25
+ guard: { query: ast, equals: canonical(complete) },
26
+ }),
27
+ }));
@@ -0,0 +1,73 @@
1
+ import { Path } from '@astrale-os/sdk/graph/path';
2
+ import { Query, defineQuery, queryResult, } from '@astrale-os/sdk/query';
3
+ const anchors = (ids) => [Path.id(ids[0]), ...ids.slice(1).map((id) => Path.id(id))];
4
+ /** Every member and the group, in one read whatever the number of members. */
5
+ export const readGroupMembersEndpoints = defineQuery()(() => ({
6
+ id: 'shell.group.members.endpoints',
7
+ page: { size: 256 },
8
+ pagination: { maximumPages: 2 },
9
+ build: ({ members, group }) => Query.from({ nodes: [...anchors(members), Path.id(group)] }).select({
10
+ kind: 'nodes',
11
+ projection: { kind: 'reference' },
12
+ }),
13
+ project(pages, input, { ast, complete }) {
14
+ const visible = new Set(queryResult.completeNodeIds(pages, 'Shell group members'));
15
+ let groupClass;
16
+ for (const page of pages) {
17
+ if (page.kind !== 'nodes')
18
+ continue;
19
+ for (const item of page.nodes) {
20
+ if (item.kind === 'reference' && item.value.id === input.group)
21
+ groupClass = item.value.class;
22
+ }
23
+ }
24
+ return {
25
+ visible,
26
+ ...(groupClass === undefined ? {} : { groupClass }),
27
+ // The Kernel takes a guard's Nodes in canonical order.
28
+ guard: { query: ast, equals: { ...complete, ids: [...complete.ids].sort(order) } },
29
+ };
30
+ },
31
+ }));
32
+ /** The `extends_with` Edges from the members to the group: their memberships (G1), in one read. */
33
+ export const readGroupMembers = defineQuery()((domain) => ({
34
+ id: 'shell.group.members.read',
35
+ page: { size: 256 },
36
+ pagination: { maximumPages: 2 },
37
+ build({ members, group }) {
38
+ const walk = Query.from({ nodes: anchors(members) }).expand({
39
+ via: [domain.dependencies.kernel.classes.extends_with],
40
+ direction: 'outgoing',
41
+ });
42
+ return walk.filter({ predicate: { kind: 'identity.equal', identity: group } }).select({
43
+ kind: 'edges',
44
+ binding: walk.edge,
45
+ projection: { edge: 'reference', source: 'reference', target: 'reference' },
46
+ });
47
+ },
48
+ project: (pages, _input, { ast, complete }) => ({
49
+ present: sources(pages, 'Shell group members'),
50
+ guard: { query: ast, equals: canonical(complete) },
51
+ }),
52
+ }));
53
+ /** The members an Edge read finds, by source. */
54
+ export function sources(pages, label) {
55
+ const found = new Set();
56
+ for (const page of pages) {
57
+ for (const item of queryResult.edgeItems(page, label)) {
58
+ if (item.source.kind === 'reference')
59
+ found.add(item.source.value.id);
60
+ }
61
+ }
62
+ return found;
63
+ }
64
+ const order = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
65
+ /** A guard's Edges in the Kernel's canonical order: by source, then target, then Class. */
66
+ export function canonical(selection) {
67
+ if (selection.kind !== 'edge')
68
+ return selection;
69
+ const keys = [...selection.keys].sort((left, right) => order(left.source, right.source) ||
70
+ order(left.target, right.target) ||
71
+ order(left.class, right.class));
72
+ return { ...selection, keys };
73
+ }
@@ -1 +1 @@
1
- export { readInvitation } from './read-invitation.js';
1
+ export { readInvitation, readPendingInvitations } from './read-invitation.js';
@@ -1,5 +1,17 @@
1
1
  import { Path } from '@astrale-os/sdk/graph/path';
2
2
  import { defineQuery, Query, queryResult, } from '@astrale-os/sdk/query';
3
+ /** An invitation Node as the methods and the run read it. */
4
+ function toInvitation(node, keys) {
5
+ const props = node.props;
6
+ const email = props[keys.email.key];
7
+ return {
8
+ id: node.id,
9
+ status: String(props[keys.status.key]),
10
+ execution: String(props[keys.execution.key]),
11
+ round: Number(props[keys.round.key] ?? 0),
12
+ ...(typeof email === 'string' ? { email } : {}),
13
+ };
14
+ }
3
15
  /** One invitation, or undefined when there is none the reader reads. */
4
16
  export const readInvitation = defineQuery()((domain) => ({
5
17
  id: 'shell.invitation.read',
@@ -13,13 +25,26 @@ export const readInvitation = defineQuery()((domain) => ({
13
25
  const node = queryResult.completeNodeValues(pages, 'Shell invitation')[0];
14
26
  if (node === undefined)
15
27
  return undefined;
16
- const props = node.props;
17
- const keys = domain.classes.Invitation.properties;
18
- return {
19
- id: node.id,
20
- status: String(props[keys.status.key]),
21
- execution: String(props[keys.execution.key]),
22
- round: Number(props[keys.round.key] ?? 0),
23
- };
28
+ return toInvitation(node, domain.classes.Invitation.properties);
24
29
  },
25
30
  }));
31
+ /** The pending invitations of one account: at most one, unless recorded before that rule. */
32
+ export const readPendingInvitations = defineQuery()((domain) => ({
33
+ id: 'shell.invitation.pending',
34
+ page: { size: 64 },
35
+ pagination: { maximumPages: 1 },
36
+ build: ({ user }) => Query.from({ nodes: [Path.id(user)] })
37
+ .expand({ via: [domain.classes.invitation_user], direction: 'incoming' })
38
+ .filter({ class: domain.classes.Invitation })
39
+ .filter({
40
+ predicate: {
41
+ kind: 'property.equal',
42
+ property: domain.classes.Invitation.properties.status.key,
43
+ value: 'pending',
44
+ },
45
+ })
46
+ .select({ kind: 'nodes', projection: { kind: 'value' } }),
47
+ project: (pages) => queryResult
48
+ .completeNodeValues(pages, 'Shell pending invitations')
49
+ .map((node) => toInvitation(node, domain.classes.Invitation.properties)),
50
+ }));
@@ -1 +1 @@
1
- export { readRoleNode } from './read-role-node.js';
1
+ export { readRoleId, readRoleNode } from './read-role-node.js';
@@ -11,3 +11,11 @@ export const readRoleNode = defineQuery()(() => ({
11
11
  build: ({ role }) => Query.from({ nodes: [role] }).select({ kind: 'nodes', projection: { kind: 'value' } }),
12
12
  project: (pages) => queryResult.completeNodeValues(pages, 'Shell Role').length === 1,
13
13
  }));
14
+ /** One Role's Node, read by its Core target; nothing when it is not installed. */
15
+ export const readRoleId = defineQuery()(() => ({
16
+ id: 'shell.role.id',
17
+ page: { size: 1 },
18
+ pagination: { maximumPages: 1 },
19
+ build: ({ role }) => Query.from({ nodes: [role] }).select({ kind: 'nodes', projection: { kind: 'reference' } }),
20
+ project: (pages) => queryResult.completeNodeIds(pages, 'Shell Role')[0],
21
+ }));