@house-rules/party 0.0.0-stage → 0.1.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.
Files changed (69) hide show
  1. package/AGENTS.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +142 -2
  4. package/dist/adapters/memory/party.d.ts +3 -0
  5. package/dist/adapters/memory/party.js +170 -0
  6. package/dist/adapters/memory/person.d.ts +3 -0
  7. package/dist/adapters/memory/person.js +87 -0
  8. package/dist/adapters/postgres/party.d.ts +4 -0
  9. package/dist/adapters/postgres/party.js +73 -0
  10. package/dist/adapters/postgres/person.d.ts +4 -0
  11. package/dist/adapters/postgres/person.js +63 -0
  12. package/dist/assignment/assignment.d.ts +13 -0
  13. package/dist/assignment/assignment.js +9 -0
  14. package/dist/assignment/holders.d.ts +11 -0
  15. package/dist/assignment/holders.js +7 -0
  16. package/dist/contract.d.ts +9 -0
  17. package/dist/contract.js +9 -0
  18. package/dist/facade.d.ts +22 -0
  19. package/dist/facade.js +35 -0
  20. package/dist/index.d.ts +294 -0
  21. package/dist/index.js +20 -0
  22. package/dist/invitation/invitation.d.ts +36 -0
  23. package/dist/invitation/invitation.js +17 -0
  24. package/dist/invitation/invitations.d.ts +34 -0
  25. package/dist/invitation/invitations.js +70 -0
  26. package/dist/organisation/organisation.d.ts +59 -0
  27. package/dist/organisation/organisation.js +46 -0
  28. package/dist/person/bind-party.d.ts +7 -0
  29. package/dist/person/bind-party.js +42 -0
  30. package/dist/person/operations.d.ts +306 -0
  31. package/dist/person/operations.js +92 -0
  32. package/dist/person/party.d.ts +36 -0
  33. package/dist/person/party.js +55 -0
  34. package/dist/person/permissions.d.ts +6 -0
  35. package/dist/person/permissions.js +5 -0
  36. package/dist/person/person-store.d.ts +21 -0
  37. package/dist/person/person-store.js +3 -0
  38. package/dist/person/person.d.ts +43 -0
  39. package/dist/person/person.js +25 -0
  40. package/dist/person/policy.d.ts +1 -0
  41. package/dist/person/policy.js +6 -0
  42. package/dist/person/principal-operations.d.ts +110 -0
  43. package/dist/person/principal-operations.js +77 -0
  44. package/dist/person/principal.d.ts +126 -0
  45. package/dist/person/principal.js +49 -0
  46. package/dist/person/principals.d.ts +102 -0
  47. package/dist/person/principals.js +73 -0
  48. package/dist/relationship/relationship.d.ts +30 -0
  49. package/dist/relationship/relationship.js +12 -0
  50. package/dist/relationship/removal.d.ts +101 -0
  51. package/dist/relationship/removal.js +22 -0
  52. package/dist/role/access.d.ts +171 -0
  53. package/dist/role/access.js +252 -0
  54. package/dist/role/operations.d.ts +145 -0
  55. package/dist/role/operations.js +215 -0
  56. package/dist/role/people.d.ts +45 -0
  57. package/dist/role/people.js +57 -0
  58. package/dist/role/permission-catalogue.d.ts +2 -0
  59. package/dist/role/permission-catalogue.js +1 -0
  60. package/dist/role/role.d.ts +31 -0
  61. package/dist/role/role.js +12 -0
  62. package/dist/storage.d.ts +38 -0
  63. package/dist/storage.js +4 -0
  64. package/migrations/0001_party_person.sql +5 -0
  65. package/migrations/0002_party_model.sql +27 -0
  66. package/migrations/0003_party_invitation.sql +11 -0
  67. package/migrations/0004_publish_staff.sql +4 -0
  68. package/migrations/0005_principals.sql +18 -0
  69. package/package.json +57 -3
