@kindgi/platform 0.1.2 → 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
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* `updateRole`).
|
|
13
13
|
* - `listForUser` returns memberships across teams within a tenant.
|
|
14
14
|
* - Cross-tenant isolation on every reachable read.
|
|
15
|
+
* - Outcomes: `team-not-found` and `team-membership-not-found` from
|
|
16
|
+
* writes on a missing row, `slug-conflict` from a slug the tenant
|
|
17
|
+
* already has (another tenant may reuse it).
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { describe, expect, it } from 'vitest';
|
|
@@ -19,6 +22,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
19
22
|
import type { OrgId, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
20
23
|
|
|
21
24
|
import type { TeamBinding, TeamMembershipBinding } from '../src/team-binding.js';
|
|
25
|
+
import type { TeamSpec } from '../src/types.js';
|
|
22
26
|
|
|
23
27
|
const T1 = 'tenant-1' as TenantId;
|
|
24
28
|
const T2 = 'tenant-2' as TenantId;
|
|
@@ -26,6 +30,14 @@ const U1 = 'user-1' as UserId;
|
|
|
26
30
|
const U2 = 'user-2' as UserId;
|
|
27
31
|
const ORG_A = 'org-a' as OrgId;
|
|
28
32
|
|
|
33
|
+
/** Create a team the test expects to succeed; its id. */
|
|
34
|
+
async function createTeam(teams: TeamBinding, tenantId: TenantId, spec: TeamSpec): Promise<TeamId> {
|
|
35
|
+
const outcome = await teams.create(tenantId, spec);
|
|
36
|
+
if (outcome.kind !== 'ok')
|
|
37
|
+
throw new Error(`expected the team to be created, got ${outcome.kind}`);
|
|
38
|
+
return outcome.teamId;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
export function runTeamBindingConformance(
|
|
30
42
|
makeBinding: () => {
|
|
31
43
|
readonly teams: TeamBinding;
|
|
@@ -36,7 +48,7 @@ export function runTeamBindingConformance(
|
|
|
36
48
|
describe(`${label} — team CRUD`, () => {
|
|
37
49
|
it('creates + gets a team with orgId', async () => {
|
|
38
50
|
const { teams } = makeBinding();
|
|
39
|
-
const id = await teams
|
|
51
|
+
const id = await createTeam(teams, T1, {
|
|
40
52
|
name: 'Litigation',
|
|
41
53
|
slug: 'lit',
|
|
42
54
|
orgId: ORG_A,
|
|
@@ -49,27 +61,27 @@ export function runTeamBindingConformance(
|
|
|
49
61
|
|
|
50
62
|
it('creates a cross-org team (no orgId)', async () => {
|
|
51
63
|
const { teams } = makeBinding();
|
|
52
|
-
const id = await teams
|
|
64
|
+
const id = await createTeam(teams, T1, { name: 'Cross', slug: 'cross' });
|
|
53
65
|
const got = await teams.get(T1, id);
|
|
54
66
|
expect(got?.orgId).toBeUndefined();
|
|
55
67
|
});
|
|
56
68
|
|
|
57
69
|
it('updates a team; orgId=null clears the FK', async () => {
|
|
58
70
|
const { teams } = makeBinding();
|
|
59
|
-
const id = await teams
|
|
71
|
+
const id = await createTeam(teams, T1, {
|
|
60
72
|
name: 'Litigation',
|
|
61
73
|
slug: 'lit',
|
|
62
74
|
orgId: ORG_A,
|
|
63
75
|
});
|
|
64
|
-
await teams.update(T1, id, { orgId: null });
|
|
76
|
+
expect(await teams.update(T1, id, { orgId: null })).toEqual({ kind: 'ok' });
|
|
65
77
|
const got = await teams.get(T1, id);
|
|
66
78
|
expect(got?.orgId).toBeUndefined();
|
|
67
79
|
});
|
|
68
80
|
|
|
69
81
|
it('lists teams filtered by orgId', async () => {
|
|
70
82
|
const { teams } = makeBinding();
|
|
71
|
-
await teams
|
|
72
|
-
await teams
|
|
83
|
+
await createTeam(teams, T1, { name: 'A', slug: 'a', orgId: ORG_A });
|
|
84
|
+
await createTeam(teams, T1, { name: 'B', slug: 'b' });
|
|
73
85
|
const inOrg = await teams.list(T1, { orgId: ORG_A });
|
|
74
86
|
expect(inOrg.items).toHaveLength(1);
|
|
75
87
|
expect(inOrg.items[0]?.name).toBe('A');
|
|
@@ -77,7 +89,7 @@ export function runTeamBindingConformance(
|
|
|
77
89
|
|
|
78
90
|
it('deletes a team + cascades memberships', async () => {
|
|
79
91
|
const { teams, memberships } = makeBinding();
|
|
80
|
-
const id = await teams
|
|
92
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
81
93
|
await memberships.add(T1, { teamId: id, userId: U1, role: 'member' });
|
|
82
94
|
await teams.delete(T1, id);
|
|
83
95
|
const page = await memberships.listForUser(T1, U1, {});
|
|
@@ -88,29 +100,37 @@ export function runTeamBindingConformance(
|
|
|
88
100
|
describe(`${label} — cross-tenant isolation`, () => {
|
|
89
101
|
it('team get across tenants returns undefined', async () => {
|
|
90
102
|
const { teams } = makeBinding();
|
|
91
|
-
const id = await teams
|
|
103
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
92
104
|
expect(await teams.get(T2, id)).toBeUndefined();
|
|
93
105
|
});
|
|
94
106
|
|
|
95
107
|
it('team list across tenants does not leak', async () => {
|
|
96
108
|
const { teams } = makeBinding();
|
|
97
|
-
await teams
|
|
109
|
+
await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
98
110
|
const page = await teams.list(T2, {});
|
|
99
111
|
expect(page.items).toEqual([]);
|
|
100
112
|
});
|
|
101
113
|
|
|
102
|
-
it('adding a membership under wrong tenant
|
|
114
|
+
it('adding a membership under wrong tenant → team-not-found', async () => {
|
|
103
115
|
const { teams, memberships } = makeBinding();
|
|
104
|
-
const id = await teams
|
|
105
|
-
await
|
|
106
|
-
|
|
107
|
-
)
|
|
116
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
117
|
+
expect(await memberships.add(T2, { teamId: id, userId: U1, role: 'member' })).toEqual({
|
|
118
|
+
kind: 'team-not-found',
|
|
119
|
+
});
|
|
120
|
+
expect((await memberships.list(T1, id, {})).items).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('update under wrong tenant → team-not-found', async () => {
|
|
124
|
+
const { teams } = makeBinding();
|
|
125
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
126
|
+
expect(await teams.update(T2, id, { name: 'Y' })).toEqual({ kind: 'team-not-found' });
|
|
127
|
+
expect((await teams.get(T1, id))?.name).toBe('X');
|
|
108
128
|
});
|
|
109
129
|
|
|
110
130
|
it('listForUser across tenants only returns tenant-scoped memberships', async () => {
|
|
111
131
|
const { teams, memberships } = makeBinding();
|
|
112
|
-
const t1Team = await teams
|
|
113
|
-
const t2Team = await teams
|
|
132
|
+
const t1Team = await createTeam(teams, T1, { name: 'A', slug: 'a' });
|
|
133
|
+
const t2Team = await createTeam(teams, T2, { name: 'B', slug: 'b' });
|
|
114
134
|
await memberships.add(T1, { teamId: t1Team, userId: U1, role: 'member' });
|
|
115
135
|
await memberships.add(T2, { teamId: t2Team, userId: U1, role: 'member' });
|
|
116
136
|
const t1Page = await memberships.listForUser(T1, U1, {});
|
|
@@ -125,10 +145,14 @@ export function runTeamBindingConformance(
|
|
|
125
145
|
describe(`${label} — membership idempotency`, () => {
|
|
126
146
|
it('add on existing membership is a no-op (does NOT overwrite role)', async () => {
|
|
127
147
|
const { teams, memberships } = makeBinding();
|
|
128
|
-
const id = await teams
|
|
129
|
-
await memberships.add(T1, { teamId: id, userId: U1, role: 'admin' })
|
|
148
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
149
|
+
expect(await memberships.add(T1, { teamId: id, userId: U1, role: 'admin' })).toEqual({
|
|
150
|
+
kind: 'ok',
|
|
151
|
+
});
|
|
130
152
|
// Re-add with a different role — should be a no-op, not a mutation.
|
|
131
|
-
await memberships.add(T1, { teamId: id, userId: U1, role: 'member' })
|
|
153
|
+
expect(await memberships.add(T1, { teamId: id, userId: U1, role: 'member' })).toEqual({
|
|
154
|
+
kind: 'ok',
|
|
155
|
+
});
|
|
132
156
|
const page = await memberships.list(T1, id, {});
|
|
133
157
|
expect(page.items).toHaveLength(1);
|
|
134
158
|
expect(page.items[0]?.role).toBe('admin');
|
|
@@ -136,21 +160,32 @@ export function runTeamBindingConformance(
|
|
|
136
160
|
|
|
137
161
|
it('remove on missing membership is a no-op', async () => {
|
|
138
162
|
const { teams, memberships } = makeBinding();
|
|
139
|
-
const id = await teams
|
|
163
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
140
164
|
await expect(memberships.remove(T1, id, U1)).resolves.toBeUndefined();
|
|
141
165
|
});
|
|
142
166
|
|
|
143
|
-
it('updateRole on missing membership
|
|
167
|
+
it('updateRole on missing membership → team-membership-not-found', async () => {
|
|
144
168
|
const { teams, memberships } = makeBinding();
|
|
145
|
-
const id = await teams
|
|
146
|
-
await
|
|
169
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
170
|
+
expect(await memberships.updateRole(T1, id, U1, 'admin')).toEqual({
|
|
171
|
+
kind: 'team-membership-not-found',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('updateRole on a team in another tenant → team-not-found', async () => {
|
|
176
|
+
const { teams, memberships } = makeBinding();
|
|
177
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
178
|
+
await memberships.add(T1, { teamId: id, userId: U1, role: 'member' });
|
|
179
|
+
expect(await memberships.updateRole(T2, id, U1, 'admin')).toEqual({
|
|
180
|
+
kind: 'team-not-found',
|
|
181
|
+
});
|
|
147
182
|
});
|
|
148
183
|
|
|
149
184
|
it('updateRole on existing membership mutates the role', async () => {
|
|
150
185
|
const { teams, memberships } = makeBinding();
|
|
151
|
-
const id = await teams
|
|
186
|
+
const id = await createTeam(teams, T1, { name: 'X', slug: 'x' });
|
|
152
187
|
await memberships.add(T1, { teamId: id, userId: U1, role: 'member' });
|
|
153
|
-
await memberships.updateRole(T1, id, U1, 'admin');
|
|
188
|
+
expect(await memberships.updateRole(T1, id, U1, 'admin')).toEqual({ kind: 'ok' });
|
|
154
189
|
const page = await memberships.list(T1, id, {});
|
|
155
190
|
expect(page.items[0]?.role).toBe('admin');
|
|
156
191
|
});
|
|
@@ -159,9 +194,9 @@ export function runTeamBindingConformance(
|
|
|
159
194
|
describe(`${label} — listForUser across teams`, () => {
|
|
160
195
|
it('returns every team the user belongs to in the tenant', async () => {
|
|
161
196
|
const { teams, memberships } = makeBinding();
|
|
162
|
-
const a = await teams
|
|
163
|
-
const b = await teams
|
|
164
|
-
const c = await teams
|
|
197
|
+
const a = await createTeam(teams, T1, { name: 'A', slug: 'a' });
|
|
198
|
+
const b = await createTeam(teams, T1, { name: 'B', slug: 'b' });
|
|
199
|
+
const c = await createTeam(teams, T1, { name: 'C', slug: 'c' });
|
|
165
200
|
await memberships.add(T1, { teamId: a, userId: U1, role: 'member' });
|
|
166
201
|
await memberships.add(T1, { teamId: b, userId: U1, role: 'admin' });
|
|
167
202
|
await memberships.add(T1, { teamId: c, userId: U2, role: 'member' });
|
|
@@ -172,4 +207,33 @@ export function runTeamBindingConformance(
|
|
|
172
207
|
expect(teamIds.has(b)).toBe(true);
|
|
173
208
|
});
|
|
174
209
|
});
|
|
210
|
+
|
|
211
|
+
describe(`${label} — slug uniqueness`, () => {
|
|
212
|
+
it('create with a slug the tenant already has → slug-conflict', async () => {
|
|
213
|
+
const { teams } = makeBinding();
|
|
214
|
+
await createTeam(teams, T1, { name: 'Litigation', slug: 'lit' });
|
|
215
|
+
expect(await teams.create(T1, { name: 'Litigation 2', slug: 'lit' })).toEqual({
|
|
216
|
+
kind: 'slug-conflict',
|
|
217
|
+
slug: 'lit',
|
|
218
|
+
});
|
|
219
|
+
expect((await teams.list(T1, {})).items).toHaveLength(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('another tenant may use the same slug', async () => {
|
|
223
|
+
const { teams } = makeBinding();
|
|
224
|
+
await createTeam(teams, T1, { name: 'Litigation', slug: 'lit' });
|
|
225
|
+
expect((await teams.create(T2, { name: 'Litigation', slug: 'lit' })).kind).toBe('ok');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("update to another team's slug → slug-conflict; the team is unchanged", async () => {
|
|
229
|
+
const { teams } = makeBinding();
|
|
230
|
+
await createTeam(teams, T1, { name: 'A', slug: 'a' });
|
|
231
|
+
const id = await createTeam(teams, T1, { name: 'B', slug: 'b' });
|
|
232
|
+
expect(await teams.update(T1, id, { slug: 'a' })).toEqual({
|
|
233
|
+
kind: 'slug-conflict',
|
|
234
|
+
slug: 'a',
|
|
235
|
+
});
|
|
236
|
+
expect((await teams.get(T1, id))?.slug).toBe('b');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
175
239
|
}
|