@medicine-wheel/storage-provider 0.4.13 → 0.5.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.
- package/dist/ceremony-events.d.ts +11 -0
- package/dist/ceremony-events.js +27 -0
- package/dist/diary-records.d.ts +19 -0
- package/dist/diary-records.js +52 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +6 -0
- package/dist/interface.d.ts +132 -0
- package/dist/interface.js +37 -1
- package/dist/jsonl.d.ts +16 -1
- package/dist/jsonl.js +110 -0
- package/dist/neon.d.ts +14 -1
- package/dist/neon.js +192 -0
- package/package.json +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceremony Event filter semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* A ceremony event is a GitHub happening (issue, pull request, merge, commit)
|
|
5
|
+
* witnessed through a ceremonial lens and recorded as a relational bead. The
|
|
6
|
+
* detection and processing logic lives in @medicine-wheel/github-ceremony;
|
|
7
|
+
* here we keep only the record shape's filter matching so jsonl and neon agree.
|
|
8
|
+
*/
|
|
9
|
+
import type { CeremonyEventFilters, CeremonyEventRecord } from './interface.js';
|
|
10
|
+
/** True when a ceremony-event record satisfies every provided filter. */
|
|
11
|
+
export declare function matchesCeremonyEventFilters(record: CeremonyEventRecord, filters: CeremonyEventFilters): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceremony Event filter semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* A ceremony event is a GitHub happening (issue, pull request, merge, commit)
|
|
5
|
+
* witnessed through a ceremonial lens and recorded as a relational bead. The
|
|
6
|
+
* detection and processing logic lives in @medicine-wheel/github-ceremony;
|
|
7
|
+
* here we keep only the record shape's filter matching so jsonl and neon agree.
|
|
8
|
+
*/
|
|
9
|
+
/** True when a ceremony-event record satisfies every provided filter. */
|
|
10
|
+
export function matchesCeremonyEventFilters(record, filters) {
|
|
11
|
+
if (filters.source !== undefined && record.source !== filters.source) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (filters.kind !== undefined && record.kind !== filters.kind) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (filters.phase !== undefined && record.phase !== filters.phase) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (filters.direction !== undefined && record.direction !== filters.direction) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (filters.repository !== undefined && record.repository !== filters.repository) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diary Entry filter + ordering semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* The ceremonial diary is a participant's voice — intention, observation,
|
|
5
|
+
* reflection — written across the Five-Phase ceremonial methodology. The
|
|
6
|
+
* record schema lives with the providers so both the jsonl and neon backends
|
|
7
|
+
* read and write the same shape; the diary domain package
|
|
8
|
+
* (@medicine-wheel/ceremonial-diary) owns creation helpers, pattern detection,
|
|
9
|
+
* and markdown export.
|
|
10
|
+
*/
|
|
11
|
+
import type { DiaryEntryFilters, DiaryEntryRecord } from './interface.js';
|
|
12
|
+
/** True when a record satisfies every provided filter (limit is applied separately). */
|
|
13
|
+
export declare function matchesDiaryEntryFilters(record: DiaryEntryRecord, filters: DiaryEntryFilters): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Filter, order (oldest → newest, mirroring the ceremonial journey), and apply
|
|
16
|
+
* the optional limit. Providers call this after loading their raw records so
|
|
17
|
+
* jsonl and neon return identical result sets.
|
|
18
|
+
*/
|
|
19
|
+
export declare function filterAndOrderDiaryEntries(records: readonly DiaryEntryRecord[], filters?: DiaryEntryFilters): DiaryEntryRecord[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diary Entry filter + ordering semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* The ceremonial diary is a participant's voice — intention, observation,
|
|
5
|
+
* reflection — written across the Five-Phase ceremonial methodology. The
|
|
6
|
+
* record schema lives with the providers so both the jsonl and neon backends
|
|
7
|
+
* read and write the same shape; the diary domain package
|
|
8
|
+
* (@medicine-wheel/ceremonial-diary) owns creation helpers, pattern detection,
|
|
9
|
+
* and markdown export.
|
|
10
|
+
*/
|
|
11
|
+
/** True when a record satisfies every provided filter (limit is applied separately). */
|
|
12
|
+
export function matchesDiaryEntryFilters(record, filters) {
|
|
13
|
+
if (filters.participant !== undefined && record.participant !== filters.participant) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (filters.phase !== undefined && record.phase !== filters.phase) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (filters.entryType !== undefined && record.entryType !== filters.entryType) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (filters.dateRange !== undefined) {
|
|
23
|
+
const entryTime = Date.parse(record.timestamp);
|
|
24
|
+
const startTime = Date.parse(filters.dateRange.start);
|
|
25
|
+
const endTime = Date.parse(filters.dateRange.end);
|
|
26
|
+
if (entryTime < startTime || entryTime > endTime) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (filters.tags !== undefined && filters.tags.length > 0) {
|
|
31
|
+
const entryTags = record.metadata?.tags ?? [];
|
|
32
|
+
const hasTag = filters.tags.some((tag) => entryTags.includes(tag));
|
|
33
|
+
if (!hasTag) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Filter, order (oldest → newest, mirroring the ceremonial journey), and apply
|
|
41
|
+
* the optional limit. Providers call this after loading their raw records so
|
|
42
|
+
* jsonl and neon return identical result sets.
|
|
43
|
+
*/
|
|
44
|
+
export function filterAndOrderDiaryEntries(records, filters = {}) {
|
|
45
|
+
const filtered = records
|
|
46
|
+
.filter((record) => matchesDiaryEntryFilters(record, filters))
|
|
47
|
+
.sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp));
|
|
48
|
+
if (typeof filters.limit === 'number' && filters.limit >= 0) {
|
|
49
|
+
return filtered.slice(0, filters.limit);
|
|
50
|
+
}
|
|
51
|
+
return filtered;
|
|
52
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveEpisode, PlanPerspectiveFilters, ProviderType, } from './interface.js';
|
|
18
|
+
export type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, WeaveSyncState, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveEpisode, PlanPerspectiveFilters, CeremonialPhase, DiaryEntryType, DiaryEntryLocation, DiaryEntryMetadata, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventKind, CeremonyEventParticipant, CeremonyEventRecord, CeremonyEventFilters, ProviderType, NodePatch, EdgePatch, } from './interface.js';
|
|
19
|
+
export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
19
20
|
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
21
|
+
export { matchesDiaryEntryFilters, filterAndOrderDiaryEntries, } from './diary-records.js';
|
|
22
|
+
export { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
20
23
|
export { createProvider, detectProvider } from './factory.js';
|
|
21
24
|
export { JsonlProvider } from './jsonl.js';
|
|
22
25
|
export { NeonProvider } from './neon.js';
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
+
// Typed errors (thrown by update/delete operations)
|
|
19
|
+
export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
18
20
|
// Plan Perspective merge/filter semantics (shared across providers)
|
|
19
21
|
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
22
|
+
// Ceremonial Diary filter/ordering semantics (shared across providers)
|
|
23
|
+
export { matchesDiaryEntryFilters, filterAndOrderDiaryEntries, } from './diary-records.js';
|
|
24
|
+
// Ceremony Event filter semantics (shared across providers)
|
|
25
|
+
export { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
20
26
|
// Factory
|
|
21
27
|
export { createProvider, detectProvider } from './factory.js';
|
|
22
28
|
// Providers (for direct instantiation)
|
package/dist/interface.d.ts
CHANGED
|
@@ -13,6 +13,42 @@ export interface RelationalEdge extends Omit<OntologyRelationalEdge, 'id'> {
|
|
|
13
13
|
last_ceremony?: string;
|
|
14
14
|
}
|
|
15
15
|
export type CeremonyLog = OntologyCeremonyLog;
|
|
16
|
+
/** Partial update for a node. `direction: null` releases the node from its direction. */
|
|
17
|
+
export interface NodePatch {
|
|
18
|
+
name?: string;
|
|
19
|
+
type?: NodeType;
|
|
20
|
+
description?: string;
|
|
21
|
+
direction?: DirectionName | null;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
/** Partial update for an edge (identified by from_id + to_id). */
|
|
25
|
+
export interface EdgePatch {
|
|
26
|
+
relationship_type?: string;
|
|
27
|
+
strength?: number;
|
|
28
|
+
ceremony_honored?: boolean;
|
|
29
|
+
obligations?: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare class NodeNotFoundError extends Error {
|
|
32
|
+
readonly nodeId: string;
|
|
33
|
+
readonly code = "NODE_NOT_FOUND";
|
|
34
|
+
constructor(nodeId: string);
|
|
35
|
+
}
|
|
36
|
+
export declare class EdgeNotFoundError extends Error {
|
|
37
|
+
readonly fromId: string;
|
|
38
|
+
readonly toId: string;
|
|
39
|
+
readonly code = "EDGE_NOT_FOUND";
|
|
40
|
+
constructor(fromId: string, toId: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Deleting a node with living relations is refused: relational accountability
|
|
44
|
+
* means severing each relation consciously before releasing the node.
|
|
45
|
+
*/
|
|
46
|
+
export declare class NodeHasRelationsError extends Error {
|
|
47
|
+
readonly nodeId: string;
|
|
48
|
+
readonly relationCount: number;
|
|
49
|
+
readonly code = "NODE_HAS_RELATIONS";
|
|
50
|
+
constructor(nodeId: string, relationCount: number);
|
|
51
|
+
}
|
|
16
52
|
export type WeaveSyncState = 'never-synced' | 'in-sync' | 'stale' | 'episode-copy-diverged';
|
|
17
53
|
export interface WeaveRecord extends Record<string, unknown> {
|
|
18
54
|
id: string;
|
|
@@ -101,6 +137,87 @@ export interface PlanPerspectiveFilters {
|
|
|
101
137
|
session_id?: string;
|
|
102
138
|
id?: string;
|
|
103
139
|
}
|
|
140
|
+
export type CeremonialPhase = 'miigwechiwendam' | 'nindokendaan' | 'ningwaab' | 'nindoodam' | 'migwech';
|
|
141
|
+
export type DiaryEntryType = 'intention' | 'observation' | 'hypothesis' | 'data' | 'synthesis' | 'action' | 'reflection' | 'learning';
|
|
142
|
+
export interface DiaryEntryLocation {
|
|
143
|
+
lat: number;
|
|
144
|
+
lon: number;
|
|
145
|
+
name?: string;
|
|
146
|
+
}
|
|
147
|
+
export interface DiaryEntryMetadata {
|
|
148
|
+
location?: DiaryEntryLocation;
|
|
149
|
+
activity?: 'walking' | 'sitting' | 'coding' | 'reflecting' | string;
|
|
150
|
+
weather?: string;
|
|
151
|
+
emotionalTone?: string;
|
|
152
|
+
/** Related stories, sessions, or other entry keys. */
|
|
153
|
+
relatedKeys?: string[];
|
|
154
|
+
tags?: string[];
|
|
155
|
+
[key: string]: unknown;
|
|
156
|
+
}
|
|
157
|
+
export interface DiaryEntryRecord extends Record<string, unknown> {
|
|
158
|
+
id: string;
|
|
159
|
+
/** ISO 8601 timestamp. */
|
|
160
|
+
timestamp: string;
|
|
161
|
+
/** Author identity — the participant whose voice this entry carries. */
|
|
162
|
+
participant: string;
|
|
163
|
+
/** Which agent facilitated this entry, when applicable. */
|
|
164
|
+
agent?: string;
|
|
165
|
+
phase: CeremonialPhase;
|
|
166
|
+
entryType: DiaryEntryType;
|
|
167
|
+
/** Main diary text; supports Markdown. */
|
|
168
|
+
content: string;
|
|
169
|
+
metadata: DiaryEntryMetadata;
|
|
170
|
+
/**
|
|
171
|
+
* Optional relation into the chronicle. Medicine-wheel node-id convention is
|
|
172
|
+
* `chronicle:<episode-folder-name>` (a chronicle_episode node under parent
|
|
173
|
+
* `chronicle:miadi-chronicle`). Entries without a chronicle reference remain
|
|
174
|
+
* fully valid — the diary can speak whether or not it writes into an episode.
|
|
175
|
+
*/
|
|
176
|
+
chronicle?: string;
|
|
177
|
+
}
|
|
178
|
+
export interface DiaryEntryFilters {
|
|
179
|
+
participant?: string;
|
|
180
|
+
phase?: CeremonialPhase;
|
|
181
|
+
entryType?: DiaryEntryType;
|
|
182
|
+
dateRange?: {
|
|
183
|
+
start: string;
|
|
184
|
+
end: string;
|
|
185
|
+
};
|
|
186
|
+
tags?: string[];
|
|
187
|
+
limit?: number;
|
|
188
|
+
}
|
|
189
|
+
export type CeremonyEventKind = 'issue' | 'pr' | 'merge' | 'commit';
|
|
190
|
+
export interface CeremonyEventParticipant {
|
|
191
|
+
name: string;
|
|
192
|
+
role: string;
|
|
193
|
+
perspective: 'indigenous' | 'western' | 'both';
|
|
194
|
+
}
|
|
195
|
+
export interface CeremonyEventRecord extends Record<string, unknown> {
|
|
196
|
+
id: string;
|
|
197
|
+
/** Origin of the event — currently 'github'. */
|
|
198
|
+
source: string;
|
|
199
|
+
kind: CeremonyEventKind;
|
|
200
|
+
phase: CeremonialPhase;
|
|
201
|
+
/** Four-Directions bridge derived from the ceremonial phase. */
|
|
202
|
+
direction?: DirectionName;
|
|
203
|
+
participants: CeremonyEventParticipant[];
|
|
204
|
+
relationshipImpacts: string[];
|
|
205
|
+
/** Spiral key preserved from Miadi semantics (non-Redis, deterministic). */
|
|
206
|
+
spiralKey: string;
|
|
207
|
+
/** ISO 8601 timestamp. */
|
|
208
|
+
timestamp: string;
|
|
209
|
+
repository?: string;
|
|
210
|
+
/** Issue/PR number or short commit sha. */
|
|
211
|
+
reference?: string | number;
|
|
212
|
+
metadata?: Record<string, unknown>;
|
|
213
|
+
}
|
|
214
|
+
export interface CeremonyEventFilters {
|
|
215
|
+
source?: string;
|
|
216
|
+
kind?: CeremonyEventKind;
|
|
217
|
+
phase?: CeremonialPhase;
|
|
218
|
+
direction?: DirectionName;
|
|
219
|
+
repository?: string;
|
|
220
|
+
}
|
|
104
221
|
export interface StorageProvider {
|
|
105
222
|
readonly name: string;
|
|
106
223
|
connect(): Promise<void>;
|
|
@@ -110,11 +227,19 @@ export interface StorageProvider {
|
|
|
110
227
|
getNodesByType(type: NodeType): Promise<RelationalNode[]>;
|
|
111
228
|
getNodesByDirection(direction: DirectionName): Promise<RelationalNode[]>;
|
|
112
229
|
getAllNodes(limit?: number): Promise<RelationalNode[]>;
|
|
230
|
+
/** Throws NodeNotFoundError when the node does not exist. */
|
|
231
|
+
updateNode(id: string, patch: NodePatch): Promise<RelationalNode>;
|
|
232
|
+
/** Throws NodeNotFoundError or NodeHasRelationsError (refusal, never cascade). */
|
|
233
|
+
deleteNode(id: string): Promise<void>;
|
|
113
234
|
createEdge(edge: RelationalEdge): Promise<void>;
|
|
114
235
|
getEdge(fromId: string, toId: string): Promise<RelationalEdge | null>;
|
|
115
236
|
getAllEdges(limit?: number): Promise<RelationalEdge[]>;
|
|
116
237
|
getRelatedNodes(nodeId: string, direction?: 'from' | 'to' | 'both'): Promise<string[]>;
|
|
117
238
|
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): Promise<void>;
|
|
239
|
+
/** Throws EdgeNotFoundError when no relation exists between the pair. */
|
|
240
|
+
updateEdge(fromId: string, toId: string, patch: EdgePatch): Promise<RelationalEdge>;
|
|
241
|
+
/** Throws EdgeNotFoundError when no relation exists between the pair. */
|
|
242
|
+
deleteEdge(fromId: string, toId: string): Promise<void>;
|
|
118
243
|
logCeremony(ceremony: CeremonyLog): Promise<void>;
|
|
119
244
|
getCeremony(id: string): Promise<CeremonyLog | null>;
|
|
120
245
|
getCeremoniesTimeline(limit?: number): Promise<CeremonyLog[]>;
|
|
@@ -127,5 +252,12 @@ export interface StorageProvider {
|
|
|
127
252
|
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
128
253
|
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
129
254
|
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
255
|
+
registerDiaryEntry(record: DiaryEntryRecord): Promise<DiaryEntryRecord>;
|
|
256
|
+
getDiaryEntry(id: string): Promise<DiaryEntryRecord | null>;
|
|
257
|
+
listDiaryEntries(filters?: DiaryEntryFilters): Promise<DiaryEntryRecord[]>;
|
|
258
|
+
deleteDiaryEntry(id: string): Promise<void>;
|
|
259
|
+
registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
|
|
260
|
+
getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
|
|
261
|
+
listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
|
|
130
262
|
}
|
|
131
263
|
export type ProviderType = 'jsonl' | 'neon' | 'redis' | 'auto';
|
package/dist/interface.js
CHANGED
|
@@ -4,4 +4,40 @@
|
|
|
4
4
|
* Abstract interface for Medicine Wheel data persistence.
|
|
5
5
|
* Implementations: JsonlProvider (local/default), NeonProvider (Postgres), RedisProvider (future)
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
// ── Typed Errors ──
|
|
8
|
+
// Providers throw these so API layers can answer with honest status codes.
|
|
9
|
+
export class NodeNotFoundError extends Error {
|
|
10
|
+
nodeId;
|
|
11
|
+
code = 'NODE_NOT_FOUND';
|
|
12
|
+
constructor(nodeId) {
|
|
13
|
+
super(`Node not found: ${nodeId}`);
|
|
14
|
+
this.nodeId = nodeId;
|
|
15
|
+
this.name = 'NodeNotFoundError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class EdgeNotFoundError extends Error {
|
|
19
|
+
fromId;
|
|
20
|
+
toId;
|
|
21
|
+
code = 'EDGE_NOT_FOUND';
|
|
22
|
+
constructor(fromId, toId) {
|
|
23
|
+
super(`Relation not found between ${fromId} and ${toId}`);
|
|
24
|
+
this.fromId = fromId;
|
|
25
|
+
this.toId = toId;
|
|
26
|
+
this.name = 'EdgeNotFoundError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Deleting a node with living relations is refused: relational accountability
|
|
31
|
+
* means severing each relation consciously before releasing the node.
|
|
32
|
+
*/
|
|
33
|
+
export class NodeHasRelationsError extends Error {
|
|
34
|
+
nodeId;
|
|
35
|
+
relationCount;
|
|
36
|
+
code = 'NODE_HAS_RELATIONS';
|
|
37
|
+
constructor(nodeId, relationCount) {
|
|
38
|
+
super(`This node holds ${relationCount} relation${relationCount === 1 ? '' : 's'} — release them first`);
|
|
39
|
+
this.nodeId = nodeId;
|
|
40
|
+
this.relationCount = relationCount;
|
|
41
|
+
this.name = 'NodeHasRelationsError';
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/jsonl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DirectionName, NodeType, CeremonyType } from '@medicine-wheel/ontology-core';
|
|
2
|
-
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters } from './interface.js';
|
|
2
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, NodePatch, EdgePatch } from './interface.js';
|
|
3
3
|
export declare class JsonlProvider implements StorageProvider {
|
|
4
4
|
readonly name = "jsonl";
|
|
5
5
|
readonly dataDir: string;
|
|
@@ -8,6 +8,8 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
8
8
|
private readonly ceremoniesFile;
|
|
9
9
|
private readonly inquiryWeavesFile;
|
|
10
10
|
private readonly planPerspectivesFile;
|
|
11
|
+
private readonly diaryEntriesFile;
|
|
12
|
+
private readonly ceremonyEventsFile;
|
|
11
13
|
constructor(dataDir?: string);
|
|
12
14
|
connect(): Promise<void>;
|
|
13
15
|
disconnect(): Promise<void>;
|
|
@@ -16,11 +18,15 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
16
18
|
getNodesByType(type: NodeType): Promise<RelationalNode[]>;
|
|
17
19
|
getNodesByDirection(direction: DirectionName): Promise<RelationalNode[]>;
|
|
18
20
|
getAllNodes(limit?: number): Promise<RelationalNode[]>;
|
|
21
|
+
updateNode(id: string, patch: NodePatch): Promise<RelationalNode>;
|
|
22
|
+
deleteNode(id: string): Promise<void>;
|
|
19
23
|
createEdge(edge: RelationalEdge): Promise<void>;
|
|
20
24
|
getEdge(fromId: string, toId: string): Promise<RelationalEdge | null>;
|
|
21
25
|
getRelatedNodes(nodeId: string, direction?: 'from' | 'to' | 'both'): Promise<string[]>;
|
|
22
26
|
getAllEdges(limit?: number): Promise<RelationalEdge[]>;
|
|
23
27
|
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): Promise<void>;
|
|
28
|
+
updateEdge(fromId: string, toId: string, patch: EdgePatch): Promise<RelationalEdge>;
|
|
29
|
+
deleteEdge(fromId: string, toId: string): Promise<void>;
|
|
24
30
|
logCeremony(ceremony: CeremonyLog): Promise<void>;
|
|
25
31
|
getCeremony(id: string): Promise<CeremonyLog | null>;
|
|
26
32
|
getCeremoniesTimeline(limit?: number): Promise<CeremonyLog[]>;
|
|
@@ -33,11 +39,20 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
33
39
|
registerPlanPerspective(record: PlanPerspectiveRecord): Promise<PlanPerspectiveRecord>;
|
|
34
40
|
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
35
41
|
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
42
|
+
registerDiaryEntry(record: DiaryEntryRecord): Promise<DiaryEntryRecord>;
|
|
43
|
+
getDiaryEntry(id: string): Promise<DiaryEntryRecord | null>;
|
|
44
|
+
listDiaryEntries(filters?: DiaryEntryFilters): Promise<DiaryEntryRecord[]>;
|
|
45
|
+
deleteDiaryEntry(id: string): Promise<void>;
|
|
46
|
+
registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
|
|
47
|
+
getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
|
|
48
|
+
listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
|
|
36
49
|
private readNodes;
|
|
37
50
|
private readEdges;
|
|
38
51
|
private readCeremonies;
|
|
39
52
|
private readInquiryWeaves;
|
|
40
53
|
private readPlanPerspectives;
|
|
54
|
+
private readDiaryEntries;
|
|
55
|
+
private readCeremonyEvents;
|
|
41
56
|
private upsertById;
|
|
42
57
|
private upsertEdge;
|
|
43
58
|
private parseNode;
|
package/dist/jsonl.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
|
+
import { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
4
5
|
import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
6
|
+
import { filterAndOrderDiaryEntries } from './diary-records.js';
|
|
7
|
+
import { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
5
8
|
export class JsonlProvider {
|
|
6
9
|
name = 'jsonl';
|
|
7
10
|
dataDir;
|
|
@@ -10,6 +13,8 @@ export class JsonlProvider {
|
|
|
10
13
|
ceremoniesFile;
|
|
11
14
|
inquiryWeavesFile;
|
|
12
15
|
planPerspectivesFile;
|
|
16
|
+
diaryEntriesFile;
|
|
17
|
+
ceremonyEventsFile;
|
|
13
18
|
constructor(dataDir) {
|
|
14
19
|
this.dataDir = path.resolve(dataDir ?? resolveProjectDataDir());
|
|
15
20
|
this.nodesFile = path.join(this.dataDir, 'nodes.jsonl');
|
|
@@ -17,6 +22,8 @@ export class JsonlProvider {
|
|
|
17
22
|
this.ceremoniesFile = path.join(this.dataDir, 'ceremonies.jsonl');
|
|
18
23
|
this.inquiryWeavesFile = path.join(this.dataDir, 'inquiry-weaves.jsonl');
|
|
19
24
|
this.planPerspectivesFile = path.join(this.dataDir, 'plan-perspectives.jsonl');
|
|
25
|
+
this.diaryEntriesFile = path.join(this.dataDir, 'diary-entries.jsonl');
|
|
26
|
+
this.ceremonyEventsFile = path.join(this.dataDir, 'ceremony-events.jsonl');
|
|
20
27
|
}
|
|
21
28
|
async connect() {
|
|
22
29
|
ensureDirectory(this.dataDir);
|
|
@@ -49,6 +56,42 @@ export class JsonlProvider {
|
|
|
49
56
|
.slice(0, limit)
|
|
50
57
|
.map((node) => this.parseNode(node));
|
|
51
58
|
}
|
|
59
|
+
async updateNode(id, patch) {
|
|
60
|
+
return withWriteLock(this.nodesFile, () => {
|
|
61
|
+
const nodes = this.readNodes();
|
|
62
|
+
const index = nodes.findIndex((candidate) => candidate.id === id);
|
|
63
|
+
if (index === -1)
|
|
64
|
+
throw new NodeNotFoundError(id);
|
|
65
|
+
const current = nodes[index];
|
|
66
|
+
const next = {
|
|
67
|
+
...current,
|
|
68
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
69
|
+
...(patch.type !== undefined ? { type: patch.type } : {}),
|
|
70
|
+
...(patch.description !== undefined ? { description: patch.description } : {}),
|
|
71
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
72
|
+
updated_at: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
if (patch.direction !== undefined) {
|
|
75
|
+
next.direction = patch.direction === null ? undefined : patch.direction;
|
|
76
|
+
}
|
|
77
|
+
nodes[index] = next;
|
|
78
|
+
writeJsonl(this.nodesFile, nodes);
|
|
79
|
+
return this.parseNode(next);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async deleteNode(id) {
|
|
83
|
+
await withWriteLock(this.nodesFile, () => {
|
|
84
|
+
const nodes = this.readNodes();
|
|
85
|
+
if (!nodes.some((candidate) => candidate.id === id)) {
|
|
86
|
+
throw new NodeNotFoundError(id);
|
|
87
|
+
}
|
|
88
|
+
const relationCount = this.readEdges().filter((edge) => edge.from_id === id || edge.to_id === id).length;
|
|
89
|
+
if (relationCount > 0) {
|
|
90
|
+
throw new NodeHasRelationsError(id, relationCount);
|
|
91
|
+
}
|
|
92
|
+
writeJsonl(this.nodesFile, nodes.filter((candidate) => candidate.id !== id));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
52
95
|
async createEdge(edge) {
|
|
53
96
|
await this.upsertEdge({
|
|
54
97
|
...edge,
|
|
@@ -92,6 +135,38 @@ export class JsonlProvider {
|
|
|
92
135
|
writeJsonl(this.edgesFile, nextEdges);
|
|
93
136
|
});
|
|
94
137
|
}
|
|
138
|
+
async updateEdge(fromId, toId, patch) {
|
|
139
|
+
return withWriteLock(this.edgesFile, () => {
|
|
140
|
+
const edges = this.readEdges();
|
|
141
|
+
const index = edges.findIndex((candidate) => candidate.from_id === fromId && candidate.to_id === toId);
|
|
142
|
+
if (index === -1)
|
|
143
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
144
|
+
const next = {
|
|
145
|
+
...edges[index],
|
|
146
|
+
...(patch.relationship_type !== undefined
|
|
147
|
+
? { relationship_type: patch.relationship_type }
|
|
148
|
+
: {}),
|
|
149
|
+
...(patch.strength !== undefined ? { strength: patch.strength } : {}),
|
|
150
|
+
...(patch.ceremony_honored !== undefined
|
|
151
|
+
? { ceremony_honored: patch.ceremony_honored }
|
|
152
|
+
: {}),
|
|
153
|
+
...(patch.obligations !== undefined ? { obligations: patch.obligations } : {}),
|
|
154
|
+
};
|
|
155
|
+
edges[index] = next;
|
|
156
|
+
writeJsonl(this.edgesFile, edges);
|
|
157
|
+
return this.parseEdge(next);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async deleteEdge(fromId, toId) {
|
|
161
|
+
await withWriteLock(this.edgesFile, () => {
|
|
162
|
+
const edges = this.readEdges();
|
|
163
|
+
const remaining = edges.filter((candidate) => !(candidate.from_id === fromId && candidate.to_id === toId));
|
|
164
|
+
if (remaining.length === edges.length) {
|
|
165
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
166
|
+
}
|
|
167
|
+
writeJsonl(this.edgesFile, remaining);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
95
170
|
async logCeremony(ceremony) {
|
|
96
171
|
await this.upsertById(this.ceremoniesFile, ceremony);
|
|
97
172
|
}
|
|
@@ -150,6 +225,34 @@ export class JsonlProvider {
|
|
|
150
225
|
async listPlanPerspectives(filters = {}) {
|
|
151
226
|
return this.readPlanPerspectives().filter((record) => matchesPlanPerspectiveFilters(record, filters));
|
|
152
227
|
}
|
|
228
|
+
async registerDiaryEntry(record) {
|
|
229
|
+
await this.upsertById(this.diaryEntriesFile, record);
|
|
230
|
+
return record;
|
|
231
|
+
}
|
|
232
|
+
async getDiaryEntry(id) {
|
|
233
|
+
return this.readDiaryEntries().find((record) => record.id === id) ?? null;
|
|
234
|
+
}
|
|
235
|
+
async listDiaryEntries(filters = {}) {
|
|
236
|
+
return filterAndOrderDiaryEntries(this.readDiaryEntries(), filters);
|
|
237
|
+
}
|
|
238
|
+
async deleteDiaryEntry(id) {
|
|
239
|
+
await withWriteLock(this.diaryEntriesFile, () => {
|
|
240
|
+
const remaining = this.readDiaryEntries().filter((record) => record.id !== id);
|
|
241
|
+
writeJsonl(this.diaryEntriesFile, remaining);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async registerCeremonyEvent(record) {
|
|
245
|
+
await this.upsertById(this.ceremonyEventsFile, record);
|
|
246
|
+
return record;
|
|
247
|
+
}
|
|
248
|
+
async getCeremonyEvent(id) {
|
|
249
|
+
return this.readCeremonyEvents().find((record) => record.id === id) ?? null;
|
|
250
|
+
}
|
|
251
|
+
async listCeremonyEvents(filters = {}) {
|
|
252
|
+
return this.readCeremonyEvents()
|
|
253
|
+
.filter((record) => matchesCeremonyEventFilters(record, filters))
|
|
254
|
+
.sort(sortByNewest('timestamp'));
|
|
255
|
+
}
|
|
153
256
|
readNodes() {
|
|
154
257
|
return readJsonl(this.nodesFile);
|
|
155
258
|
}
|
|
@@ -165,6 +268,12 @@ export class JsonlProvider {
|
|
|
165
268
|
readPlanPerspectives() {
|
|
166
269
|
return readJsonl(this.planPerspectivesFile);
|
|
167
270
|
}
|
|
271
|
+
readDiaryEntries() {
|
|
272
|
+
return readJsonl(this.diaryEntriesFile);
|
|
273
|
+
}
|
|
274
|
+
readCeremonyEvents() {
|
|
275
|
+
return readJsonl(this.ceremonyEventsFile);
|
|
276
|
+
}
|
|
168
277
|
async upsertById(filePath, item) {
|
|
169
278
|
await withWriteLock(filePath, () => {
|
|
170
279
|
const nextRecords = readJsonl(filePath).filter((candidate) => candidate.id !== item.id);
|
|
@@ -193,6 +302,7 @@ export class JsonlProvider {
|
|
|
193
302
|
}
|
|
194
303
|
parseEdge(edge) {
|
|
195
304
|
return {
|
|
305
|
+
id: edge.id ?? `${edge.from_id}:${edge.to_id}`,
|
|
196
306
|
from_id: edge.from_id,
|
|
197
307
|
to_id: edge.to_id,
|
|
198
308
|
relationship_type: edge.relationship_type,
|
package/dist/neon.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Neon (Postgres) Storage Provider
|
|
3
3
|
*/
|
|
4
4
|
import type { DirectionName, NodeType, CeremonyType } from '@medicine-wheel/ontology-core';
|
|
5
|
-
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters } from './interface.js';
|
|
5
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, NodePatch, EdgePatch } from './interface.js';
|
|
6
6
|
export declare class NeonProvider implements StorageProvider {
|
|
7
7
|
readonly name = "neon";
|
|
8
8
|
private sql;
|
|
@@ -14,12 +14,16 @@ export declare class NeonProvider implements StorageProvider {
|
|
|
14
14
|
getNodesByType(type: NodeType): Promise<RelationalNode[]>;
|
|
15
15
|
getNodesByDirection(direction: DirectionName): Promise<RelationalNode[]>;
|
|
16
16
|
getAllNodes(limit?: number): Promise<RelationalNode[]>;
|
|
17
|
+
updateNode(id: string, patch: NodePatch): Promise<RelationalNode>;
|
|
18
|
+
deleteNode(id: string): Promise<void>;
|
|
17
19
|
private parseNode;
|
|
18
20
|
createEdge(edge: RelationalEdge): Promise<void>;
|
|
19
21
|
getEdge(fromId: string, toId: string): Promise<RelationalEdge | null>;
|
|
20
22
|
getRelatedNodes(nodeId: string, direction?: 'from' | 'to' | 'both'): Promise<string[]>;
|
|
21
23
|
getAllEdges(limit?: number): Promise<RelationalEdge[]>;
|
|
22
24
|
updateEdgeCeremony(fromId: string, toId: string, ceremonyId: string): Promise<void>;
|
|
25
|
+
updateEdge(fromId: string, toId: string, patch: EdgePatch): Promise<RelationalEdge>;
|
|
26
|
+
deleteEdge(fromId: string, toId: string): Promise<void>;
|
|
23
27
|
private parseEdge;
|
|
24
28
|
logCeremony(ceremony: CeremonyLog): Promise<void>;
|
|
25
29
|
getCeremony(id: string): Promise<CeremonyLog | null>;
|
|
@@ -37,4 +41,13 @@ export declare class NeonProvider implements StorageProvider {
|
|
|
37
41
|
getPlanPerspective(id: string): Promise<PlanPerspectiveRecord | null>;
|
|
38
42
|
listPlanPerspectives(filters?: PlanPerspectiveFilters): Promise<PlanPerspectiveRecord[]>;
|
|
39
43
|
private ensurePlanPerspectivesTable;
|
|
44
|
+
registerDiaryEntry(record: DiaryEntryRecord): Promise<DiaryEntryRecord>;
|
|
45
|
+
getDiaryEntry(id: string): Promise<DiaryEntryRecord | null>;
|
|
46
|
+
listDiaryEntries(filters?: DiaryEntryFilters): Promise<DiaryEntryRecord[]>;
|
|
47
|
+
deleteDiaryEntry(id: string): Promise<void>;
|
|
48
|
+
private ensureDiaryEntriesTable;
|
|
49
|
+
registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
|
|
50
|
+
getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
|
|
51
|
+
listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
|
|
52
|
+
private ensureCeremonyEventsTable;
|
|
40
53
|
}
|
package/dist/neon.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Neon (Postgres) Storage Provider
|
|
3
3
|
*/
|
|
4
|
+
import { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
4
5
|
import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
6
|
+
import { filterAndOrderDiaryEntries } from './diary-records.js';
|
|
7
|
+
import { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
5
8
|
export class NeonProvider {
|
|
6
9
|
name = 'neon';
|
|
7
10
|
sql = null;
|
|
@@ -13,6 +16,8 @@ export class NeonProvider {
|
|
|
13
16
|
this.sql = neon(url);
|
|
14
17
|
await this.ensureInquiryWeavesTable();
|
|
15
18
|
await this.ensurePlanPerspectivesTable();
|
|
19
|
+
await this.ensureDiaryEntriesTable();
|
|
20
|
+
await this.ensureCeremonyEventsTable();
|
|
16
21
|
}
|
|
17
22
|
async disconnect() {
|
|
18
23
|
this.sql = null;
|
|
@@ -56,6 +61,45 @@ export class NeonProvider {
|
|
|
56
61
|
const rows = await this.db `SELECT * FROM nodes ORDER BY created_at DESC LIMIT ${limit}`;
|
|
57
62
|
return rows.map((row) => this.parseNode(row));
|
|
58
63
|
}
|
|
64
|
+
async updateNode(id, patch) {
|
|
65
|
+
const existing = await this.getNode(id);
|
|
66
|
+
if (!existing)
|
|
67
|
+
throw new NodeNotFoundError(id);
|
|
68
|
+
const next = {
|
|
69
|
+
...existing,
|
|
70
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
71
|
+
...(patch.type !== undefined ? { type: patch.type } : {}),
|
|
72
|
+
...(patch.description !== undefined ? { description: patch.description } : {}),
|
|
73
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
74
|
+
updated_at: new Date().toISOString(),
|
|
75
|
+
};
|
|
76
|
+
if (patch.direction !== undefined) {
|
|
77
|
+
next.direction = patch.direction === null ? undefined : patch.direction;
|
|
78
|
+
}
|
|
79
|
+
await this.db `
|
|
80
|
+
UPDATE nodes SET
|
|
81
|
+
name = ${next.name},
|
|
82
|
+
type = ${next.type},
|
|
83
|
+
description = ${next.description ?? ''},
|
|
84
|
+
direction = ${next.direction || null},
|
|
85
|
+
metadata = ${JSON.stringify(next.metadata)},
|
|
86
|
+
updated_at = ${next.updated_at}
|
|
87
|
+
WHERE id = ${id}
|
|
88
|
+
`;
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
async deleteNode(id) {
|
|
92
|
+
const existing = await this.db `SELECT id FROM nodes WHERE id = ${id}`;
|
|
93
|
+
if (existing.length === 0)
|
|
94
|
+
throw new NodeNotFoundError(id);
|
|
95
|
+
const counts = await this.db `
|
|
96
|
+
SELECT COUNT(*)::int AS count FROM edges WHERE from_id = ${id} OR to_id = ${id}
|
|
97
|
+
`;
|
|
98
|
+
const relationCount = Number(counts[0]?.count ?? 0);
|
|
99
|
+
if (relationCount > 0)
|
|
100
|
+
throw new NodeHasRelationsError(id, relationCount);
|
|
101
|
+
await this.db `DELETE FROM nodes WHERE id = ${id}`;
|
|
102
|
+
}
|
|
59
103
|
parseNode(row) {
|
|
60
104
|
return {
|
|
61
105
|
id: row.id,
|
|
@@ -115,8 +159,41 @@ export class NeonProvider {
|
|
|
115
159
|
WHERE from_id = ${fromId} AND to_id = ${toId}
|
|
116
160
|
`;
|
|
117
161
|
}
|
|
162
|
+
async updateEdge(fromId, toId, patch) {
|
|
163
|
+
const existing = await this.getEdge(fromId, toId);
|
|
164
|
+
if (!existing)
|
|
165
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
166
|
+
const next = {
|
|
167
|
+
...existing,
|
|
168
|
+
...(patch.relationship_type !== undefined
|
|
169
|
+
? { relationship_type: patch.relationship_type }
|
|
170
|
+
: {}),
|
|
171
|
+
...(patch.strength !== undefined ? { strength: patch.strength } : {}),
|
|
172
|
+
...(patch.ceremony_honored !== undefined
|
|
173
|
+
? { ceremony_honored: patch.ceremony_honored }
|
|
174
|
+
: {}),
|
|
175
|
+
...(patch.obligations !== undefined ? { obligations: patch.obligations } : {}),
|
|
176
|
+
};
|
|
177
|
+
await this.db `
|
|
178
|
+
UPDATE edges SET
|
|
179
|
+
relationship_type = ${next.relationship_type},
|
|
180
|
+
strength = ${next.strength},
|
|
181
|
+
ceremony_honored = ${next.ceremony_honored},
|
|
182
|
+
obligations = ${JSON.stringify(next.obligations)}
|
|
183
|
+
WHERE from_id = ${fromId} AND to_id = ${toId}
|
|
184
|
+
`;
|
|
185
|
+
return next;
|
|
186
|
+
}
|
|
187
|
+
async deleteEdge(fromId, toId) {
|
|
188
|
+
const deleted = await this.db `
|
|
189
|
+
DELETE FROM edges WHERE from_id = ${fromId} AND to_id = ${toId} RETURNING from_id
|
|
190
|
+
`;
|
|
191
|
+
if (deleted.length === 0)
|
|
192
|
+
throw new EdgeNotFoundError(fromId, toId);
|
|
193
|
+
}
|
|
118
194
|
parseEdge(row) {
|
|
119
195
|
return {
|
|
196
|
+
id: `${row.from_id}:${row.to_id}`,
|
|
120
197
|
from_id: row.from_id,
|
|
121
198
|
to_id: row.to_id,
|
|
122
199
|
relationship_type: row.relationship_type,
|
|
@@ -297,6 +374,121 @@ export class NeonProvider {
|
|
|
297
374
|
await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_session_id ON plan_perspectives(session_id)`;
|
|
298
375
|
await this.db `CREATE INDEX IF NOT EXISTS idx_plan_perspectives_episode_paths ON plan_perspectives USING GIN(episode_paths)`;
|
|
299
376
|
}
|
|
377
|
+
// ── Ceremonial Diary Operations ──
|
|
378
|
+
async registerDiaryEntry(record) {
|
|
379
|
+
await this.db `
|
|
380
|
+
INSERT INTO diary_entries (id, payload, participant, phase, entry_type, chronicle, timestamp, updated_at)
|
|
381
|
+
VALUES (
|
|
382
|
+
${record.id},
|
|
383
|
+
${JSON.stringify(record)},
|
|
384
|
+
${record.participant},
|
|
385
|
+
${record.phase},
|
|
386
|
+
${record.entryType},
|
|
387
|
+
${record.chronicle ?? null},
|
|
388
|
+
${record.timestamp},
|
|
389
|
+
NOW()
|
|
390
|
+
)
|
|
391
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
392
|
+
payload = EXCLUDED.payload,
|
|
393
|
+
participant = EXCLUDED.participant,
|
|
394
|
+
phase = EXCLUDED.phase,
|
|
395
|
+
entry_type = EXCLUDED.entry_type,
|
|
396
|
+
chronicle = EXCLUDED.chronicle,
|
|
397
|
+
timestamp = EXCLUDED.timestamp,
|
|
398
|
+
updated_at = NOW()
|
|
399
|
+
`;
|
|
400
|
+
return record;
|
|
401
|
+
}
|
|
402
|
+
async getDiaryEntry(id) {
|
|
403
|
+
const rows = await this.db `SELECT payload FROM diary_entries WHERE id = ${id}`;
|
|
404
|
+
if (rows.length === 0)
|
|
405
|
+
return null;
|
|
406
|
+
return parseJsonValue(rows[0].payload, null);
|
|
407
|
+
}
|
|
408
|
+
async listDiaryEntries(filters = {}) {
|
|
409
|
+
const rows = await this.db `SELECT payload FROM diary_entries`;
|
|
410
|
+
const records = rows
|
|
411
|
+
.map((row) => parseJsonValue(row.payload, null))
|
|
412
|
+
.filter((record) => record !== null);
|
|
413
|
+
return filterAndOrderDiaryEntries(records, filters);
|
|
414
|
+
}
|
|
415
|
+
async deleteDiaryEntry(id) {
|
|
416
|
+
await this.db `DELETE FROM diary_entries WHERE id = ${id}`;
|
|
417
|
+
}
|
|
418
|
+
async ensureDiaryEntriesTable() {
|
|
419
|
+
await this.db `
|
|
420
|
+
CREATE TABLE IF NOT EXISTS diary_entries (
|
|
421
|
+
id TEXT PRIMARY KEY,
|
|
422
|
+
payload JSONB NOT NULL,
|
|
423
|
+
participant TEXT NOT NULL,
|
|
424
|
+
phase TEXT NOT NULL,
|
|
425
|
+
entry_type TEXT NOT NULL,
|
|
426
|
+
chronicle TEXT,
|
|
427
|
+
timestamp TIMESTAMPTZ NOT NULL,
|
|
428
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
429
|
+
)
|
|
430
|
+
`;
|
|
431
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_diary_entries_participant ON diary_entries(participant)`;
|
|
432
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_diary_entries_phase ON diary_entries(phase)`;
|
|
433
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_diary_entries_chronicle ON diary_entries(chronicle)`;
|
|
434
|
+
}
|
|
435
|
+
// ── Ceremony Event Operations ──
|
|
436
|
+
async registerCeremonyEvent(record) {
|
|
437
|
+
await this.db `
|
|
438
|
+
INSERT INTO ceremony_events (id, payload, source, kind, phase, direction, repository, timestamp, updated_at)
|
|
439
|
+
VALUES (
|
|
440
|
+
${record.id},
|
|
441
|
+
${JSON.stringify(record)},
|
|
442
|
+
${record.source},
|
|
443
|
+
${record.kind},
|
|
444
|
+
${record.phase},
|
|
445
|
+
${record.direction ?? null},
|
|
446
|
+
${record.repository ?? null},
|
|
447
|
+
${record.timestamp},
|
|
448
|
+
NOW()
|
|
449
|
+
)
|
|
450
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
451
|
+
payload = EXCLUDED.payload,
|
|
452
|
+
source = EXCLUDED.source,
|
|
453
|
+
kind = EXCLUDED.kind,
|
|
454
|
+
phase = EXCLUDED.phase,
|
|
455
|
+
direction = EXCLUDED.direction,
|
|
456
|
+
repository = EXCLUDED.repository,
|
|
457
|
+
timestamp = EXCLUDED.timestamp,
|
|
458
|
+
updated_at = NOW()
|
|
459
|
+
`;
|
|
460
|
+
return record;
|
|
461
|
+
}
|
|
462
|
+
async getCeremonyEvent(id) {
|
|
463
|
+
const rows = await this.db `SELECT payload FROM ceremony_events WHERE id = ${id}`;
|
|
464
|
+
if (rows.length === 0)
|
|
465
|
+
return null;
|
|
466
|
+
return parseJsonValue(rows[0].payload, null);
|
|
467
|
+
}
|
|
468
|
+
async listCeremonyEvents(filters = {}) {
|
|
469
|
+
const rows = await this.db `SELECT payload FROM ceremony_events ORDER BY timestamp DESC`;
|
|
470
|
+
return rows
|
|
471
|
+
.map((row) => parseJsonValue(row.payload, null))
|
|
472
|
+
.filter((record) => record !== null && matchesCeremonyEventFilters(record, filters));
|
|
473
|
+
}
|
|
474
|
+
async ensureCeremonyEventsTable() {
|
|
475
|
+
await this.db `
|
|
476
|
+
CREATE TABLE IF NOT EXISTS ceremony_events (
|
|
477
|
+
id TEXT PRIMARY KEY,
|
|
478
|
+
payload JSONB NOT NULL,
|
|
479
|
+
source TEXT NOT NULL,
|
|
480
|
+
kind TEXT NOT NULL,
|
|
481
|
+
phase TEXT NOT NULL,
|
|
482
|
+
direction TEXT,
|
|
483
|
+
repository TEXT,
|
|
484
|
+
timestamp TIMESTAMPTZ NOT NULL,
|
|
485
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
486
|
+
)
|
|
487
|
+
`;
|
|
488
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_kind ON ceremony_events(kind)`;
|
|
489
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_phase ON ceremony_events(phase)`;
|
|
490
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_repository ON ceremony_events(repository)`;
|
|
491
|
+
}
|
|
300
492
|
}
|
|
301
493
|
function getConnectionString() {
|
|
302
494
|
return process.env.DATABASE_URL ?? process.env.POSTGRES_URL ?? process.env.NEON_DATABASE_URL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medicine-wheel/storage-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@neondatabase/serverless": "^0.10.0",
|
|
18
18
|
"@upstash/redis": "^1.34.3",
|
|
19
|
-
"@medicine-wheel/ontology-core": "^0.
|
|
19
|
+
"@medicine-wheel/ontology-core": "^0.5.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.7.0"
|