@@ -0,0 +1,57 @@
1
+ import { Effect } from "effect";
2
+ import { PartyDenied, PartyNotFound } from "../organisation/organisation.js";
3
+ export const makePeople = (store, persons, role, adminScopes, organisationExists) => {
4
+ const assignmentDetails = Effect.fn("Party.assignmentDetails")(function* (personId, scopes) {
5
+ return yield* Effect.forEach(yield* store.scopedAssignments(personId, scopes), Effect.fnUntraced(function* (a) {
6
+ const held = yield* role(a.roleKey);
7
+ const org = a.organisationId === null ? undefined : yield* store.organisation(a.organisationId);
8
+ return {
9
+ roleKey: a.roleKey,
10
+ roleName: held?.name ?? a.roleKey,
11
+ organisationId: a.organisationId,
12
+ organisationName: org?.name ?? null,
13
+ };
14
+ }));
15
+ });
16
+ return {
17
+ listPeople: Effect.fn("Party.listPeople")(function* (caller) {
18
+ const scopes = yield* adminScopes(caller);
19
+ return yield* Effect.forEach(yield* persons.list(scopes === null ? null : yield* store.memberIds(scopes)), Effect.fnUntraced(function* (person) {
20
+ return {
21
+ ...person,
22
+ assignments: yield* assignmentDetails(person.id, scopes),
23
+ organisations: yield* store.relationsFor(person.id, scopes),
24
+ };
25
+ }));
26
+ }),
27
+ listMembers: Effect.fn("Party.listMembers")(function* (caller, organisationId) {
28
+ const scopes = yield* adminScopes(caller).pipe(Effect.catchTag("PartyDenied", () => new PartyNotFound()));
29
+ if (scopes !== null && !scopes.includes(organisationId))
30
+ return yield* new PartyNotFound();
31
+ yield* organisationExists(organisationId);
32
+ return yield* Effect.forEach(yield* persons.list(yield* store.memberIds([organisationId])), Effect.fnUntraced(function* (person) {
33
+ const relations = yield* store.relationsFor(person.id, [organisationId]);
34
+ return {
35
+ ...person,
36
+ relation: relations[0]?.relation ?? null,
37
+ roles: (yield* assignmentDetails(person.id, [organisationId])).map(({ roleKey, roleName }) => ({ roleKey, roleName })),
38
+ };
39
+ }));
40
+ }),
41
+ myOrganisations: Effect.fn("Party.myOrganisations")(function* (caller) {
42
+ if (caller.kind !== "person")
43
+ return yield* new PartyDenied();
44
+ return yield* Effect.forEach(yield* store.organisationsFor(caller.actor.id), Effect.fnUntraced(function* (org) {
45
+ const roles = yield* Effect.forEach(yield* store.scopedAssignments(caller.actor.id, [org.id]), Effect.fnUntraced(function* (a) {
46
+ const held = yield* role(a.roleKey);
47
+ return {
48
+ roleKey: a.roleKey,
49
+ roleName: held?.name ?? a.roleKey,
50
+ permissions: held?.permissions ?? [],
51
+ };
52
+ }));
53
+ return { ...org, roles };
54
+ }));
55
+ }),
56
+ };
57
+ };
@@ -0,0 +1,2 @@
1
+ import type { PermissionCatalogue } from "@house-rules/application";
2
+ export type RoleCatalogue = PermissionCatalogue["Service"];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { PartyInputInvalid } from "../organisation/organisation.js";
3
+ export declare const RoleKey: Schema.NonEmptyString;
4
+ export declare const RolePermission: Schema.String;
5
+ export declare const Role: Schema.Struct<{
6
+ readonly key: Schema.NonEmptyString;
7
+ readonly name: Schema.NonEmptyString;
8
+ readonly permissions: Schema.$Array<Schema.String>;
9
+ readonly organisationId: Schema.NullOr<Schema.NonEmptyString>;
10
+ readonly builtIn: Schema.Boolean;
11
+ }>;
12
+ export type Role = typeof Role.Type;
13
+ export type BuiltInRoles = Readonly<Record<string, {
14
+ readonly name: string;
15
+ readonly permissions: readonly string[];
16
+ }>>;
17
+ export interface PartyOptions {
18
+ readonly builtInRoles: BuiltInRoles;
19
+ readonly holdersLimit?: number;
20
+ readonly handles?: {
21
+ readonly pattern?: RegExp;
22
+ readonly reserved?: readonly string[];
23
+ };
24
+ }
25
+ export declare const validateRole: (input: Role) => Effect.Effect<{
26
+ readonly key: string;
27
+ readonly name: string;
28
+ readonly permissions: readonly string[];
29
+ readonly organisationId: string | null;
30
+ readonly builtIn: boolean;
31
+ }, PartyInputInvalid, never>;
@@ -0,0 +1,12 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { PartyInputInvalid } from "../organisation/organisation.js";
3
+ export const RoleKey = Schema.NonEmptyString;
4
+ export const RolePermission = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$/u));
5
+ export const Role = Schema.Struct({
6
+ key: RoleKey,
7
+ name: Schema.NonEmptyString,
8
+ permissions: Schema.Array(RolePermission),
9
+ organisationId: Schema.NullOr(Schema.NonEmptyString),
10
+ builtIn: Schema.Boolean,
11
+ });
12
+ export const validateRole = (input) => Schema.decodeEffect(Role)(input).pipe(Effect.mapError(() => new PartyInputInvalid({ message: "Invalid role or permission." })));
@@ -0,0 +1,38 @@
1
+ import type { ActorId, NoOpenUnit } from "@house-rules/domain";
2
+ import { Context, type Effect } from "effect";
3
+ import type { Assignment } from "./assignment/assignment.js";
4
+ import type { Organisation, OrganisationRelation, OwnOrganisation, PartyUnavailable } from "./organisation/organisation.js";
5
+ import type { Relationship } from "./relationship/relationship.js";
6
+ import type { Role } from "./role/role.js";
7
+ import type { StoredInvitation } from "./invitation/invitation.js";
8
+ export { PersonStore } from "./person/person-store.js";
9
+ type Read<A> = Effect.Effect<A, PartyUnavailable>;
10
+ type Write<A> = Effect.Effect<A, PartyUnavailable | NoOpenUnit>;
11
+ declare const PartyStore_base: Context.ServiceClass<PartyStore, "@house-rules/party/PartyStore", {
12
+ readonly organisation: (id: string) => Read<Organisation | undefined>;
13
+ readonly organisationsFor: (personId: ActorId) => Read<readonly OwnOrganisation[]>;
14
+ readonly relationsFor: (personId: ActorId, organisationIds: readonly string[] | null) => Read<readonly OrganisationRelation[]>;
15
+ readonly scopedAssignments: (personId: ActorId, organisationIds: readonly string[] | null) => Read<readonly Assignment[]>;
16
+ readonly assignmentsInOrganisation: (organisationId: string) => Read<readonly Assignment[]>;
17
+ readonly holderIds: (role: string, scope: string | null, take: number) => Read<readonly ActorId[]>;
18
+ readonly lockOrganisation: (organisationId: string) => Write<void>;
19
+ readonly memberIds: (organisationIds: readonly string[]) => Read<readonly ActorId[]>;
20
+ readonly insertInvitation: (value: StoredInvitation) => Write<void>;
21
+ readonly consumeInvitation: (hash: string, now: number) => Write<StoredInvitation | undefined>;
22
+ readonly invitation: (id: string) => Read<StoredInvitation | undefined>;
23
+ readonly invitations: (organisationId: string) => Read<readonly StoredInvitation[]>;
24
+ readonly revokeInvitation: (id: string) => Write<void>;
25
+ readonly insertOrganisation: (organisation: Organisation) => Write<boolean>;
26
+ readonly relate: (relationship: Relationship) => Write<void>;
27
+ readonly unrelate: (relationship: Relationship) => Write<void>;
28
+ readonly isRelated: (relationship: Relationship) => Read<boolean>;
29
+ readonly role: (key: string) => Read<Role | undefined>;
30
+ readonly roles: (organisationIds?: readonly string[] | null) => Read<readonly Role[]>;
31
+ readonly putRole: (role: Role) => Write<void>;
32
+ readonly deleteRole: (key: string) => Write<void>;
33
+ readonly assign: (assignment: Assignment) => Write<void>;
34
+ readonly revoke: (assignment: Assignment) => Write<void>;
35
+ readonly assignments: (personId: ActorId, organisationIds?: readonly string[] | null, builtInKeys?: readonly string[]) => Read<readonly Assignment[]>;
36
+ }>;
37
+ export declare class PartyStore extends PartyStore_base {
38
+ }
@@ -0,0 +1,4 @@
1
+ import { Context } from "effect";
2
+ export { PersonStore } from "./person/person-store.js";
3
+ export class PartyStore extends Context.Service()("@house-rules/party/PartyStore") {
4
+ }
@@ -0,0 +1,5 @@
1
+ create table party_person (
2
+ id text primary key,
3
+ display_name text not null,
4
+ created_at timestamptz not null default now()
5
+ );
@@ -0,0 +1,27 @@
1
+ CREATE TABLE party_organisation (
2
+ id text PRIMARY KEY,
3
+ name text NOT NULL CHECK (length(name) > 0)
4
+ );
5
+ --> statement-breakpoint
6
+ CREATE TABLE party_relationship (
7
+ from_kind text NOT NULL CHECK (from_kind IN ('person', 'organisation')),
8
+ from_id text NOT NULL,
9
+ to_kind text NOT NULL CHECK (to_kind IN ('person', 'organisation')),
10
+ to_id text NOT NULL,
11
+ kind text NOT NULL CHECK (length(kind) > 0),
12
+ PRIMARY KEY (from_kind, from_id, to_kind, to_id, kind)
13
+ );
14
+ --> statement-breakpoint
15
+ CREATE TABLE party_role (
16
+ key text PRIMARY KEY,
17
+ name text NOT NULL CHECK (length(name) > 0),
18
+ permissions text[] NOT NULL,
19
+ organisation_id text REFERENCES party_organisation(id)
20
+ );
21
+ --> statement-breakpoint
22
+ CREATE TABLE party_assignment (
23
+ person_id text NOT NULL REFERENCES party_person(id) ON DELETE CASCADE,
24
+ role_key text NOT NULL,
25
+ organisation_id text REFERENCES party_organisation(id),
26
+ UNIQUE NULLS NOT DISTINCT (person_id, role_key, organisation_id)
27
+ );
@@ -0,0 +1,11 @@
1
+ CREATE TABLE party_invitation (
2
+ id text PRIMARY KEY,
3
+ organisation_id text NOT NULL,
4
+ role_key text,
5
+ created_by text NOT NULL,
6
+ code_hash text NOT NULL UNIQUE,
7
+ expires_at double precision NOT NULL,
8
+ state text NOT NULL CHECK (state IN ('pending', 'used', 'revoked'))
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE INDEX party_invitation_org ON party_invitation (organisation_id);
@@ -0,0 +1,4 @@
1
+ CREATE VIEW party_staff_read AS
2
+ SELECT from_id AS person_id, to_id AS organisation_id
3
+ FROM party_relationship
4
+ WHERE from_kind = 'person' AND to_kind = 'organisation' AND kind = 'staff';
@@ -0,0 +1,18 @@
1
+ alter table party_person
2
+ add column kind text not null default 'person' check (kind in ('person', 'agent')),
3
+ add column handle text,
4
+ add column created_by text,
5
+ add column disabled_at timestamptz;
6
+ --> statement-breakpoint
7
+ update party_person set handle = id;
8
+ --> statement-breakpoint
9
+ alter table party_person alter column handle set not null;
10
+ --> statement-breakpoint
11
+ alter table party_person add constraint party_person_handle_unique unique (handle);
12
+ --> statement-breakpoint
13
+ create view party_principals_read as
14
+ select id, display_name, kind, handle, created_by, created_at, disabled_at
15
+ from party_person;
16
+ --> statement-breakpoint
17
+ create view party_assignments_read as
18
+ select person_id, role_key, organisation_id from party_assignment;
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@house-rules/party",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Person records keyed by ActorId, with self-only rename and module-owned storage.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "migrations",
10
+ "AGENTS.md",
11
+ "README.md"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./contract": {
19
+ "types": "./dist/contract.d.ts",
20
+ "default": "./dist/contract.js"
21
+ }
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "houseRules": {
27
+ "migrations": "migrations"
28
+ },
29
+ "peerDependencies": {
30
+ "@house-rules/application": "0.2.0",
31
+ "@house-rules/domain": "0.2.0",
32
+ "effect": "4.0.3"
33
+ },
34
+ "devDependencies": {
35
+ "@effect/platform-node": "4.0.3",
36
+ "@effect/platform-node-shared": "4.0.3",
37
+ "@effect/sql-pg": "4.0.3",
38
+ "@effect/vitest": "4.0.3",
39
+ "@house-rules/application": "0.2.0",
40
+ "@house-rules/domain": "0.2.0",
41
+ "@house-rules/migrations": "0.4.0",
42
+ "effect": "4.0.3",
43
+ "typescript": "7.0.2",
44
+ "vitest": "5.0.1"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/JakubSzwajka/house-rules.git",
49
+ "directory": "packages/party"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/JakubSzwajka/house-rules/issues"
53
+ },
54
+ "homepage": "https://github.com/JakubSzwajka/house-rules",
55
+ "scripts": {
56
+ "build": "tsc --project tsconfig.build.json",
57
+ "typecheck": "tsc --project tsconfig.json",
58
+ "test": "varlock run --path ../../ -- vitest run"
59
+ }
6
60
  }