@kindgi/platform 0.1.3 → 0.1.4-rc.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.
- package/README.md +31 -0
- package/dist/in-memory/org-binding.d.ts.map +1 -1
- package/dist/in-memory/org-binding.js +33 -16
- package/dist/in-memory/org-binding.js.map +1 -1
- package/dist/in-memory/project-binding.d.ts.map +1 -1
- package/dist/in-memory/project-binding.js +38 -7
- package/dist/in-memory/project-binding.js.map +1 -1
- package/dist/in-memory/team-binding.d.ts.map +1 -1
- package/dist/in-memory/team-binding.js +22 -6
- package/dist/in-memory/team-binding.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/org-binding.d.ts +56 -12
- package/dist/org-binding.d.ts.map +1 -1
- package/dist/project-binding.d.ts +62 -12
- package/dist/project-binding.d.ts.map +1 -1
- package/dist/team-binding.d.ts +56 -12
- package/dist/team-binding.d.ts.map +1 -1
- package/dist/tenant-hierarchy-binding.d.ts +19 -5
- package/dist/tenant-hierarchy-binding.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/in-memory/org-binding.ts +41 -19
- package/src/in-memory/project-binding.ts +64 -14
- package/src/in-memory/team-binding.ts +36 -11
- package/src/index.ts +13 -1
- package/src/org-binding.ts +60 -12
- package/src/project-binding.ts +72 -12
- package/src/team-binding.ts +66 -12
- package/src/tenant-hierarchy-binding.ts +17 -6
- package/tests/org-binding.conformance.ts +93 -18
- package/tests/project-binding.conformance.ts +186 -37
- package/tests/team-binding.conformance.ts +92 -28
|
@@ -15,16 +15,21 @@
|
|
|
15
15
|
* Both factories are returned together so the two adapters share the
|
|
16
16
|
* same team map (a membership add must fail cleanly for a
|
|
17
17
|
* non-existent team; splitting into two disjoint factories would
|
|
18
|
-
* require an extra cross-binding contract).
|
|
18
|
+
* require an extra cross-binding contract). Team slugs are unique
|
|
19
|
+
* within a tenant.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
import type { Filter, Page, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
22
23
|
|
|
23
24
|
import type {
|
|
24
25
|
TeamBinding,
|
|
26
|
+
TeamCreateOutcome,
|
|
25
27
|
TeamListFilter,
|
|
26
28
|
TeamMembershipAddInput,
|
|
29
|
+
TeamMembershipAddOutcome,
|
|
27
30
|
TeamMembershipBinding,
|
|
31
|
+
TeamMembershipUpdateRoleOutcome,
|
|
32
|
+
TeamUpdateOutcome,
|
|
28
33
|
} from '../team-binding.js';
|
|
29
34
|
import type { Team, TeamMembership, TeamPatch, TeamRole, TeamSpec } from '../types.js';
|
|
30
35
|
|
|
@@ -59,8 +64,17 @@ export function makeInMemoryTeamBinding(): {
|
|
|
59
64
|
return t;
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
/** Is `slug` held by a team in the tenant other than `exceptId`? */
|
|
68
|
+
function slugTaken(tenantId: TenantId, slug: string, exceptId?: TeamId): boolean {
|
|
69
|
+
for (const row of teamRows.values()) {
|
|
70
|
+
if (row.tenantId === tenantId && row.slug === slug && row.id !== exceptId) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
const teams: TeamBinding = {
|
|
63
|
-
async create(tenantId, spec: TeamSpec): Promise<
|
|
76
|
+
async create(tenantId, spec: TeamSpec): Promise<TeamCreateOutcome> {
|
|
77
|
+
if (slugTaken(tenantId, spec.slug)) return { kind: 'slug-conflict', slug: spec.slug };
|
|
64
78
|
const id = nextTeamId();
|
|
65
79
|
const now = nowTimestamp();
|
|
66
80
|
const row: Team = {
|
|
@@ -74,7 +88,7 @@ export function makeInMemoryTeamBinding(): {
|
|
|
74
88
|
...(spec.description !== undefined ? { description: spec.description } : {}),
|
|
75
89
|
};
|
|
76
90
|
teamRows.set(id, row);
|
|
77
|
-
return id;
|
|
91
|
+
return { kind: 'ok', teamId: id };
|
|
78
92
|
},
|
|
79
93
|
|
|
80
94
|
async get(tenantId, teamId): Promise<Team | undefined> {
|
|
@@ -98,10 +112,13 @@ export function makeInMemoryTeamBinding(): {
|
|
|
98
112
|
return paginate(all, filter.limit, filter.cursor);
|
|
99
113
|
},
|
|
100
114
|
|
|
101
|
-
async update(tenantId, teamId, patch: TeamPatch): Promise<
|
|
115
|
+
async update(tenantId, teamId, patch: TeamPatch): Promise<TeamUpdateOutcome> {
|
|
102
116
|
const row = findTeamInTenant(tenantId, teamId);
|
|
103
117
|
if (row === undefined) {
|
|
104
|
-
|
|
118
|
+
return { kind: 'team-not-found' };
|
|
119
|
+
}
|
|
120
|
+
if (patch.slug !== undefined && slugTaken(tenantId, patch.slug, teamId)) {
|
|
121
|
+
return { kind: 'slug-conflict', slug: patch.slug };
|
|
105
122
|
}
|
|
106
123
|
// orgId patch tri-state: undefined = untouched; null = clear;
|
|
107
124
|
// OrgId value = set.
|
|
@@ -121,6 +138,7 @@ export function makeInMemoryTeamBinding(): {
|
|
|
121
138
|
...(nextDescription !== undefined ? { description: nextDescription } : {}),
|
|
122
139
|
};
|
|
123
140
|
teamRows.set(teamId, next);
|
|
141
|
+
return { kind: 'ok' };
|
|
124
142
|
},
|
|
125
143
|
|
|
126
144
|
async delete(tenantId, teamId): Promise<void> {
|
|
@@ -137,15 +155,15 @@ export function makeInMemoryTeamBinding(): {
|
|
|
137
155
|
};
|
|
138
156
|
|
|
139
157
|
const memberships: TeamMembershipBinding = {
|
|
140
|
-
async add(tenantId, input: TeamMembershipAddInput): Promise<
|
|
158
|
+
async add(tenantId, input: TeamMembershipAddInput): Promise<TeamMembershipAddOutcome> {
|
|
141
159
|
const team = findTeamInTenant(tenantId, input.teamId);
|
|
142
160
|
if (team === undefined) {
|
|
143
|
-
|
|
161
|
+
return { kind: 'team-not-found' };
|
|
144
162
|
}
|
|
145
163
|
const key = membershipKey(input.teamId, input.userId);
|
|
146
164
|
if (membershipRows.has(key)) {
|
|
147
165
|
// Idempotent — role mutation goes through updateRole.
|
|
148
|
-
return;
|
|
166
|
+
return { kind: 'ok' };
|
|
149
167
|
}
|
|
150
168
|
const row: TeamMembership = {
|
|
151
169
|
teamId: input.teamId,
|
|
@@ -154,6 +172,7 @@ export function makeInMemoryTeamBinding(): {
|
|
|
154
172
|
joinedAt: nowTimestamp(),
|
|
155
173
|
};
|
|
156
174
|
membershipRows.set(key, row);
|
|
175
|
+
return { kind: 'ok' };
|
|
157
176
|
},
|
|
158
177
|
|
|
159
178
|
async remove(tenantId, teamId, userId): Promise<void> {
|
|
@@ -187,17 +206,23 @@ export function makeInMemoryTeamBinding(): {
|
|
|
187
206
|
return paginate(all, filter.limit, filter.cursor);
|
|
188
207
|
},
|
|
189
208
|
|
|
190
|
-
async updateRole(
|
|
209
|
+
async updateRole(
|
|
210
|
+
tenantId,
|
|
211
|
+
teamId,
|
|
212
|
+
userId,
|
|
213
|
+
role: TeamRole,
|
|
214
|
+
): Promise<TeamMembershipUpdateRoleOutcome> {
|
|
191
215
|
const team = findTeamInTenant(tenantId, teamId);
|
|
192
216
|
if (team === undefined) {
|
|
193
|
-
|
|
217
|
+
return { kind: 'team-not-found' };
|
|
194
218
|
}
|
|
195
219
|
const key = membershipKey(teamId, userId);
|
|
196
220
|
const row = membershipRows.get(key);
|
|
197
221
|
if (row === undefined) {
|
|
198
|
-
|
|
222
|
+
return { kind: 'team-membership-not-found' };
|
|
199
223
|
}
|
|
200
224
|
membershipRows.set(key, { ...row, role });
|
|
225
|
+
return { kind: 'ok' };
|
|
201
226
|
},
|
|
202
227
|
};
|
|
203
228
|
|
package/src/index.ts
CHANGED
|
@@ -15,19 +15,30 @@ export { scopeKey } from './scope.js';
|
|
|
15
15
|
|
|
16
16
|
export type {
|
|
17
17
|
OrgBinding,
|
|
18
|
+
OrgCreateOutcome,
|
|
18
19
|
OrgListFilter,
|
|
20
|
+
OrgDeleteConflict,
|
|
21
|
+
OrgUpdateOutcome,
|
|
19
22
|
} from './org-binding.js';
|
|
20
23
|
export type {
|
|
21
24
|
TeamBinding,
|
|
25
|
+
TeamCreateOutcome,
|
|
22
26
|
TeamListFilter,
|
|
23
27
|
TeamMembershipAddInput,
|
|
28
|
+
TeamMembershipAddOutcome,
|
|
24
29
|
TeamMembershipBinding,
|
|
30
|
+
TeamMembershipUpdateRoleOutcome,
|
|
31
|
+
TeamUpdateOutcome,
|
|
25
32
|
} from './team-binding.js';
|
|
26
33
|
export type {
|
|
27
34
|
ProjectBinding,
|
|
35
|
+
ProjectCreateOutcome,
|
|
28
36
|
ProjectListFilter,
|
|
29
37
|
ProjectMembershipAddInput,
|
|
38
|
+
ProjectMembershipAddOutcome,
|
|
30
39
|
ProjectMembershipBinding,
|
|
40
|
+
ProjectMembershipUpdateRoleOutcome,
|
|
41
|
+
ProjectUpdateOutcome,
|
|
31
42
|
} from './project-binding.js';
|
|
32
43
|
export type {
|
|
33
44
|
TeamProjectGrant,
|
|
@@ -42,7 +53,9 @@ export {
|
|
|
42
53
|
} from './in-memory/index.js';
|
|
43
54
|
|
|
44
55
|
export type {
|
|
56
|
+
AddProjectMemberError,
|
|
45
57
|
AddProjectMemberParams,
|
|
58
|
+
AddTeamMemberError,
|
|
46
59
|
AddTeamMemberParams,
|
|
47
60
|
CreateOrgError,
|
|
48
61
|
CreateOrgParams,
|
|
@@ -50,7 +63,6 @@ export type {
|
|
|
50
63
|
CreateProjectParams,
|
|
51
64
|
CreateTeamError,
|
|
52
65
|
CreateTeamParams,
|
|
53
|
-
MembershipMutationError,
|
|
54
66
|
TenantHierarchyBinding,
|
|
55
67
|
TenantSummary,
|
|
56
68
|
} from './tenant-hierarchy-binding.js';
|
package/src/org-binding.ts
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* `@kindgi/types` (`items` + `nextCursor`).
|
|
13
13
|
* - `list` filters extend the shared `Filter` shape so every list call
|
|
14
14
|
* shares the same pagination fields.
|
|
15
|
+
* - A write the caller can get wrong (a slug already taken, a row that
|
|
16
|
+
* doesn't exist) resolves to an outcome — a union discriminated on
|
|
17
|
+
* `kind`, `'ok'` on success — and never rejects for it. Each
|
|
18
|
+
* non-`ok` `kind` is also the API error code the routes answer with
|
|
19
|
+
* (`slug-conflict` is a 409, `org-not-found` a 404). A rejected
|
|
20
|
+
* promise means the backend failed.
|
|
15
21
|
*
|
|
16
22
|
* This package ships the interface + reference in-memory adapter
|
|
17
23
|
* (in `./in-memory/org-binding.ts`). Any adapter can be checked with
|
|
@@ -43,11 +49,13 @@ export interface OrgListFilter extends Filter {
|
|
|
43
49
|
*/
|
|
44
50
|
export interface OrgBinding {
|
|
45
51
|
/**
|
|
46
|
-
* Create a new `Org` in the given tenant.
|
|
47
|
-
* assigned `OrgId
|
|
48
|
-
*
|
|
52
|
+
* Create a new `Org` in the given tenant. Resolves to `ok` with the
|
|
53
|
+
* freshly-assigned `OrgId`, or to `slug-conflict` when another org in
|
|
54
|
+
* the tenant has `spec.slug`. The binding is responsible for
|
|
55
|
+
* allocating IDs, timestamps, and slug-uniqueness enforcement within
|
|
56
|
+
* the tenant.
|
|
49
57
|
*/
|
|
50
|
-
create(tenantId: TenantId, spec: OrgSpec): Promise<
|
|
58
|
+
create(tenantId: TenantId, spec: OrgSpec): Promise<OrgCreateOutcome>;
|
|
51
59
|
/**
|
|
52
60
|
* Look up an `Org` by id. Returns `undefined` when no row exists in
|
|
53
61
|
* the given tenant, or when the row exists in a different tenant
|
|
@@ -62,15 +70,55 @@ export interface OrgBinding {
|
|
|
62
70
|
list(tenantId: TenantId, filter: OrgListFilter): Promise<Page<Org>>;
|
|
63
71
|
/**
|
|
64
72
|
* Partially update an `Org`. Fields absent from `patch` are left
|
|
65
|
-
* unchanged; fields present are set to the new value.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
73
|
+
* unchanged; fields present are set to the new value. Resolves to
|
|
74
|
+
* `org-not-found` when no org has `orgId` in the tenant, and to
|
|
75
|
+
* `slug-conflict` when `patch.slug` is another org's slug.
|
|
68
76
|
*/
|
|
69
|
-
update(tenantId: TenantId, orgId: OrgId, patch: OrgPatch): Promise<
|
|
77
|
+
update(tenantId: TenantId, orgId: OrgId, patch: OrgPatch): Promise<OrgUpdateOutcome>;
|
|
70
78
|
/**
|
|
71
|
-
* Delete an `Org
|
|
72
|
-
*
|
|
73
|
-
*
|
|
79
|
+
* Delete an `Org`: a tombstone, not an erase. From then on `get`,
|
|
80
|
+
* `list` and `update` treat it as unknown, and its slug is free for a
|
|
81
|
+
* new org; a retention policy on the `org` domain purges the row.
|
|
82
|
+
* What happens to dependents (teams, projects, org-scoped config) is
|
|
83
|
+
* the storage's concern; the in-memory adapter has none. A storage
|
|
84
|
+
* whose projects leave a deleted org (the Kindgi runtime's: they
|
|
85
|
+
* become projects without an org) answers `slug-conflict` instead
|
|
86
|
+
* when one of them has the slug of a project that has no org, and
|
|
87
|
+
* deletes nothing. Nothing (`void`): the org is deleted, or wasn't
|
|
88
|
+
* there (or was already deleted).
|
|
74
89
|
*/
|
|
75
|
-
|
|
90
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: `void`, not `undefined`, so a binding whose `delete` returns `Promise<void>` still conforms.
|
|
91
|
+
delete(tenantId: TenantId, orgId: OrgId): Promise<void | OrgDeleteConflict>;
|
|
76
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Why `OrgBinding.delete` deleted nothing: the org's projects would leave
|
|
96
|
+
* it with `slugs` that projects without an org already have. Rename or
|
|
97
|
+
* move them first.
|
|
98
|
+
*/
|
|
99
|
+
export interface OrgDeleteConflict {
|
|
100
|
+
readonly kind: 'slug-conflict';
|
|
101
|
+
readonly slugs: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What `OrgBinding.create` did. */
|
|
105
|
+
export type OrgCreateOutcome =
|
|
106
|
+
| { readonly kind: 'ok'; readonly orgId: OrgId }
|
|
107
|
+
| {
|
|
108
|
+
/** Another org in the tenant already has this slug. */
|
|
109
|
+
readonly kind: 'slug-conflict';
|
|
110
|
+
readonly slug: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** What `OrgBinding.update` did. */
|
|
114
|
+
export type OrgUpdateOutcome =
|
|
115
|
+
| { readonly kind: 'ok' }
|
|
116
|
+
| {
|
|
117
|
+
/** No org with this id in the tenant. */
|
|
118
|
+
readonly kind: 'org-not-found';
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
/** Another org in the tenant already has the patched slug. */
|
|
122
|
+
readonly kind: 'slug-conflict';
|
|
123
|
+
readonly slug: string;
|
|
124
|
+
};
|
package/src/project-binding.ts
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* `AgentPublishInput.projectId` in `@kindgi/api`).
|
|
21
21
|
*
|
|
22
22
|
* Same convention as `org-binding.ts`: `tenantId` first, `Page<T>`
|
|
23
|
-
* return, `Filter`-extending filter shapes
|
|
23
|
+
* return, `Filter`-extending filter shapes, and writes the caller can
|
|
24
|
+
* get wrong resolve to an outcome discriminated on `kind` instead of
|
|
25
|
+
* rejecting.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
import type { Filter, OrgId, Page, ProjectId, TenantId, UserId } from '@kindgi/types';
|
|
@@ -58,12 +60,14 @@ export interface ProjectMembershipAddInput {
|
|
|
58
60
|
*/
|
|
59
61
|
export interface ProjectBinding {
|
|
60
62
|
/**
|
|
61
|
-
* Create a `Project` within the tenant. `spec.orgId` optional.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
63
|
+
* Create a `Project` within the tenant. `spec.orgId` optional.
|
|
64
|
+
* Resolves to `ok` with the assigned `ProjectId`; to `slug-conflict`
|
|
65
|
+
* when another project in the tenant has `spec.slug`; and, when
|
|
66
|
+
* `spec.isDefault === true` and the tenant already has a Default, to
|
|
67
|
+
* `project-default-already-exists` — the isDefault-uniqueness
|
|
68
|
+
* guardrail is enforced at write time.
|
|
65
69
|
*/
|
|
66
|
-
create(tenantId: TenantId, spec: ProjectSpec): Promise<
|
|
70
|
+
create(tenantId: TenantId, spec: ProjectSpec): Promise<ProjectCreateOutcome>;
|
|
67
71
|
/**
|
|
68
72
|
* Look up a `Project` by id. `undefined` when unknown or in a
|
|
69
73
|
* different tenant.
|
|
@@ -81,9 +85,15 @@ export interface ProjectBinding {
|
|
|
81
85
|
getDefault(tenantId: TenantId): Promise<Project | undefined>;
|
|
82
86
|
/**
|
|
83
87
|
* Partially update a `Project`. `ProjectPatch.orgId` supports
|
|
84
|
-
* `null` to re-assign the project out of any org.
|
|
88
|
+
* `null` to re-assign the project out of any org. Resolves to
|
|
89
|
+
* `project-not-found` when no project has `projectId` in the tenant,
|
|
90
|
+
* and to `slug-conflict` when `patch.slug` is another project's slug.
|
|
85
91
|
*/
|
|
86
|
-
update(
|
|
92
|
+
update(
|
|
93
|
+
tenantId: TenantId,
|
|
94
|
+
projectId: ProjectId,
|
|
95
|
+
patch: ProjectPatch,
|
|
96
|
+
): Promise<ProjectUpdateOutcome>;
|
|
87
97
|
/**
|
|
88
98
|
* Delete a `Project`. What happens to content rows that still
|
|
89
99
|
* reference the project is the storage layer's concern; the
|
|
@@ -104,8 +114,10 @@ export interface ProjectMembershipBinding {
|
|
|
104
114
|
* Add a user directly to a project. Idempotent on
|
|
105
115
|
* `(projectId, userId)` — re-adding the same user does not mutate
|
|
106
116
|
* a stored differing role. Use `updateRole` to change roles.
|
|
117
|
+
* Resolves to `project-not-found` when no project has
|
|
118
|
+
* `input.projectId` in the tenant.
|
|
107
119
|
*/
|
|
108
|
-
add(tenantId: TenantId, input: ProjectMembershipAddInput): Promise<
|
|
120
|
+
add(tenantId: TenantId, input: ProjectMembershipAddInput): Promise<ProjectMembershipAddOutcome>;
|
|
109
121
|
/**
|
|
110
122
|
* Remove the direct project grant. No-op when absent.
|
|
111
123
|
*/
|
|
@@ -122,13 +134,61 @@ export interface ProjectMembershipBinding {
|
|
|
122
134
|
*/
|
|
123
135
|
listForUser(tenantId: TenantId, userId: UserId, filter: Filter): Promise<Page<ProjectMembership>>;
|
|
124
136
|
/**
|
|
125
|
-
* Update an existing direct-membership's role.
|
|
126
|
-
*
|
|
137
|
+
* Update an existing direct-membership's role. Resolves to
|
|
138
|
+
* `project-not-found` when the project isn't in the tenant, and to
|
|
139
|
+
* `project-membership-not-found` when the user has no direct
|
|
140
|
+
* membership.
|
|
127
141
|
*/
|
|
128
142
|
updateRole(
|
|
129
143
|
tenantId: TenantId,
|
|
130
144
|
projectId: ProjectId,
|
|
131
145
|
userId: UserId,
|
|
132
146
|
role: ProjectRole,
|
|
133
|
-
): Promise<
|
|
147
|
+
): Promise<ProjectMembershipUpdateRoleOutcome>;
|
|
134
148
|
}
|
|
149
|
+
|
|
150
|
+
/** What `ProjectBinding.create` did. */
|
|
151
|
+
export type ProjectCreateOutcome =
|
|
152
|
+
| { readonly kind: 'ok'; readonly projectId: ProjectId }
|
|
153
|
+
| {
|
|
154
|
+
/** Another project in the tenant already has this slug. */
|
|
155
|
+
readonly kind: 'slug-conflict';
|
|
156
|
+
readonly slug: string;
|
|
157
|
+
}
|
|
158
|
+
| {
|
|
159
|
+
/** `spec.isDefault` was `true` and the tenant already has a Default project. */
|
|
160
|
+
readonly kind: 'project-default-already-exists';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** What `ProjectBinding.update` did. */
|
|
164
|
+
export type ProjectUpdateOutcome =
|
|
165
|
+
| { readonly kind: 'ok' }
|
|
166
|
+
| {
|
|
167
|
+
/** No project with this id in the tenant. */
|
|
168
|
+
readonly kind: 'project-not-found';
|
|
169
|
+
}
|
|
170
|
+
| {
|
|
171
|
+
/** Another project in the tenant already has the patched slug. */
|
|
172
|
+
readonly kind: 'slug-conflict';
|
|
173
|
+
readonly slug: string;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** What `ProjectMembershipBinding.add` did (`ok` when already a member, too). */
|
|
177
|
+
export type ProjectMembershipAddOutcome =
|
|
178
|
+
| { readonly kind: 'ok' }
|
|
179
|
+
| {
|
|
180
|
+
/** No project with this id in the tenant. */
|
|
181
|
+
readonly kind: 'project-not-found';
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** What `ProjectMembershipBinding.updateRole` did. */
|
|
185
|
+
export type ProjectMembershipUpdateRoleOutcome =
|
|
186
|
+
| { readonly kind: 'ok' }
|
|
187
|
+
| {
|
|
188
|
+
/** No project with this id in the tenant. */
|
|
189
|
+
readonly kind: 'project-not-found';
|
|
190
|
+
}
|
|
191
|
+
| {
|
|
192
|
+
/** The user has no direct membership on the project. */
|
|
193
|
+
readonly kind: 'project-membership-not-found';
|
|
194
|
+
};
|
package/src/team-binding.ts
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* truth.
|
|
11
11
|
*
|
|
12
12
|
* Same convention as `org-binding.ts`: `tenantId` first, `Page<T>`
|
|
13
|
-
* return, `Filter`-extending filter shapes
|
|
13
|
+
* return, `Filter`-extending filter shapes, and writes the caller can
|
|
14
|
+
* get wrong resolve to an outcome discriminated on `kind` instead of
|
|
15
|
+
* rejecting.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import type { Filter, OrgId, Page, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
@@ -46,10 +48,11 @@ export interface TeamMembershipAddInput {
|
|
|
46
48
|
export interface TeamBinding {
|
|
47
49
|
/**
|
|
48
50
|
* Create a `Team` within the tenant. `spec.orgId` optional (a team
|
|
49
|
-
* may be cross-org — nullable FK).
|
|
50
|
-
* `TeamId
|
|
51
|
+
* may be cross-org — nullable FK). Resolves to `ok` with the
|
|
52
|
+
* assigned `TeamId`, or to `slug-conflict` when another team in the
|
|
53
|
+
* tenant has `spec.slug`.
|
|
51
54
|
*/
|
|
52
|
-
create(tenantId: TenantId, spec: TeamSpec): Promise<
|
|
55
|
+
create(tenantId: TenantId, spec: TeamSpec): Promise<TeamCreateOutcome>;
|
|
53
56
|
/**
|
|
54
57
|
* Look up a `Team` by id. `undefined` when unknown or in a different
|
|
55
58
|
* tenant.
|
|
@@ -63,9 +66,11 @@ export interface TeamBinding {
|
|
|
63
66
|
/**
|
|
64
67
|
* Partially update a `Team`. `TeamPatch.orgId` supports `null` to
|
|
65
68
|
* explicitly re-assign the team out of any org (cross-org), and
|
|
66
|
-
* `undefined` to leave the FK untouched.
|
|
69
|
+
* `undefined` to leave the FK untouched. Resolves to `team-not-found`
|
|
70
|
+
* when no team has `teamId` in the tenant, and to `slug-conflict`
|
|
71
|
+
* when `patch.slug` is another team's slug.
|
|
67
72
|
*/
|
|
68
|
-
update(tenantId: TenantId, teamId: TeamId, patch: TeamPatch): Promise<
|
|
73
|
+
update(tenantId: TenantId, teamId: TeamId, patch: TeamPatch): Promise<TeamUpdateOutcome>;
|
|
69
74
|
/**
|
|
70
75
|
* Delete a `Team`. Its membership rows are removed with it (the
|
|
71
76
|
* in-memory adapter deletes them from its map).
|
|
@@ -88,9 +93,10 @@ export interface TeamMembershipBinding {
|
|
|
88
93
|
* silently mutate a differing role).
|
|
89
94
|
*
|
|
90
95
|
* The conformance suite pins this: adding an existing member whose
|
|
91
|
-
* stored role differs is a no-op (not a role overwrite).
|
|
96
|
+
* stored role differs is a no-op (not a role overwrite). Resolves to
|
|
97
|
+
* `team-not-found` when no team has `input.teamId` in the tenant.
|
|
92
98
|
*/
|
|
93
|
-
add(tenantId: TenantId, input: TeamMembershipAddInput): Promise<
|
|
99
|
+
add(tenantId: TenantId, input: TeamMembershipAddInput): Promise<TeamMembershipAddOutcome>;
|
|
94
100
|
/**
|
|
95
101
|
* Remove a user from a team. Missing membership is a no-op (not an
|
|
96
102
|
* error) — matches every peer platform's membership-remove shape.
|
|
@@ -108,9 +114,57 @@ export interface TeamMembershipBinding {
|
|
|
108
114
|
*/
|
|
109
115
|
listForUser(tenantId: TenantId, userId: UserId, filter: Filter): Promise<Page<TeamMembership>>;
|
|
110
116
|
/**
|
|
111
|
-
* Update an existing membership's role.
|
|
112
|
-
*
|
|
113
|
-
* not
|
|
117
|
+
* Update an existing membership's role. Resolves to `team-not-found`
|
|
118
|
+
* when the team isn't in the tenant, and to
|
|
119
|
+
* `team-membership-not-found` when the user isn't a member — role
|
|
120
|
+
* mutation of a non-member is a caller mistake, not a silent add.
|
|
114
121
|
*/
|
|
115
|
-
updateRole(
|
|
122
|
+
updateRole(
|
|
123
|
+
tenantId: TenantId,
|
|
124
|
+
teamId: TeamId,
|
|
125
|
+
userId: UserId,
|
|
126
|
+
role: TeamRole,
|
|
127
|
+
): Promise<TeamMembershipUpdateRoleOutcome>;
|
|
116
128
|
}
|
|
129
|
+
|
|
130
|
+
/** What `TeamBinding.create` did. */
|
|
131
|
+
export type TeamCreateOutcome =
|
|
132
|
+
| { readonly kind: 'ok'; readonly teamId: TeamId }
|
|
133
|
+
| {
|
|
134
|
+
/** Another team in the tenant already has this slug. */
|
|
135
|
+
readonly kind: 'slug-conflict';
|
|
136
|
+
readonly slug: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** What `TeamBinding.update` did. */
|
|
140
|
+
export type TeamUpdateOutcome =
|
|
141
|
+
| { readonly kind: 'ok' }
|
|
142
|
+
| {
|
|
143
|
+
/** No team with this id in the tenant. */
|
|
144
|
+
readonly kind: 'team-not-found';
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
/** Another team in the tenant already has the patched slug. */
|
|
148
|
+
readonly kind: 'slug-conflict';
|
|
149
|
+
readonly slug: string;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** What `TeamMembershipBinding.add` did (`ok` when already a member, too). */
|
|
153
|
+
export type TeamMembershipAddOutcome =
|
|
154
|
+
| { readonly kind: 'ok' }
|
|
155
|
+
| {
|
|
156
|
+
/** No team with this id in the tenant. */
|
|
157
|
+
readonly kind: 'team-not-found';
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** What `TeamMembershipBinding.updateRole` did. */
|
|
161
|
+
export type TeamMembershipUpdateRoleOutcome =
|
|
162
|
+
| { readonly kind: 'ok' }
|
|
163
|
+
| {
|
|
164
|
+
/** No team with this id in the tenant. */
|
|
165
|
+
readonly kind: 'team-not-found';
|
|
166
|
+
}
|
|
167
|
+
| {
|
|
168
|
+
/** The user isn't a member of the team. */
|
|
169
|
+
readonly kind: 'team-membership-not-found';
|
|
170
|
+
};
|
|
@@ -62,10 +62,21 @@ export interface AddProjectMemberParams {
|
|
|
62
62
|
readonly role: ProjectRole;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
export type AddTeamMemberError =
|
|
66
|
+
| {
|
|
67
|
+
/** No team with this id in the tenant. */
|
|
68
|
+
readonly code: 'team-not-found';
|
|
69
|
+
readonly message: string;
|
|
70
|
+
}
|
|
71
|
+
| { readonly code: 'add-failed'; readonly message: string; readonly cause?: unknown };
|
|
72
|
+
|
|
73
|
+
export type AddProjectMemberError =
|
|
74
|
+
| {
|
|
75
|
+
/** No project with this id in the tenant. */
|
|
76
|
+
readonly code: 'project-not-found';
|
|
77
|
+
readonly message: string;
|
|
78
|
+
}
|
|
79
|
+
| { readonly code: 'add-failed'; readonly message: string; readonly cause?: unknown };
|
|
69
80
|
|
|
70
81
|
/**
|
|
71
82
|
* Tenant-summary read shape returned by `getTenant`. Only the fields
|
|
@@ -96,8 +107,8 @@ export interface TenantHierarchyBinding {
|
|
|
96
107
|
createProject(
|
|
97
108
|
params: CreateProjectParams,
|
|
98
109
|
): Promise<Result<{ readonly projectId: ProjectId }, CreateProjectError>>;
|
|
99
|
-
addTeamMember(params: AddTeamMemberParams): Promise<Result<void,
|
|
100
|
-
addProjectMember(params: AddProjectMemberParams): Promise<Result<void,
|
|
110
|
+
addTeamMember(params: AddTeamMemberParams): Promise<Result<void, AddTeamMemberError>>;
|
|
111
|
+
addProjectMember(params: AddProjectMemberParams): Promise<Result<void, AddProjectMemberError>>;
|
|
101
112
|
/**
|
|
102
113
|
* Look up a tenant by id. Returns `null` when no tenant exists.
|
|
103
114
|
* Not itself tenant-scoped: it is how a caller finds out which
|