@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
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* `AgentPublishInput.projectId` in `@kindgi/api`).
|
|
18
18
|
*
|
|
19
19
|
* Same convention as `org-binding.ts`: `tenantId` first, `Page<T>`
|
|
20
|
-
* return, `Filter`-extending filter shapes
|
|
20
|
+
* return, `Filter`-extending filter shapes, and writes the caller can
|
|
21
|
+
* get wrong resolve to an outcome discriminated on `kind` instead of
|
|
22
|
+
* rejecting.
|
|
21
23
|
*/
|
|
22
24
|
import type { Filter, OrgId, Page, ProjectId, TenantId, UserId } from '@kindgi/types';
|
|
23
25
|
import type { Project, ProjectMembership, ProjectPatch, ProjectRole, ProjectSpec } from './types.js';
|
|
@@ -44,12 +46,14 @@ export interface ProjectMembershipAddInput {
|
|
|
44
46
|
*/
|
|
45
47
|
export interface ProjectBinding {
|
|
46
48
|
/**
|
|
47
|
-
* Create a `Project` within the tenant. `spec.orgId` optional.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Create a `Project` within the tenant. `spec.orgId` optional.
|
|
50
|
+
* Resolves to `ok` with the assigned `ProjectId`; to `slug-conflict`
|
|
51
|
+
* when another project in the tenant has `spec.slug`; and, when
|
|
52
|
+
* `spec.isDefault === true` and the tenant already has a Default, to
|
|
53
|
+
* `project-default-already-exists` — the isDefault-uniqueness
|
|
54
|
+
* guardrail is enforced at write time.
|
|
51
55
|
*/
|
|
52
|
-
create(tenantId: TenantId, spec: ProjectSpec): Promise<
|
|
56
|
+
create(tenantId: TenantId, spec: ProjectSpec): Promise<ProjectCreateOutcome>;
|
|
53
57
|
/**
|
|
54
58
|
* Look up a `Project` by id. `undefined` when unknown or in a
|
|
55
59
|
* different tenant.
|
|
@@ -67,9 +71,11 @@ export interface ProjectBinding {
|
|
|
67
71
|
getDefault(tenantId: TenantId): Promise<Project | undefined>;
|
|
68
72
|
/**
|
|
69
73
|
* Partially update a `Project`. `ProjectPatch.orgId` supports
|
|
70
|
-
* `null` to re-assign the project out of any org.
|
|
74
|
+
* `null` to re-assign the project out of any org. Resolves to
|
|
75
|
+
* `project-not-found` when no project has `projectId` in the tenant,
|
|
76
|
+
* and to `slug-conflict` when `patch.slug` is another project's slug.
|
|
71
77
|
*/
|
|
72
|
-
update(tenantId: TenantId, projectId: ProjectId, patch: ProjectPatch): Promise<
|
|
78
|
+
update(tenantId: TenantId, projectId: ProjectId, patch: ProjectPatch): Promise<ProjectUpdateOutcome>;
|
|
73
79
|
/**
|
|
74
80
|
* Delete a `Project`. What happens to content rows that still
|
|
75
81
|
* reference the project is the storage layer's concern; the
|
|
@@ -89,8 +95,10 @@ export interface ProjectMembershipBinding {
|
|
|
89
95
|
* Add a user directly to a project. Idempotent on
|
|
90
96
|
* `(projectId, userId)` — re-adding the same user does not mutate
|
|
91
97
|
* a stored differing role. Use `updateRole` to change roles.
|
|
98
|
+
* Resolves to `project-not-found` when no project has
|
|
99
|
+
* `input.projectId` in the tenant.
|
|
92
100
|
*/
|
|
93
|
-
add(tenantId: TenantId, input: ProjectMembershipAddInput): Promise<
|
|
101
|
+
add(tenantId: TenantId, input: ProjectMembershipAddInput): Promise<ProjectMembershipAddOutcome>;
|
|
94
102
|
/**
|
|
95
103
|
* Remove the direct project grant. No-op when absent.
|
|
96
104
|
*/
|
|
@@ -107,9 +115,51 @@ export interface ProjectMembershipBinding {
|
|
|
107
115
|
*/
|
|
108
116
|
listForUser(tenantId: TenantId, userId: UserId, filter: Filter): Promise<Page<ProjectMembership>>;
|
|
109
117
|
/**
|
|
110
|
-
* Update an existing direct-membership's role.
|
|
111
|
-
*
|
|
118
|
+
* Update an existing direct-membership's role. Resolves to
|
|
119
|
+
* `project-not-found` when the project isn't in the tenant, and to
|
|
120
|
+
* `project-membership-not-found` when the user has no direct
|
|
121
|
+
* membership.
|
|
112
122
|
*/
|
|
113
|
-
updateRole(tenantId: TenantId, projectId: ProjectId, userId: UserId, role: ProjectRole): Promise<
|
|
123
|
+
updateRole(tenantId: TenantId, projectId: ProjectId, userId: UserId, role: ProjectRole): Promise<ProjectMembershipUpdateRoleOutcome>;
|
|
114
124
|
}
|
|
125
|
+
/** What `ProjectBinding.create` did. */
|
|
126
|
+
export type ProjectCreateOutcome = {
|
|
127
|
+
readonly kind: 'ok';
|
|
128
|
+
readonly projectId: ProjectId;
|
|
129
|
+
} | {
|
|
130
|
+
/** Another project in the tenant already has this slug. */
|
|
131
|
+
readonly kind: 'slug-conflict';
|
|
132
|
+
readonly slug: string;
|
|
133
|
+
} | {
|
|
134
|
+
/** `spec.isDefault` was `true` and the tenant already has a Default project. */
|
|
135
|
+
readonly kind: 'project-default-already-exists';
|
|
136
|
+
};
|
|
137
|
+
/** What `ProjectBinding.update` did. */
|
|
138
|
+
export type ProjectUpdateOutcome = {
|
|
139
|
+
readonly kind: 'ok';
|
|
140
|
+
} | {
|
|
141
|
+
/** No project with this id in the tenant. */
|
|
142
|
+
readonly kind: 'project-not-found';
|
|
143
|
+
} | {
|
|
144
|
+
/** Another project in the tenant already has the patched slug. */
|
|
145
|
+
readonly kind: 'slug-conflict';
|
|
146
|
+
readonly slug: string;
|
|
147
|
+
};
|
|
148
|
+
/** What `ProjectMembershipBinding.add` did (`ok` when already a member, too). */
|
|
149
|
+
export type ProjectMembershipAddOutcome = {
|
|
150
|
+
readonly kind: 'ok';
|
|
151
|
+
} | {
|
|
152
|
+
/** No project with this id in the tenant. */
|
|
153
|
+
readonly kind: 'project-not-found';
|
|
154
|
+
};
|
|
155
|
+
/** What `ProjectMembershipBinding.updateRole` did. */
|
|
156
|
+
export type ProjectMembershipUpdateRoleOutcome = {
|
|
157
|
+
readonly kind: 'ok';
|
|
158
|
+
} | {
|
|
159
|
+
/** No project with this id in the tenant. */
|
|
160
|
+
readonly kind: 'project-not-found';
|
|
161
|
+
} | {
|
|
162
|
+
/** The user has no direct membership on the project. */
|
|
163
|
+
readonly kind: 'project-membership-not-found';
|
|
164
|
+
};
|
|
115
165
|
//# sourceMappingURL=project-binding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-binding.d.ts","sourceRoot":"","sources":["../src/project-binding.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"project-binding.d.ts","sourceRoot":"","sources":["../src/project-binding.ts"],"names":[],"mappings":"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"}
|
package/dist/team-binding.d.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* truth.
|
|
8
8
|
*
|
|
9
9
|
* Same convention as `org-binding.ts`: `tenantId` first, `Page<T>`
|
|
10
|
-
* return, `Filter`-extending filter shapes
|
|
10
|
+
* return, `Filter`-extending filter shapes, and writes the caller can
|
|
11
|
+
* get wrong resolve to an outcome discriminated on `kind` instead of
|
|
12
|
+
* rejecting.
|
|
11
13
|
*/
|
|
12
14
|
import type { Filter, OrgId, Page, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
13
15
|
import type { Team, TeamMembership, TeamPatch, TeamRole, TeamSpec } from './types.js';
|
|
@@ -38,10 +40,11 @@ export interface TeamMembershipAddInput {
|
|
|
38
40
|
export interface TeamBinding {
|
|
39
41
|
/**
|
|
40
42
|
* Create a `Team` within the tenant. `spec.orgId` optional (a team
|
|
41
|
-
* may be cross-org — nullable FK).
|
|
42
|
-
* `TeamId
|
|
43
|
+
* may be cross-org — nullable FK). Resolves to `ok` with the
|
|
44
|
+
* assigned `TeamId`, or to `slug-conflict` when another team in the
|
|
45
|
+
* tenant has `spec.slug`.
|
|
43
46
|
*/
|
|
44
|
-
create(tenantId: TenantId, spec: TeamSpec): Promise<
|
|
47
|
+
create(tenantId: TenantId, spec: TeamSpec): Promise<TeamCreateOutcome>;
|
|
45
48
|
/**
|
|
46
49
|
* Look up a `Team` by id. `undefined` when unknown or in a different
|
|
47
50
|
* tenant.
|
|
@@ -55,9 +58,11 @@ export interface TeamBinding {
|
|
|
55
58
|
/**
|
|
56
59
|
* Partially update a `Team`. `TeamPatch.orgId` supports `null` to
|
|
57
60
|
* explicitly re-assign the team out of any org (cross-org), and
|
|
58
|
-
* `undefined` to leave the FK untouched.
|
|
61
|
+
* `undefined` to leave the FK untouched. Resolves to `team-not-found`
|
|
62
|
+
* when no team has `teamId` in the tenant, and to `slug-conflict`
|
|
63
|
+
* when `patch.slug` is another team's slug.
|
|
59
64
|
*/
|
|
60
|
-
update(tenantId: TenantId, teamId: TeamId, patch: TeamPatch): Promise<
|
|
65
|
+
update(tenantId: TenantId, teamId: TeamId, patch: TeamPatch): Promise<TeamUpdateOutcome>;
|
|
61
66
|
/**
|
|
62
67
|
* Delete a `Team`. Its membership rows are removed with it (the
|
|
63
68
|
* in-memory adapter deletes them from its map).
|
|
@@ -79,9 +84,10 @@ export interface TeamMembershipBinding {
|
|
|
79
84
|
* silently mutate a differing role).
|
|
80
85
|
*
|
|
81
86
|
* The conformance suite pins this: adding an existing member whose
|
|
82
|
-
* stored role differs is a no-op (not a role overwrite).
|
|
87
|
+
* stored role differs is a no-op (not a role overwrite). Resolves to
|
|
88
|
+
* `team-not-found` when no team has `input.teamId` in the tenant.
|
|
83
89
|
*/
|
|
84
|
-
add(tenantId: TenantId, input: TeamMembershipAddInput): Promise<
|
|
90
|
+
add(tenantId: TenantId, input: TeamMembershipAddInput): Promise<TeamMembershipAddOutcome>;
|
|
85
91
|
/**
|
|
86
92
|
* Remove a user from a team. Missing membership is a no-op (not an
|
|
87
93
|
* error) — matches every peer platform's membership-remove shape.
|
|
@@ -99,10 +105,48 @@ export interface TeamMembershipBinding {
|
|
|
99
105
|
*/
|
|
100
106
|
listForUser(tenantId: TenantId, userId: UserId, filter: Filter): Promise<Page<TeamMembership>>;
|
|
101
107
|
/**
|
|
102
|
-
* Update an existing membership's role.
|
|
103
|
-
*
|
|
104
|
-
* not
|
|
108
|
+
* Update an existing membership's role. Resolves to `team-not-found`
|
|
109
|
+
* when the team isn't in the tenant, and to
|
|
110
|
+
* `team-membership-not-found` when the user isn't a member — role
|
|
111
|
+
* mutation of a non-member is a caller mistake, not a silent add.
|
|
105
112
|
*/
|
|
106
|
-
updateRole(tenantId: TenantId, teamId: TeamId, userId: UserId, role: TeamRole): Promise<
|
|
113
|
+
updateRole(tenantId: TenantId, teamId: TeamId, userId: UserId, role: TeamRole): Promise<TeamMembershipUpdateRoleOutcome>;
|
|
107
114
|
}
|
|
115
|
+
/** What `TeamBinding.create` did. */
|
|
116
|
+
export type TeamCreateOutcome = {
|
|
117
|
+
readonly kind: 'ok';
|
|
118
|
+
readonly teamId: TeamId;
|
|
119
|
+
} | {
|
|
120
|
+
/** Another team in the tenant already has this slug. */
|
|
121
|
+
readonly kind: 'slug-conflict';
|
|
122
|
+
readonly slug: string;
|
|
123
|
+
};
|
|
124
|
+
/** What `TeamBinding.update` did. */
|
|
125
|
+
export type TeamUpdateOutcome = {
|
|
126
|
+
readonly kind: 'ok';
|
|
127
|
+
} | {
|
|
128
|
+
/** No team with this id in the tenant. */
|
|
129
|
+
readonly kind: 'team-not-found';
|
|
130
|
+
} | {
|
|
131
|
+
/** Another team in the tenant already has the patched slug. */
|
|
132
|
+
readonly kind: 'slug-conflict';
|
|
133
|
+
readonly slug: string;
|
|
134
|
+
};
|
|
135
|
+
/** What `TeamMembershipBinding.add` did (`ok` when already a member, too). */
|
|
136
|
+
export type TeamMembershipAddOutcome = {
|
|
137
|
+
readonly kind: 'ok';
|
|
138
|
+
} | {
|
|
139
|
+
/** No team with this id in the tenant. */
|
|
140
|
+
readonly kind: 'team-not-found';
|
|
141
|
+
};
|
|
142
|
+
/** What `TeamMembershipBinding.updateRole` did. */
|
|
143
|
+
export type TeamMembershipUpdateRoleOutcome = {
|
|
144
|
+
readonly kind: 'ok';
|
|
145
|
+
} | {
|
|
146
|
+
/** No team with this id in the tenant. */
|
|
147
|
+
readonly kind: 'team-not-found';
|
|
148
|
+
} | {
|
|
149
|
+
/** The user isn't a member of the team. */
|
|
150
|
+
readonly kind: 'team-membership-not-found';
|
|
151
|
+
};
|
|
108
152
|
//# sourceMappingURL=team-binding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-binding.d.ts","sourceRoot":"","sources":["../src/team-binding.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"team-binding.d.ts","sourceRoot":"","sources":["../src/team-binding.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEnF,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,MAAM;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACvE;;;OAGG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAAC;IACnE;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACtE;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzF;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC1F;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;IACxF;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;IAC/F;;;;;OAKG;IACH,UAAU,CACR,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAC7C;AAED,qCAAqC;AACrC,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChD;IACE,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN,qCAAqC;AACrC,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACvB;IACE,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC,GACD;IACE,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN,8EAA8E;AAC9E,MAAM,MAAM,wBAAwB,GAChC;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACvB;IACE,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC,CAAC;AAEN,mDAAmD;AACnD,MAAM,MAAM,+BAA+B,GACvC;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACvB;IACE,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC,GACD;IACE,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;CAC5C,CAAC"}
|
|
@@ -58,10 +58,24 @@ export interface AddProjectMemberParams {
|
|
|
58
58
|
readonly userId: UserId;
|
|
59
59
|
readonly role: ProjectRole;
|
|
60
60
|
}
|
|
61
|
-
export
|
|
62
|
-
|
|
61
|
+
export type AddTeamMemberError = {
|
|
62
|
+
/** No team with this id in the tenant. */
|
|
63
|
+
readonly code: 'team-not-found';
|
|
63
64
|
readonly message: string;
|
|
64
|
-
}
|
|
65
|
+
} | {
|
|
66
|
+
readonly code: 'add-failed';
|
|
67
|
+
readonly message: string;
|
|
68
|
+
readonly cause?: unknown;
|
|
69
|
+
};
|
|
70
|
+
export type AddProjectMemberError = {
|
|
71
|
+
/** No project with this id in the tenant. */
|
|
72
|
+
readonly code: 'project-not-found';
|
|
73
|
+
readonly message: string;
|
|
74
|
+
} | {
|
|
75
|
+
readonly code: 'add-failed';
|
|
76
|
+
readonly message: string;
|
|
77
|
+
readonly cause?: unknown;
|
|
78
|
+
};
|
|
65
79
|
/**
|
|
66
80
|
* Tenant-summary read shape returned by `getTenant`. Only the fields
|
|
67
81
|
* the `/v1/tenant` GET route surfaces — id / name / slug / createdAt /
|
|
@@ -92,8 +106,8 @@ export interface TenantHierarchyBinding {
|
|
|
92
106
|
createProject(params: CreateProjectParams): Promise<Result<{
|
|
93
107
|
readonly projectId: ProjectId;
|
|
94
108
|
}, CreateProjectError>>;
|
|
95
|
-
addTeamMember(params: AddTeamMemberParams): Promise<Result<void,
|
|
96
|
-
addProjectMember(params: AddProjectMemberParams): Promise<Result<void,
|
|
109
|
+
addTeamMember(params: AddTeamMemberParams): Promise<Result<void, AddTeamMemberError>>;
|
|
110
|
+
addProjectMember(params: AddProjectMemberParams): Promise<Result<void, AddProjectMemberError>>;
|
|
97
111
|
/**
|
|
98
112
|
* Look up a tenant by id. Returns `null` when no tenant exists.
|
|
99
113
|
* Not itself tenant-scoped: it is how a caller finds out which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-hierarchy-binding.d.ts","sourceRoot":"","sources":["../src/tenant-hierarchy-binding.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxF,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACtF;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACtF;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC5B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACtF;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACzF;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC5B;AAED,MAAM,
|
|
1
|
+
{"version":3,"file":"tenant-hierarchy-binding.d.ts","sourceRoot":"","sources":["../src/tenant-hierarchy-binding.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/platform",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4-rc.0",
|
|
4
4
|
"description": "Multi-tenant hierarchy primitives — the Tenant → Org → Team → Project + User type surface, the `Scope` discriminated access-boundary, binding interfaces for orgs, teams, projects, memberships, team-project grants and the tenant hierarchy, and reference in-memory adapters.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@kindgi/types": "0.1.
|
|
33
|
+
"@kindgi/types": "0.1.4-rc.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.10.5",
|
|
@@ -10,12 +10,19 @@
|
|
|
10
10
|
* filters by `tenantId` at the row level — a lookup from tenant A
|
|
11
11
|
* against an orgId that exists in tenant B returns `undefined` (not a
|
|
12
12
|
* hit). Cross-tenant leakage is a hard guardrail, asserted by the
|
|
13
|
-
* conformance suite.
|
|
13
|
+
* conformance suite. Slugs are unique within a tenant: `create` and
|
|
14
|
+
* `update` resolve to `slug-conflict` for a slug another org holds.
|
|
15
|
+
* `delete` tombstones: a deleted org reads as unknown and frees its slug.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import type { OrgId, Page, TenantId, Timestamp } from '@kindgi/types';
|
|
17
19
|
|
|
18
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
OrgBinding,
|
|
22
|
+
OrgCreateOutcome,
|
|
23
|
+
OrgListFilter,
|
|
24
|
+
OrgUpdateOutcome,
|
|
25
|
+
} from '../org-binding.js';
|
|
19
26
|
import type { Org, OrgPatch, OrgSpec } from '../types.js';
|
|
20
27
|
|
|
21
28
|
import { nowTimestamp, paginate } from './util.js';
|
|
@@ -31,9 +38,28 @@ function nextOrgId(): OrgId {
|
|
|
31
38
|
*/
|
|
32
39
|
export function makeInMemoryOrgBinding(): OrgBinding {
|
|
33
40
|
const rows = new Map<OrgId, Org>();
|
|
41
|
+
/** Tombstoned orgs: kept, and read as unknown. */
|
|
42
|
+
const deleted = new Set<OrgId>();
|
|
43
|
+
|
|
44
|
+
/** The tenant's live org with that id. */
|
|
45
|
+
function live(tenantId: TenantId, orgId: OrgId): Org | undefined {
|
|
46
|
+
const row = rows.get(orgId);
|
|
47
|
+
if (row === undefined || row.tenantId !== tenantId || deleted.has(orgId)) return undefined;
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Is `slug` held by a live org in the tenant other than `exceptId`? */
|
|
52
|
+
function slugTaken(tenantId: TenantId, slug: string, exceptId?: OrgId): boolean {
|
|
53
|
+
for (const row of rows.values()) {
|
|
54
|
+
if (deleted.has(row.id)) continue;
|
|
55
|
+
if (row.tenantId === tenantId && row.slug === slug && row.id !== exceptId) return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
34
59
|
|
|
35
60
|
return {
|
|
36
|
-
async create(tenantId: TenantId, spec: OrgSpec): Promise<
|
|
61
|
+
async create(tenantId: TenantId, spec: OrgSpec): Promise<OrgCreateOutcome> {
|
|
62
|
+
if (slugTaken(tenantId, spec.slug)) return { kind: 'slug-conflict', slug: spec.slug };
|
|
37
63
|
const id = nextOrgId();
|
|
38
64
|
const now = nowTimestamp();
|
|
39
65
|
const row: Org = {
|
|
@@ -45,20 +71,17 @@ export function makeInMemoryOrgBinding(): OrgBinding {
|
|
|
45
71
|
updatedAt: now,
|
|
46
72
|
};
|
|
47
73
|
rows.set(id, row);
|
|
48
|
-
return id;
|
|
74
|
+
return { kind: 'ok', orgId: id };
|
|
49
75
|
},
|
|
50
76
|
|
|
51
77
|
async get(tenantId: TenantId, orgId: OrgId): Promise<Org | undefined> {
|
|
52
|
-
|
|
53
|
-
if (row === undefined) return undefined;
|
|
54
|
-
if (row.tenantId !== tenantId) return undefined;
|
|
55
|
-
return row;
|
|
78
|
+
return live(tenantId, orgId);
|
|
56
79
|
},
|
|
57
80
|
|
|
58
81
|
async list(tenantId: TenantId, filter: OrgListFilter): Promise<Page<Org>> {
|
|
59
82
|
const all: Org[] = [];
|
|
60
83
|
for (const row of rows.values()) {
|
|
61
|
-
if (row.tenantId !== tenantId) continue;
|
|
84
|
+
if (row.tenantId !== tenantId || deleted.has(row.id)) continue;
|
|
62
85
|
if (filter.nameContains !== undefined && !row.name.includes(filter.nameContains)) {
|
|
63
86
|
continue;
|
|
64
87
|
}
|
|
@@ -72,10 +95,11 @@ export function makeInMemoryOrgBinding(): OrgBinding {
|
|
|
72
95
|
return paginate(all, filter.limit, filter.cursor);
|
|
73
96
|
},
|
|
74
97
|
|
|
75
|
-
async update(tenantId: TenantId, orgId: OrgId, patch: OrgPatch): Promise<
|
|
76
|
-
const row =
|
|
77
|
-
if (row === undefined
|
|
78
|
-
|
|
98
|
+
async update(tenantId: TenantId, orgId: OrgId, patch: OrgPatch): Promise<OrgUpdateOutcome> {
|
|
99
|
+
const row = live(tenantId, orgId);
|
|
100
|
+
if (row === undefined) return { kind: 'org-not-found' };
|
|
101
|
+
if (patch.slug !== undefined && slugTaken(tenantId, patch.slug, orgId)) {
|
|
102
|
+
return { kind: 'slug-conflict', slug: patch.slug };
|
|
79
103
|
}
|
|
80
104
|
const now: Timestamp = nowTimestamp();
|
|
81
105
|
const next: Org = {
|
|
@@ -87,15 +111,13 @@ export function makeInMemoryOrgBinding(): OrgBinding {
|
|
|
87
111
|
updatedAt: now,
|
|
88
112
|
};
|
|
89
113
|
rows.set(orgId, next);
|
|
114
|
+
return { kind: 'ok' };
|
|
90
115
|
},
|
|
91
116
|
|
|
92
117
|
async delete(tenantId: TenantId, orgId: OrgId): Promise<void> {
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
rows.delete(orgId);
|
|
118
|
+
// Idempotent: a no-op when absent, already deleted or in another tenant.
|
|
119
|
+
if (live(tenantId, orgId) === undefined) return;
|
|
120
|
+
deleted.add(orgId);
|
|
99
121
|
},
|
|
100
122
|
};
|
|
101
123
|
}
|
|
@@ -13,16 +13,22 @@
|
|
|
13
13
|
*
|
|
14
14
|
* `create` enforces the `Project.isDefault = true` uniqueness
|
|
15
15
|
* guardrail at write time: creating a second Default in the same
|
|
16
|
-
* tenant
|
|
16
|
+
* tenant resolves to `project-default-already-exists`. A project's
|
|
17
|
+
* slug is unique within its org, and a project without an org's among
|
|
18
|
+
* the tenant's projects without one (`slug-conflict`).
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import type { Filter, Page, ProjectId, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
20
22
|
|
|
21
23
|
import type {
|
|
22
24
|
ProjectBinding,
|
|
25
|
+
ProjectCreateOutcome,
|
|
23
26
|
ProjectListFilter,
|
|
24
27
|
ProjectMembershipAddInput,
|
|
28
|
+
ProjectMembershipAddOutcome,
|
|
25
29
|
ProjectMembershipBinding,
|
|
30
|
+
ProjectMembershipUpdateRoleOutcome,
|
|
31
|
+
ProjectUpdateOutcome,
|
|
26
32
|
} from '../project-binding.js';
|
|
27
33
|
import type {
|
|
28
34
|
TeamProjectGrant,
|
|
@@ -82,13 +88,40 @@ export function makeInMemoryProjectBinding(): {
|
|
|
82
88
|
return false;
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Is `slug` held by another project (not `exceptId`) where a project of
|
|
93
|
+
* `orgId` lives: in that org, or, without an org, among the tenant's
|
|
94
|
+
* projects without one?
|
|
95
|
+
*/
|
|
96
|
+
function slugTaken(
|
|
97
|
+
tenantId: TenantId,
|
|
98
|
+
orgId: Project['orgId'],
|
|
99
|
+
slug: string,
|
|
100
|
+
exceptId?: ProjectId,
|
|
101
|
+
): boolean {
|
|
102
|
+
for (const row of projectRows.values()) {
|
|
103
|
+
if (
|
|
104
|
+
row.tenantId === tenantId &&
|
|
105
|
+
row.orgId === orgId &&
|
|
106
|
+
row.slug === slug &&
|
|
107
|
+
row.id !== exceptId
|
|
108
|
+
) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
const projects: ProjectBinding = {
|
|
86
|
-
async create(tenantId, spec: ProjectSpec): Promise<
|
|
116
|
+
async create(tenantId, spec: ProjectSpec): Promise<ProjectCreateOutcome> {
|
|
87
117
|
const isDefault = spec.isDefault === true;
|
|
118
|
+
// The Default guardrail first: a caller asking for the Default
|
|
119
|
+
// wants to hear that one exists, whatever its slug.
|
|
88
120
|
if (isDefault && tenantAlreadyHasDefault(tenantId)) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
121
|
+
return { kind: 'project-default-already-exists' };
|
|
122
|
+
}
|
|
123
|
+
if (slugTaken(tenantId, spec.orgId, spec.slug)) {
|
|
124
|
+
return { kind: 'slug-conflict', slug: spec.slug };
|
|
92
125
|
}
|
|
93
126
|
const id = nextProjectId();
|
|
94
127
|
const now = nowTimestamp();
|
|
@@ -104,7 +137,7 @@ export function makeInMemoryProjectBinding(): {
|
|
|
104
137
|
...(spec.description !== undefined ? { description: spec.description } : {}),
|
|
105
138
|
};
|
|
106
139
|
projectRows.set(id, row);
|
|
107
|
-
return id;
|
|
140
|
+
return { kind: 'ok', projectId: id };
|
|
108
141
|
},
|
|
109
142
|
|
|
110
143
|
async get(tenantId, projectId): Promise<Project | undefined> {
|
|
@@ -135,15 +168,24 @@ export function makeInMemoryProjectBinding(): {
|
|
|
135
168
|
return undefined;
|
|
136
169
|
},
|
|
137
170
|
|
|
138
|
-
async update(tenantId, projectId, patch: ProjectPatch): Promise<
|
|
171
|
+
async update(tenantId, projectId, patch: ProjectPatch): Promise<ProjectUpdateOutcome> {
|
|
139
172
|
const row = findProjectInTenant(tenantId, projectId);
|
|
140
173
|
if (row === undefined) {
|
|
141
|
-
|
|
174
|
+
return { kind: 'project-not-found' };
|
|
142
175
|
}
|
|
143
176
|
let nextOrgId = row.orgId;
|
|
144
177
|
if (patch.orgId !== undefined) {
|
|
145
178
|
nextOrgId = patch.orgId === null ? undefined : patch.orgId;
|
|
146
179
|
}
|
|
180
|
+
// The slug it ends up with, where it ends up: a move to another org
|
|
181
|
+
// (or out of one) can meet a project with the same slug there.
|
|
182
|
+
const nextSlug = patch.slug ?? row.slug;
|
|
183
|
+
if (
|
|
184
|
+
(patch.slug !== undefined || patch.orgId !== undefined) &&
|
|
185
|
+
slugTaken(tenantId, nextOrgId, nextSlug, projectId)
|
|
186
|
+
) {
|
|
187
|
+
return { kind: 'slug-conflict', slug: nextSlug };
|
|
188
|
+
}
|
|
147
189
|
const nextDescription = patch.description ?? row.description;
|
|
148
190
|
const next: Project = {
|
|
149
191
|
id: row.id,
|
|
@@ -157,6 +199,7 @@ export function makeInMemoryProjectBinding(): {
|
|
|
157
199
|
...(nextDescription !== undefined ? { description: nextDescription } : {}),
|
|
158
200
|
};
|
|
159
201
|
projectRows.set(projectId, next);
|
|
202
|
+
return { kind: 'ok' };
|
|
160
203
|
},
|
|
161
204
|
|
|
162
205
|
async delete(tenantId, projectId): Promise<void> {
|
|
@@ -181,13 +224,13 @@ export function makeInMemoryProjectBinding(): {
|
|
|
181
224
|
};
|
|
182
225
|
|
|
183
226
|
const memberships: ProjectMembershipBinding = {
|
|
184
|
-
async add(tenantId, input: ProjectMembershipAddInput): Promise<
|
|
227
|
+
async add(tenantId, input: ProjectMembershipAddInput): Promise<ProjectMembershipAddOutcome> {
|
|
185
228
|
const proj = findProjectInTenant(tenantId, input.projectId);
|
|
186
229
|
if (proj === undefined) {
|
|
187
|
-
|
|
230
|
+
return { kind: 'project-not-found' };
|
|
188
231
|
}
|
|
189
232
|
const key = projMembershipKey(input.projectId, input.userId);
|
|
190
|
-
if (membershipRows.has(key)) return;
|
|
233
|
+
if (membershipRows.has(key)) return { kind: 'ok' };
|
|
191
234
|
const row: ProjectMembership = {
|
|
192
235
|
projectId: input.projectId,
|
|
193
236
|
userId: input.userId,
|
|
@@ -195,6 +238,7 @@ export function makeInMemoryProjectBinding(): {
|
|
|
195
238
|
joinedAt: nowTimestamp(),
|
|
196
239
|
};
|
|
197
240
|
membershipRows.set(key, row);
|
|
241
|
+
return { kind: 'ok' };
|
|
198
242
|
},
|
|
199
243
|
|
|
200
244
|
async remove(tenantId, projectId, userId): Promise<void> {
|
|
@@ -227,17 +271,23 @@ export function makeInMemoryProjectBinding(): {
|
|
|
227
271
|
return paginate(all, filter.limit, filter.cursor);
|
|
228
272
|
},
|
|
229
273
|
|
|
230
|
-
async updateRole(
|
|
274
|
+
async updateRole(
|
|
275
|
+
tenantId,
|
|
276
|
+
projectId,
|
|
277
|
+
userId,
|
|
278
|
+
role: ProjectRole,
|
|
279
|
+
): Promise<ProjectMembershipUpdateRoleOutcome> {
|
|
231
280
|
const proj = findProjectInTenant(tenantId, projectId);
|
|
232
281
|
if (proj === undefined) {
|
|
233
|
-
|
|
282
|
+
return { kind: 'project-not-found' };
|
|
234
283
|
}
|
|
235
284
|
const key = projMembershipKey(projectId, userId);
|
|
236
285
|
const row = membershipRows.get(key);
|
|
237
286
|
if (row === undefined) {
|
|
238
|
-
|
|
287
|
+
return { kind: 'project-membership-not-found' };
|
|
239
288
|
}
|
|
240
289
|
membershipRows.set(key, { ...row, role });
|
|
290
|
+
return { kind: 'ok' };
|
|
241
291
|
},
|
|
242
292
|
};
|
|
243
293
|
|