@kindgi/platform 0.1.3 → 0.1.4-rc.1

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 (40) hide show
  1. package/README.md +31 -0
  2. package/dist/in-memory/index.d.ts +1 -0
  3. package/dist/in-memory/index.d.ts.map +1 -1
  4. package/dist/in-memory/org-binding.d.ts.map +1 -1
  5. package/dist/in-memory/org-binding.js +33 -16
  6. package/dist/in-memory/org-binding.js.map +1 -1
  7. package/dist/in-memory/project-binding.d.ts +26 -1
  8. package/dist/in-memory/project-binding.d.ts.map +1 -1
  9. package/dist/in-memory/project-binding.js +47 -8
  10. package/dist/in-memory/project-binding.js.map +1 -1
  11. package/dist/in-memory/team-binding.d.ts +2 -1
  12. package/dist/in-memory/team-binding.d.ts.map +1 -1
  13. package/dist/in-memory/team-binding.js +31 -7
  14. package/dist/in-memory/team-binding.js.map +1 -1
  15. package/dist/index.d.ts +5 -4
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/org-binding.d.ts +56 -12
  19. package/dist/org-binding.d.ts.map +1 -1
  20. package/dist/project-binding.d.ts +76 -12
  21. package/dist/project-binding.d.ts.map +1 -1
  22. package/dist/team-binding.d.ts +70 -12
  23. package/dist/team-binding.d.ts.map +1 -1
  24. package/dist/tenant-hierarchy-binding.d.ts +31 -5
  25. package/dist/tenant-hierarchy-binding.d.ts.map +1 -1
  26. package/package.json +2 -2
  27. package/src/in-memory/index.ts +1 -0
  28. package/src/in-memory/org-binding.ts +41 -19
  29. package/src/in-memory/project-binding.ts +85 -16
  30. package/src/in-memory/team-binding.ts +48 -12
  31. package/src/index.ts +14 -1
  32. package/src/org-binding.ts +60 -12
  33. package/src/project-binding.ts +88 -12
  34. package/src/team-binding.ts +82 -12
  35. package/src/tenant-hierarchy-binding.ts +21 -6
  36. package/tests/in-memory-project-binding.test.ts +13 -1
  37. package/tests/in-memory-team-binding.test.ts +8 -1
  38. package/tests/org-binding.conformance.ts +93 -18
  39. package/tests/project-binding.conformance.ts +221 -37
  40. package/tests/team-binding.conformance.ts +120 -28
@@ -9,6 +9,12 @@
9
9
  * `@kindgi/types` (`items` + `nextCursor`).
10
10
  * - `list` filters extend the shared `Filter` shape so every list call
11
11
  * shares the same pagination fields.
