@house-rules/relations 0.0.0-stage → 0.1.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.
@@ -0,0 +1,169 @@
1
+ import { Schema } from "effect";
2
+ export declare const NodeRef: Schema.Struct<{
3
+ readonly kind: Schema.NonEmptyString;
4
+ readonly id: Schema.NonEmptyString;
5
+ }>;
6
+ export type NodeRef = typeof NodeRef.Type;
7
+ export declare const RelationDirection: Schema.Literals<readonly ["out", "in", "both"]>;
8
+ export type RelationDirection = typeof RelationDirection.Type;
9
+ export declare const RelationCursor: Schema.Struct<{
10
+ readonly createdAt: Schema.Int;
11
+ readonly id: Schema.NonEmptyString;
12
+ }>;
13
+ export type RelationCursor = typeof RelationCursor.Type;
14
+ export declare const RelationPage: Schema.Struct<{
15
+ readonly size: Schema.optionalKey<Schema.Int>;
16
+ readonly cursor: Schema.optionalKey<Schema.Struct<{
17
+ readonly createdAt: Schema.Int;
18
+ readonly id: Schema.NonEmptyString;
19
+ }>>;
20
+ }>;
21
+ export type RelationPage = typeof RelationPage.Type;
22
+ export declare const Relation: Schema.Struct<{
23
+ readonly id: Schema.NonEmptyString;
24
+ readonly type: Schema.NonEmptyString;
25
+ readonly source: Schema.Struct<{
26
+ readonly kind: Schema.NonEmptyString;
27
+ readonly id: Schema.NonEmptyString;
28
+ }>;
29
+ readonly target: Schema.Struct<{
30
+ readonly kind: Schema.NonEmptyString;
31
+ readonly id: Schema.NonEmptyString;
32
+ }>;
33
+ readonly createdAt: Schema.Int;
34
+ }>;
35
+ export type Relation = typeof Relation.Type;
36
+ export declare const RelateInput: Schema.Struct<{
37
+ readonly type: Schema.NonEmptyString;
38
+ readonly source: Schema.Struct<{
39
+ readonly kind: Schema.NonEmptyString;
40
+ readonly id: Schema.NonEmptyString;
41
+ }>;
42
+ readonly target: Schema.Struct<{
43
+ readonly kind: Schema.NonEmptyString;
44
+ readonly id: Schema.NonEmptyString;
45
+ }>;
46
+ }>;
47
+ export type RelateInput = typeof RelateInput.Type;
48
+ export declare const ListRelationsInput: Schema.Struct<{
49
+ readonly node: Schema.Struct<{
50
+ readonly kind: Schema.NonEmptyString;
51
+ readonly id: Schema.NonEmptyString;
52
+ }>;
53
+ readonly type: Schema.optionalKey<Schema.NonEmptyString>;
54
+ readonly direction: Schema.Literals<readonly ["out", "in", "both"]>;
55
+ readonly page: Schema.optionalKey<Schema.Struct<{
56
+ readonly size: Schema.optionalKey<Schema.Int>;
57
+ readonly cursor: Schema.optionalKey<Schema.Struct<{
58
+ readonly createdAt: Schema.Int;
59
+ readonly id: Schema.NonEmptyString;
60
+ }>>;
61
+ }>>;
62
+ }>;
63
+ export type ListRelationsInput = typeof ListRelationsInput.Type;
64
+ export declare const RelationList: Schema.Struct<{
65
+ readonly edges: Schema.$Array<Schema.Struct<{
66
+ readonly id: Schema.NonEmptyString;
67
+ readonly type: Schema.NonEmptyString;
68
+ readonly source: Schema.Struct<{
69
+ readonly kind: Schema.NonEmptyString;
70
+ readonly id: Schema.NonEmptyString;
71
+ }>;
72
+ readonly target: Schema.Struct<{
73
+ readonly kind: Schema.NonEmptyString;
74
+ readonly id: Schema.NonEmptyString;
75
+ }>;
76
+ readonly createdAt: Schema.Int;
77
+ }>>;
78
+ readonly next: Schema.optionalKey<Schema.Struct<{
79
+ readonly createdAt: Schema.Int;
80
+ readonly id: Schema.NonEmptyString;
81
+ }>>;
82
+ }>;
83
+ export type RelationList = typeof RelationList.Type;
84
+ export declare const SubgraphInput: Schema.Struct<{
85
+ readonly root: Schema.Struct<{
86
+ readonly kind: Schema.NonEmptyString;
87
+ readonly id: Schema.NonEmptyString;
88
+ }>;
89
+ readonly type: Schema.NonEmptyString;
90
+ readonly direction: Schema.Literals<readonly ["out", "in", "both"]>;
91
+ readonly depth: Schema.optionalKey<Schema.Int>;
92
+ }>;
93
+ export type SubgraphInput = typeof SubgraphInput.Type;
94
+ export declare const RelationGraph: Schema.Struct<{
95
+ readonly nodes: Schema.$Array<Schema.Struct<{
96
+ readonly kind: Schema.NonEmptyString;
97
+ readonly id: Schema.NonEmptyString;
98
+ }>>;
99
+ readonly edges: Schema.$Array<Schema.Struct<{
100
+ readonly id: Schema.NonEmptyString;
101
+ readonly type: Schema.NonEmptyString;
102
+ readonly source: Schema.Struct<{
103
+ readonly kind: Schema.NonEmptyString;
104
+ readonly id: Schema.NonEmptyString;
105
+ }>;
106
+ readonly target: Schema.Struct<{
107
+ readonly kind: Schema.NonEmptyString;
108
+ readonly id: Schema.NonEmptyString;
109
+ }>;
110
+ readonly createdAt: Schema.Int;
111
+ }>>;
112
+ }>;
113
+ export type RelationGraph = typeof RelationGraph.Type;
114
+ declare const RelationTypeUnknown_base: Schema.Class<RelationTypeUnknown, Schema.TaggedStruct<"RelationTypeUnknown", {
115
+ readonly kind: Schema.tag<"invalid">;
116
+ }>, import("effect/Cause").YieldableError>;
117
+ export declare class RelationTypeUnknown extends RelationTypeUnknown_base {
118
+ get message(): string;
119
+ }
120
+ declare const RelationEndpointInvalid_base: Schema.Class<RelationEndpointInvalid, Schema.TaggedStruct<"RelationEndpointInvalid", {
121
+ readonly kind: Schema.tag<"invalid">;
122
+ }>, import("effect/Cause").YieldableError>;
123
+ export declare class RelationEndpointInvalid extends RelationEndpointInvalid_base {
124
+ get message(): string;
125
+ }
126
+ declare const RelationsInputInvalid_base: Schema.Class<RelationsInputInvalid, Schema.TaggedStruct<"RelationsInputInvalid", {
127
+ readonly kind: Schema.tag<"invalid">;
128
+ }>, import("effect/Cause").YieldableError>;
129
+ export declare class RelationsInputInvalid extends RelationsInputInvalid_base {
130
+ get message(): string;
131
+ }
132
+ declare const DuplicateRelation_base: Schema.Class<DuplicateRelation, Schema.TaggedStruct<"DuplicateRelation", {
133
+ readonly kind: Schema.tag<"conflict">;
134
+ }>, import("effect/Cause").YieldableError>;
135
+ export declare class DuplicateRelation extends DuplicateRelation_base {
136
+ get message(): string;
137
+ }
138
+ declare const RelationCardinalityExceeded_base: Schema.Class<RelationCardinalityExceeded, Schema.TaggedStruct<"RelationCardinalityExceeded", {
139
+ readonly kind: Schema.tag<"conflict">;
140
+ }>, import("effect/Cause").YieldableError>;
141
+ export declare class RelationCardinalityExceeded extends RelationCardinalityExceeded_base {
142
+ get message(): string;
143
+ }
144
+ declare const RelationCycle_base: Schema.Class<RelationCycle, Schema.TaggedStruct<"RelationCycle", {
145
+ readonly kind: Schema.tag<"conflict">;
146
+ }>, import("effect/Cause").YieldableError>;
147
+ export declare class RelationCycle extends RelationCycle_base {
148
+ get message(): string;
149
+ }
150
+ declare const RelationNotFound_base: Schema.Class<RelationNotFound, Schema.TaggedStruct<"RelationNotFound", {
151
+ readonly kind: Schema.tag<"not_found">;
152
+ }>, import("effect/Cause").YieldableError>;
153
+ export declare class RelationNotFound extends RelationNotFound_base {
154
+ get message(): string;
155
+ }
156
+ declare const GraphTooLarge_base: Schema.Class<GraphTooLarge, Schema.TaggedStruct<"GraphTooLarge", {
157
+ readonly kind: Schema.tag<"invalid">;
158
+ }>, import("effect/Cause").YieldableError>;
159
+ export declare class GraphTooLarge extends GraphTooLarge_base {
160
+ get message(): string;
161
+ }
162
+ declare const RelationsStoreUnavailable_base: Schema.Class<RelationsStoreUnavailable, Schema.TaggedStruct<"RelationsStoreUnavailable", {
163
+ readonly kind: Schema.tag<"unavailable">;
164
+ readonly cause: Schema.Defect;
165
+ }>, import("effect/Cause").YieldableError>;
166
+ export declare class RelationsStoreUnavailable extends RelationsStoreUnavailable_base {
167
+ get message(): string;
168
+ }
169
+ export {};
@@ -0,0 +1,105 @@
1
+ import { errorKind } from "@house-rules/domain/contract";
2
+ import { Schema } from "effect";
3
+ export const NodeRef = Schema.Struct({ kind: Schema.NonEmptyString, id: Schema.NonEmptyString });
4
+ export const RelationDirection = Schema.Literals(["out", "in", "both"]);
5
+ export const RelationCursor = Schema.Struct({ createdAt: Schema.Int, id: Schema.NonEmptyString });
6
+ export const RelationPage = Schema.Struct({
7
+ size: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
8
+ cursor: Schema.optionalKey(RelationCursor),
9
+ });
10
+ export const Relation = Schema.Struct({
11
+ id: Schema.NonEmptyString,
12
+ type: Schema.NonEmptyString,
13
+ source: NodeRef,
14
+ target: NodeRef,
15
+ createdAt: Schema.Int,
16
+ });
17
+ export const RelateInput = Schema.Struct({
18
+ type: Schema.NonEmptyString,
19
+ source: NodeRef,
20
+ target: NodeRef,
21
+ });
22
+ export const ListRelationsInput = Schema.Struct({
23
+ node: NodeRef,
24
+ type: Schema.optionalKey(Schema.NonEmptyString),
25
+ direction: RelationDirection,
26
+ page: Schema.optionalKey(RelationPage),
27
+ });
28
+ export const RelationList = Schema.Struct({
29
+ edges: Schema.Array(Relation),
30
+ next: Schema.optionalKey(RelationCursor),
31
+ });
32
+ export const SubgraphInput = Schema.Struct({
33
+ root: NodeRef,
34
+ type: Schema.NonEmptyString,
35
+ direction: RelationDirection,
36
+ depth: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
37
+ });
38
+ export const RelationGraph = Schema.Struct({
39
+ nodes: Schema.Array(NodeRef),
40
+ edges: Schema.Array(Relation),
41
+ });
42
+ export class RelationTypeUnknown extends Schema.TaggedError()("RelationTypeUnknown", {
43
+ kind: errorKind("invalid"),
44
+ }) {
45
+ get message() {
46
+ return "Unknown relation type.";
47
+ }
48
+ }
49
+ export class RelationEndpointInvalid extends Schema.TaggedError()("RelationEndpointInvalid", {
50
+ kind: errorKind("invalid"),
51
+ }) {
52
+ get message() {
53
+ return "Invalid relation endpoint.";
54
+ }
55
+ }
56
+ export class RelationsInputInvalid extends Schema.TaggedError()("RelationsInputInvalid", {
57
+ kind: errorKind("invalid"),
58
+ }) {
59
+ get message() {
60
+ return "Invalid relations input or options.";
61
+ }
62
+ }
63
+ export class DuplicateRelation extends Schema.TaggedError()("DuplicateRelation", {
64
+ kind: errorKind("conflict"),
65
+ }) {
66
+ get message() {
67
+ return "This relation already exists.";
68
+ }
69
+ }
70
+ export class RelationCardinalityExceeded extends Schema.TaggedError()("RelationCardinalityExceeded", {
71
+ kind: errorKind("conflict"),
72
+ }) {
73
+ get message() {
74
+ return "Relation cardinality exceeded.";
75
+ }
76
+ }
77
+ export class RelationCycle extends Schema.TaggedError()("RelationCycle", {
78
+ kind: errorKind("conflict"),
79
+ }) {
80
+ get message() {
81
+ return "This relation would create a cycle.";
82
+ }
83
+ }
84
+ export class RelationNotFound extends Schema.TaggedError()("RelationNotFound", {
85
+ kind: errorKind("not_found"),
86
+ }) {
87
+ get message() {
88
+ return "Relation not found.";
89
+ }
90
+ }
91
+ export class GraphTooLarge extends Schema.TaggedError()("GraphTooLarge", {
92
+ kind: errorKind("invalid"),
93
+ }) {
94
+ get message() {
95
+ return "The relation graph exceeds its configured size limit.";
96
+ }
97
+ }
98
+ export class RelationsStoreUnavailable extends Schema.TaggedError()("RelationsStoreUnavailable", {
99
+ kind: errorKind("unavailable"),
100
+ cause: Schema.Defect(),
101
+ }) {
102
+ get message() {
103
+ return "Relations are unavailable. Try again shortly.";
104
+ }
105
+ }
@@ -0,0 +1,136 @@
1
+ import { Effect } from "effect";
2
+ import type { RelationStore } from "../storage.js";
3
+ import type { RelationsOptions } from "./options.js";
4
+ import { DuplicateRelation, GraphTooLarge, RelationCardinalityExceeded, RelationCycle, RelationEndpointInvalid, RelationNotFound, RelationsInputInvalid, RelationTypeUnknown } from "./relation.js";
5
+ export declare const makeRelations: (store: RelationStore["Service"], options: RelationsOptions) => {
6
+ relate: (input: {
7
+ readonly type: string;
8
+ readonly source: {
9
+ readonly kind: string;
10
+ readonly id: string;
11
+ };
12
+ readonly target: {
13
+ readonly kind: string;
14
+ readonly id: string;
15
+ };
16
+ }) => Effect.Effect<{
17
+ relation: {
18
+ readonly id: string;
19
+ readonly type: string;
20
+ readonly source: {
21
+ readonly kind: string;
22
+ readonly id: string;
23
+ };
24
+ readonly target: {
25
+ readonly kind: string;
26
+ readonly id: string;
27
+ };
28
+ readonly createdAt: number;
29
+ };
30
+ events: {
31
+ kind: "Related";
32
+ type: string;
33
+ source: {
34
+ kind: string;
35
+ id: string;
36
+ };
37
+ target: {
38
+ kind: string;
39
+ id: string;
40
+ };
41
+ }[];
42
+ }, DuplicateRelation | GraphTooLarge | import("@house-rules/domain").NoOpenUnit | RelationCardinalityExceeded | RelationCycle | RelationEndpointInvalid | RelationTypeUnknown | import("./relation.js").RelationsStoreUnavailable, never>;
43
+ unrelate: (id: string) => Effect.Effect<{
44
+ events: {
45
+ kind: "Unrelated";
46
+ type: string;
47
+ source: {
48
+ kind: string;
49
+ id: string;
50
+ };
51
+ target: {
52
+ kind: string;
53
+ id: string;
54
+ };
55
+ }[];
56
+ }, import("@house-rules/domain").NoOpenUnit | RelationNotFound | import("./relation.js").RelationsStoreUnavailable, never>;
57
+ listRelations: (input: {
58
+ readonly node: {
59
+ readonly kind: string;
60
+ readonly id: string;
61
+ };
62
+ readonly type?: string;
63
+ readonly direction: "both" | "in" | "out";
64
+ readonly page?: {
65
+ readonly size?: number;
66
+ readonly cursor?: {
67
+ readonly createdAt: number;
68
+ readonly id: string;
69
+ };
70
+ };
71
+ }) => Effect.Effect<{
72
+ edges: {
73
+ readonly id: string;
74
+ readonly type: string;
75
+ readonly source: {
76
+ readonly kind: string;
77
+ readonly id: string;
78
+ };
79
+ readonly target: {
80
+ readonly kind: string;
81
+ readonly id: string;
82
+ };
83
+ readonly createdAt: number;
84
+ }[];
85
+ next: {
86
+ createdAt: number;
87
+ id: string;
88
+ };
89
+ } | {
90
+ next?: never;
91
+ edges: {
92
+ readonly id: string;
93
+ readonly type: string;
94
+ readonly source: {
95
+ readonly kind: string;
96
+ readonly id: string;
97
+ };
98
+ readonly target: {
99
+ readonly kind: string;
100
+ readonly id: string;
101
+ };
102
+ readonly createdAt: number;
103
+ }[];
104
+ }, RelationEndpointInvalid | RelationTypeUnknown | RelationsInputInvalid | import("./relation.js").RelationsStoreUnavailable, never>;
105
+ subgraph: (input: {
106
+ readonly root: {
107
+ readonly kind: string;
108
+ readonly id: string;
109
+ };
110
+ readonly type: string;
111
+ readonly direction: "both" | "in" | "out";
112
+ readonly depth?: number;
113
+ }) => Effect.Effect<{
114
+ nodes: {
115
+ kind: string;
116
+ id: string;
117
+ }[];
118
+ edges: {
119
+ readonly id: string;
120
+ readonly type: string;
121
+ readonly source: {
122
+ readonly kind: string;
123
+ readonly id: string;
124
+ };
125
+ readonly target: {
126
+ readonly kind: string;
127
+ readonly id: string;
128
+ };
129
+ readonly createdAt: number;
130
+ }[];
131
+ }, GraphTooLarge | RelationEndpointInvalid | RelationTypeUnknown | RelationsInputInvalid | import("./relation.js").RelationsStoreUnavailable, never>;
132
+ removeNode: (ref: {
133
+ readonly kind: string;
134
+ readonly id: string;
135
+ }) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | RelationEndpointInvalid | import("./relation.js").RelationsStoreUnavailable, never>;
136
+ };
@@ -0,0 +1,135 @@
1
+ import { UnitOfWork } from "@house-rules/domain";
2
+ import { DateTime, Effect, Random, Schema } from "effect";
3
+ import { compareNodes, neighbours, nodeKey, sameNode } from "./graph.js";
4
+ import { DuplicateRelation, GraphTooLarge, ListRelationsInput, NodeRef, RelationCardinalityExceeded, RelationCycle, RelationEndpointInvalid, RelationNotFound, RelationsInputInvalid, RelationTypeUnknown, SubgraphInput, } from "./relation.js";
5
+ export const makeRelations = (store, options) => {
6
+ const invalid = () => new RelationsInputInvalid();
7
+ const decodeNode = (node) => Schema.decodeEffect(Schema.Struct({
8
+ kind: Schema.Literals(options.nodeKinds),
9
+ id: NodeRef.fields.id,
10
+ }))(node).pipe(Effect.mapError(() => new RelationEndpointInvalid()));
11
+ const getType = Effect.fn("Relations.type")(function* (name) {
12
+ const type = options.relationTypes.find((type) => type.name === name);
13
+ if (type === undefined)
14
+ return yield* new RelationTypeUnknown();
15
+ return type;
16
+ });
17
+ const checkCap = Effect.fn("Relations.cardinality")(function* (node, type, direction, cap) {
18
+ if (cap !== undefined && (yield* store.count(node, type, direction)) >= cap)
19
+ return yield* new RelationCardinalityExceeded();
20
+ });
21
+ return {
22
+ relate: Effect.fn("Relations.relate")(function* (input) {
23
+ yield* UnitOfWork.required;
24
+ const type = yield* getType(input.type);
25
+ let source = yield* decodeNode(input.source);
26
+ let target = yield* decodeNode(input.target);
27
+ const forward = type.source.includes(source.kind) && type.target.includes(target.kind);
28
+ const reverse = !type.directed && type.source.includes(target.kind) && type.target.includes(source.kind);
29
+ if (!forward && !reverse)
30
+ return yield* new RelationEndpointInvalid();
31
+ if (!type.directed && compareNodes(source, target) > 0)
32
+ [source, target] = [target, source];
33
+ yield* store.lock(type.name);
34
+ if (type.unique && (yield* store.exists(source, target, type.name)))
35
+ return yield* new DuplicateRelation();
36
+ if (type.directed) {
37
+ yield* checkCap(source, type.name, "out", type.maxOutPerSource);
38
+ yield* checkCap(target, type.name, "in", type.maxInPerTarget);
39
+ }
40
+ else {
41
+ yield* checkCap(source, type.name, "both", type.maxDegree);
42
+ if (!sameNode(source, target))
43
+ yield* checkCap(target, type.name, "both", type.maxDegree);
44
+ }
45
+ if (type.acyclic) {
46
+ const reached = yield* store.reachable(target, type.name, options.maxGraphSize + 1);
47
+ if (reached.length > options.maxGraphSize)
48
+ return yield* new GraphTooLarge();
49
+ if (reached.some((node) => sameNode(source, node)))
50
+ return yield* new RelationCycle();
51
+ }
52
+ const edge = {
53
+ id: yield* Effect.all(Array.from({ length: 4 }, () => Random.nextIntBetween(0, 0xffff_ffff))).pipe(Effect.map((words) => words.map((word) => word.toString(16).padStart(8, "0")).join(""))),
54
+ type: type.name,
55
+ source: { ...source },
56
+ target: { ...target },
57
+ createdAt: DateTime.toEpochMillis(yield* DateTime.now),
58
+ };
59
+ yield* store.insert(edge);
60
+ return {
61
+ relation: edge,
62
+ events: [
63
+ {
64
+ kind: "Related",
65
+ type: edge.type,
66
+ source: { ...edge.source },
67
+ target: { ...edge.target },
68
+ },
69
+ ],
70
+ };
71
+ }),
72
+ unrelate: Effect.fn("Relations.unrelate")(function* (id) {
73
+ yield* UnitOfWork.required;
74
+ const edge = yield* store.remove(id);
75
+ if (edge === undefined)
76
+ return yield* new RelationNotFound();
77
+ return {
78
+ events: [
79
+ {
80
+ kind: "Unrelated",
81
+ type: edge.type,
82
+ source: { ...edge.source },
83
+ target: { ...edge.target },
84
+ },
85
+ ],
86
+ };
87
+ }),
88
+ listRelations: Effect.fn("Relations.listRelations")(function* (input) {
89
+ yield* decodeNode(input.node);
90
+ const decoded = yield* Schema.decodeEffect(ListRelationsInput)(input).pipe(Effect.mapError(invalid));
91
+ if (decoded.type !== undefined)
92
+ yield* getType(decoded.type);
93
+ const size = Math.min(decoded.page?.size ?? options.maxPageSize, options.maxPageSize);
94
+ const rows = yield* store.list(decoded.node, decoded.type, decoded.direction, size + 1, decoded.page?.cursor);
95
+ const edges = rows.slice(0, size);
96
+ const last = edges[edges.length - 1];
97
+ return rows.length > size && last !== undefined
98
+ ? { edges, next: { createdAt: last.createdAt, id: last.id } }
99
+ : { edges };
100
+ }),
101
+ subgraph: Effect.fn("Relations.subgraph")(function* (input) {
102
+ yield* decodeNode(input.root);
103
+ const decoded = yield* Schema.decodeEffect(SubgraphInput)(input).pipe(Effect.mapError(invalid));
104
+ yield* getType(decoded.type);
105
+ const nodes = new Map([[nodeKey(decoded.root), { ...decoded.root }]]);
106
+ const edges = new Map();
107
+ const queue = [{ node: decoded.root, depth: 0 }];
108
+ for (let i = 0; i < queue.length; i++) {
109
+ const current = queue[i];
110
+ if (current === undefined ||
111
+ (decoded.depth !== undefined && current.depth >= decoded.depth))
112
+ continue;
113
+ const adjacent = yield* store.list(current.node, decoded.type, decoded.direction, options.maxGraphSize + 1);
114
+ for (const edge of adjacent) {
115
+ edges.set(edge.id, edge);
116
+ for (const next of neighbours(edge, current.node, decoded.direction)) {
117
+ if (nodes.has(nodeKey(next)))
118
+ continue;
119
+ nodes.set(nodeKey(next), { ...next });
120
+ queue.push({ node: next, depth: current.depth + 1 });
121
+ }
122
+ if (nodes.size > options.maxGraphSize || edges.size > options.maxGraphSize)
123
+ return yield* new GraphTooLarge();
124
+ }
125
+ }
126
+ return { nodes: [...nodes.values()], edges: [...edges.values()] };
127
+ }),
128
+ // Node deletion is a fact owned by the node's module, not an Unrelated event.
129
+ removeNode: Effect.fn("Relations.removeNode")(function* (ref) {
130
+ yield* UnitOfWork.required;
131
+ const node = yield* Schema.decodeEffect(NodeRef)(ref).pipe(Effect.mapError(() => new RelationEndpointInvalid()));
132
+ yield* store.removeNode(node);
133
+ }),
134
+ };
135
+ };
@@ -0,0 +1,18 @@
1
+ import type { NoOpenUnit } from "@house-rules/domain";
2
+ import { Context, type Effect } from "effect";
3
+ import type { NodeRef, Relation, RelationCursor, RelationDirection, RelationsStoreUnavailable } from "./relation/relation.js";
4
+ type Read<A> = Effect.Effect<A, RelationsStoreUnavailable>;
5
+ type Write<A> = Effect.Effect<A, RelationsStoreUnavailable | NoOpenUnit>;
6
+ declare const RelationStore_base: Context.ServiceClass<RelationStore, "@house-rules/relations/RelationStore", {
7
+ readonly lock: (type: string) => Write<void>;
8
+ readonly list: (node: NodeRef, type: string | undefined, direction: RelationDirection, size: number, after?: RelationCursor) => Read<ReadonlyArray<Relation>>;
9
+ readonly count: (node: NodeRef, type: string, direction: RelationDirection) => Read<number>;
10
+ readonly exists: (source: NodeRef, target: NodeRef, type: string) => Read<boolean>;
11
+ readonly reachable: (root: NodeRef, type: string, limit: number) => Read<ReadonlyArray<NodeRef>>;
12
+ readonly insert: (edge: Relation) => Write<void>;
13
+ readonly remove: (id: string) => Write<Relation | undefined>;
14
+ readonly removeNode: (node: NodeRef) => Write<void>;
15
+ }>;
16
+ export declare class RelationStore extends RelationStore_base {
17
+ }
18
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Context } from "effect";
2
+ export class RelationStore extends Context.Service()("@house-rules/relations/RelationStore") {
3
+ }
@@ -0,0 +1,17 @@
1
+ CREATE TABLE relation_edges (
2
+ id text PRIMARY KEY,
3
+ type text NOT NULL,
4
+ source_kind text NOT NULL,
5
+ source_id text NOT NULL,
6
+ target_kind text NOT NULL,
7
+ target_id text NOT NULL,
8
+ created_at bigint NOT NULL
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE INDEX relation_edges_source ON relation_edges (type, source_kind, source_id);
12
+ --> statement-breakpoint
13
+ CREATE INDEX relation_edges_target ON relation_edges (type, target_kind, target_id);
14
+ --> statement-breakpoint
15
+ CREATE INDEX relation_edges_source_page ON relation_edges (source_kind, source_id, created_at, id COLLATE "C");
16
+ --> statement-breakpoint
17
+ CREATE INDEX relation_edges_target_page ON relation_edges (target_kind, target_id, created_at, id COLLATE "C");
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@house-rules/relations",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Typed, non-authority edges between opaque nodes.",
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
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/JakubSzwajka/house-rules.git",
29
+ "directory": "packages/relations"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/JakubSzwajka/house-rules/issues"
33
+ },
34
+ "homepage": "https://github.com/JakubSzwajka/house-rules",
35
+ "houseRules": {
36
+ "migrations": "migrations"
37
+ },
38
+ "peerDependencies": {
39
+ "@house-rules/application": "0.3.0",
40
+ "@house-rules/domain": "0.2.1",
41
+ "effect": "4.0.3"
42
+ },
43
+ "devDependencies": {
44
+ "@effect/platform-node": "4.0.3",
45
+ "@effect/platform-node-shared": "4.0.3",
46
+ "@effect/sql-pg": "4.0.3",
47
+ "@effect/vitest": "4.0.3",
48
+ "@house-rules/application": "0.3.0",
49
+ "@house-rules/domain": "0.2.1",
50
+ "@house-rules/migrations": "0.4.1",
51
+ "effect": "4.0.3",
52
+ "typescript": "7.0.2",
53
+ "vitest": "5.0.1"
54
+ },
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
  }