12
+ * - A write the caller can get wrong (a slug already taken, a row that
13
+ * doesn't exist) resolves to an outcome — a union discriminated on
14
+ * `kind`, `'ok'` on success — and never rejects for it. Each
15
+ * non-`ok` `kind` is also the API error code the routes answer with
16
+ * (`slug-conflict` is a 409, `org-not-found` a 404). A rejected
17
+ * promise means the backend failed.
12
18
  *
13
19
  * This package ships the interface + reference in-memory adapter
14
20
  * (in `./in-memory/org-binding.ts`). Any adapter can be checked with
@@ -36,11 +42,13 @@ export interface OrgListFilter extends Filter {
36
42
  */
37
43
  export interface OrgBinding {
38
44
  /**
39
- * Create a new `Org` in the given tenant. Returns the freshly-
40
- * assigned `OrgId`. The binding is responsible for allocating IDs,
41
- * timestamps, and slug-uniqueness enforcement within the tenant.
45
+ * Create a new `Org` in the given tenant. Resolves to `ok` with the
46
+ * freshly-assigned `OrgId`, or to `slug-conflict` when another org in
47
+ * the tenant has `spec.slug`. The binding is responsible for
48
+ * allocating IDs, timestamps, and slug-uniqueness enforcement within
49
+ * the tenant.
42
50
  */
43
- create(tenantId: TenantId, spec: OrgSpec): Promise<OrgId>;
51
+ create(tenantId: TenantId, spec: OrgSpec): Promise<OrgCreateOutcome>;
44
52
  /**
45
53
  * Look up an `Org` by id. Returns `undefined` when no row exists in
46
54
  * the given tenant, or when the row exists in a different tenant
@@ -55,16 +63,52 @@ export interface OrgBinding {
55
63
  list(tenantId: TenantId, filter: OrgListFilter): Promise<Page<Org>>;
56
64
  /**
57
65
  * Partially update an `Org`. Fields absent from `patch` are left
58
- * unchanged; fields present are set to the new value. Not-found is
59
- * an error the adapter surfaces (e.g. as a rejected promise); the
60
- * conformance suite specifies the exact shape.
66
+ * unchanged; fields present are set to the new value. Resolves to
67
+ * `org-not-found` when no org has `orgId` in the tenant, and to
68
+ * `slug-conflict` when `patch.slug` is another org's slug.
61
69
  */
62
- update(tenantId: TenantId, orgId: OrgId, patch: OrgPatch): Promise<void>;
70
+ update(tenantId: TenantId, orgId: OrgId, patch: OrgPatch): Promise<OrgUpdateOutcome>;
63
71
  /**
64
- * Delete an `Org`. Cascade semantics (dependent teams / projects) are
65
- * a storage-layer concern — the in-memory adapter does the delete
66
- * unconditionally.
72
+ * Delete an `Org`: a tombstone, not an erase. From then on `get`,
73
+ * `list` and `update` treat it as unknown, and its slug is free for a
74
+ * new org; a retention policy on the `org` domain purges the row.
75
+ * What happens to dependents (teams, projects, org-scoped config) is
76
+ * the storage's concern; the in-memory adapter has none. A storage
77
+ * whose projects leave a deleted org (the Kindgi runtime's: they
78
+ * become projects without an org) answers `slug-conflict` instead
79
+ * when one of them has the slug of a project that has no org, and
80
+ * deletes nothing. Nothing (`void`): the org is deleted, or wasn't
81
+ * there (or was already deleted).
67
82
  */
68
- delete(tenantId: TenantId, orgId: OrgId): Promise<void>;
83
+ delete(tenantId: TenantId, orgId: OrgId): Promise<void | OrgDeleteConflict>;
69
84
  }
85
+ /**
86
+ * Why `OrgBinding.delete` deleted nothing: the org's projects would leave
87
+ * it with `slugs` that projects without an org already have. Rename or
88
+ * move them first.
89
+ */
90
+ export interface OrgDeleteConflict {
91
+ readonly kind: 'slug-conflict';
92
+ readonly slugs: readonly string[];
93
+ }
94
+ /** What `OrgBinding.create` did. */
95
+ export type OrgCreateOutcome = {
96
+ readonly kind: 'ok';
97
+ readonly orgId: OrgId;
98
+ } | {
99
+ /** Another org in the tenant already has this slug. */
100
+ readonly kind: 'slug-conflict';
101
+ readonly slug: string;
102
+ };
103
+ /** What `OrgBinding.update` did. */
104
+ export type OrgUpdateOutcome = {
105
+ readonly kind: 'ok';
106
+ } | {
107
+ /** No org with this id in the tenant. */
108
+ readonly kind: 'org-not-found';
109
+ } | {
110
+ /** Another org in the tenant already has the patched slug. */
111
+ readonly kind: 'slug-conflict';
112
+ readonly slug: string;
113
+ };
70
114
  //# sourceMappingURL=org-binding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"org-binding.d.ts","sourceRoot":"","sources":["../src/org-binding.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAc,SAAQ,MAAM;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1D;;;;OAIG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IAChE;;;;OAIG;IACH,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD"}
1
+ {"version":3,"file":"org-binding.d.ts","sourceRoot":"","sources":["../src/org-binding.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAc,SAAQ,MAAM;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrE;;;;OAIG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IAChE;;;;OAIG;IACH,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrF;;;;;;;;;;;OAWG;IAEH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,GAAG,iBAAiB,CAAC,CAAC;CAC7E;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,oCAAoC;AACpC,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC9C;IACE,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN,oCAAoC;AACpC,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACvB;IACE,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC,GACD;IACE,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC"}
@@ -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. If
48
- * `spec.isDefault === true` and a Default already exists, the
49
- * binding rejects — the isDefault-uniqueness guardrail is enforced
50
- * at write time.
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<ProjectId>;
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<void>;
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<void>;
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,65 @@ 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. Errors when the
111
- * membership doesn't exist.
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<void>;
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
+ /**
138
+ * The org the project would be in isn't one of the tenant's: it never
139
+ * existed, or it was deleted. Nothing changed.
140
+ */
141
+ readonly kind: 'org-not-found';
142
+ readonly orgId: OrgId;
143
+ };
144
+ /** What `ProjectBinding.update` did. */
145
+ export type ProjectUpdateOutcome = {
146
+ readonly kind: 'ok';
147
+ } | {
148
+ /** No project with this id in the tenant. */
149
+ readonly kind: 'project-not-found';
150
+ } | {
151
+ /** Another project in the tenant already has the patched slug. */
152
+ readonly kind: 'slug-conflict';
153
+ readonly slug: string;
154
+ } | {
155
+ /**
156
+ * The org the project would be in isn't one of the tenant's: it never
157
+ * existed, or it was deleted. Nothing changed.
158
+ */
159
+ readonly kind: 'org-not-found';
160
+ readonly orgId: OrgId;
161
+ };
162
+ /** What `ProjectMembershipBinding.add` did (`ok` when already a member, too). */
163
+ export type ProjectMembershipAddOutcome = {
164
+ readonly kind: 'ok';
165
+ } | {
166
+ /** No project with this id in the tenant. */
167
+ readonly kind: 'project-not-found';
168
+ };
169
+ /** What `ProjectMembershipBinding.updateRole` did. */
170
+ export type ProjectMembershipUpdateRoleOutcome = {
171
+ readonly kind: 'ok';
172
+ } | {
173
+ /** No project with this id in the tenant. */
174
+ readonly kind: 'project-not-found';
175
+ } | {
176
+ /** The user has no direct membership on the project. */
177
+ readonly kind: 'project-membership-not-found';
178
+ };
115
179
  //# 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;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEtF,OAAO,KAAK,EACV,OAAO,EACP,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,MAAM;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAClE;;;OAGG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC5E;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5E;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC7D;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACjG;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;IAClG;;;OAGG;IACH,UAAU,CACR,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB"}
1
+ {"version":3,"file":"project-binding.d.ts","sourceRoot":"","sources":["../src/project-binding.ts"],"names":[],"mappings":"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"}
@@ -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). Returns the assigned
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<TeamId>;
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<void>;
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<void>;
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,62 @@ 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. Errors when the membership
103
- * doesn't exist — role mutation of a non-member is a caller mistake,
104
- * not a silent add. Conformance suite pins the not-found behaviour.
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<void>;
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
+ /**
125
+ * The org the team would be in isn't one of the tenant's: it never
126
+ * existed, or it was deleted. Nothing changed.
127
+ */
128
+ readonly kind: 'org-not-found';
129
+ readonly orgId: OrgId;
130
+ };
131
+ /** What `TeamBinding.update` did. */
132
+ export type TeamUpdateOutcome = {
133
+ readonly kind: 'ok';
134
+ } | {
135
+ /** No team with this id in the tenant. */
136
+ readonly kind: 'team-not-found';
137
+ } | {
138
+ /** Another team in the tenant already has the patched slug. */
139
+ readonly kind: 'slug-conflict';
140
+ readonly slug: string;
141
+ } | {
142
+ /**
143
+ * The org the team would be in isn't one of the tenant's: it never
144
+ * existed, or it was deleted. Nothing changed.
145
+ */
146
+ readonly kind: 'org-not-found';
147
+ readonly orgId: OrgId;
148
+ };
149
+ /** What `TeamMembershipBinding.add` did (`ok` when already a member, too). */
150
+ export type TeamMembershipAddOutcome = {
151
+ readonly kind: 'ok';
152
+ } | {
153
+ /** No team with this id in the tenant. */
154
+ readonly kind: 'team-not-found';
155
+ };
156
+ /** What `TeamMembershipBinding.updateRole` did. */
157
+ export type TeamMembershipUpdateRoleOutcome = {
158
+ readonly kind: 'ok';
159
+ } | {
160
+ /** No team with this id in the tenant. */
161
+ readonly kind: 'team-not-found';
162
+ } | {
163
+ /** The user isn't a member of the team. */
164
+ readonly kind: 'team-membership-not-found';
165
+ };
108
166
  //# 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;;;;;;;;;;GAUG;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;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D;;;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;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E;;;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;;;;;;;;;OASG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE;;;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;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/F"}
1
+ {"version":3,"file":"team-binding.d.ts","sourceRoot":"","sources":["../src/team-binding.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,12 @@ export type CreateTeamError = {
23
23
  readonly code: 'slug-conflict';
24
24
  readonly message: string;
25
25
  readonly cause?: unknown;
26
+ }
27
+ /** `spec.orgId` isn't the tenant's: it never existed, or it was deleted. */
28
+ | {
29
+ readonly code: 'org-not-found';
30
+ readonly message: string;
31
+ readonly orgId: OrgId;
26
32
  } | {
27
33
  readonly code: 'insert-failed';
28
34
  readonly message: string;
@@ -43,6 +49,12 @@ export type CreateProjectError = {
43
49
  readonly code: 'slug-conflict';
44
50
  readonly message: string;
45
51
  readonly cause?: unknown;
52
+ }
53
+ /** `spec.orgId` isn't the tenant's: it never existed, or it was deleted. */
54
+ | {
55
+ readonly code: 'org-not-found';
56
+ readonly message: string;
57
+ readonly orgId: OrgId;
46
58
  } | {
47
59
  readonly code: 'default-conflict';
48
60
  readonly message: string;
@@ -58,10 +70,24 @@ export interface AddProjectMemberParams {
58
70
  readonly userId: UserId;
59
71
  readonly role: ProjectRole;
60
72
  }
61
- export interface MembershipMutationError {
62
- readonly code: string;
73
+ export type AddTeamMemberError = {
74
+ /** No team with this id in the tenant. */
75
+ readonly code: 'team-not-found';
63
76
  readonly message: string;
64
- }
77
+ } | {
78
+ readonly code: 'add-failed';
79
+ readonly message: string;
80
+ readonly cause?: unknown;
81
+ };
82
+ export type AddProjectMemberError = {
83
+ /** No project with this id in the tenant. */
84
+ readonly code: 'project-not-found';
85
+ readonly message: string;
86
+ } | {
87
+ readonly code: 'add-failed';
88
+ readonly message: string;
89
+ readonly cause?: unknown;
90
+ };
65
91
  /**
66
92
  * Tenant-summary read shape returned by `getTenant`. Only the fields
67
93
  * the `/v1/tenant` GET route surfaces — id / name / slug / createdAt /
@@ -92,8 +118,8 @@ export interface TenantHierarchyBinding {
92
118
  createProject(params: CreateProjectParams): Promise<Result<{
93
119
  readonly projectId: ProjectId;
94
120
  }, CreateProjectError>>;
95
- addTeamMember(params: AddTeamMemberParams): Promise<Result<void, MembershipMutationError>>;
96
- addProjectMember(params: AddProjectMemberParams): Promise<Result<void, MembershipMutationError>>;
121
+ addTeamMember(params: AddTeamMemberParams): Promise<Result<void, AddTeamMemberError>>;
122
+ addProjectMember(params: AddProjectMemberParams): Promise<Result<void, AddProjectMemberError>>;
97
123
  /**
98
124
  * Look up a tenant by id. Returns `null` when no tenant exists.
99
125
  * 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,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;KAAE,EAAE,cAAc,CAAC,CAAC,CAAC;IAC/F,UAAU,CACR,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,MAAM,CAAC;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IACjE,aAAa,CACX,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,MAAM,CAAC;QAAE,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;KAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC1E,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAC3F,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC,CAAC;IACjG;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;CAC9D"}
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",
3
+ "version": "0.1.4-rc.1",
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.3"
33
+ "@kindgi/types": "0.1.4-rc.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.10.5",
@@ -10,3 +10,4 @@
10
10
  export { makeInMemoryOrgBinding } from './org-binding.js';
11
11
  export { makeInMemoryTeamBinding } from './team-binding.js';
12
12
  export { makeInMemoryProjectBinding } from './project-binding.js';
13
+ export type { InMemoryHierarchyOptions } from './project-binding.js';
@@ -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 { OrgBinding, OrgListFilter } from '../org-binding.js';
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<OrgId> {
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
- const row = rows.get(orgId);
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<void> {
76
- const row = rows.get(orgId);
77
- if (row === undefined || row.tenantId !== tenantId) {
78
- throw new Error(`org-not-found: ${orgId}`);
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
- const row = rows.get(orgId);
94
- if (row === undefined || row.tenantId !== tenantId) {
95
- // Idempotent delete — no-op when absent or in another tenant.
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
  }