@medicine-wheel/storage-provider 0.5.9 → 0.5.10
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/capture-records.d.ts +38 -0
- package/dist/capture-records.js +77 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -0
- package/dist/interface.d.ts +67 -0
- package/dist/interface.js +20 -0
- package/dist/jsonl.d.ts +6 -1
- package/dist/jsonl.js +24 -0
- package/dist/neon.d.ts +5 -1
- package/dist/neon.js +66 -0
- package/package.json +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Record semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* The registry holds records and URIs — where a take lives — never the bytes
|
|
5
|
+
* themselves, which stay behind the capture service that made them. Capture is
|
|
6
|
+
* owned by @miadi/capture and the gmtermux edge; here we keep only the id
|
|
7
|
+
* convention, filter predicate, ordering, and upsert-merge rule so the jsonl
|
|
8
|
+
* and neon backends agree on what a capture means.
|
|
9
|
+
*
|
|
10
|
+
* Vocabulary law: capture-vocabulary.spec.md (§8, the registry noun ruling).
|
|
11
|
+
*/
|
|
12
|
+
import type { CaptureFilters, CaptureRecord } from './interface.js';
|
|
13
|
+
/**
|
|
14
|
+
* Compose the upsert key for a capture, mirroring @miadi/inquiry-weave's
|
|
15
|
+
* weaveRecordId composition: `capture:<episode_path>:<filename>` when the
|
|
16
|
+
* capture is episode-bound, `capture:<filename>` otherwise.
|
|
17
|
+
*
|
|
18
|
+
* This convention applies only when no id is sent — it is a fallback for
|
|
19
|
+
* id-less registration, not the identity rule. The id assigned at first
|
|
20
|
+
* registration is permanent (capture-vocabulary.spec.md §6).
|
|
21
|
+
*/
|
|
22
|
+
export declare function captureRecordId(filename: string, episodePath?: string): string;
|
|
23
|
+
/** True when a capture record satisfies every provided filter. */
|
|
24
|
+
export declare function matchesCaptureFilters(record: CaptureRecord, filters: CaptureFilters): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Filter a batch of capture rows, dropping anything that did not survive the
|
|
27
|
+
* read (a Postgres payload that fails to parse arrives here as null), then
|
|
28
|
+
* order newest registration first.
|
|
29
|
+
*/
|
|
30
|
+
export declare function filterAndOrderCaptures(records: readonly (CaptureRecord | null | undefined)[], filters?: CaptureFilters): CaptureRecord[];
|
|
31
|
+
/**
|
|
32
|
+
* Upsert merge: the incoming record wins wherever it speaks, the existing
|
|
33
|
+
* record fills wherever it stays silent. Provenance is observed, not demanded
|
|
34
|
+
* — so a later registration that learned less (no device, no sha256) must not
|
|
35
|
+
* erase what an earlier one knew. The first registration timestamp survives,
|
|
36
|
+
* matching the plan-perspective merge contract.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mergeCaptureRecords(existing: CaptureRecord, incoming: CaptureRecord): CaptureRecord;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Record semantics shared by every provider.
|
|
3
|
+
*
|
|
4
|
+
* The registry holds records and URIs — where a take lives — never the bytes
|
|
5
|
+
* themselves, which stay behind the capture service that made them. Capture is
|
|
6
|
+
* owned by @miadi/capture and the gmtermux edge; here we keep only the id
|
|
7
|
+
* convention, filter predicate, ordering, and upsert-merge rule so the jsonl
|
|
8
|
+
* and neon backends agree on what a capture means.
|
|
9
|
+
*
|
|
10
|
+
* Vocabulary law: capture-vocabulary.spec.md (§8, the registry noun ruling).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Compose the upsert key for a capture, mirroring @miadi/inquiry-weave's
|
|
14
|
+
* weaveRecordId composition: `capture:<episode_path>:<filename>` when the
|
|
15
|
+
* capture is episode-bound, `capture:<filename>` otherwise.
|
|
16
|
+
*
|
|
17
|
+
* This convention applies only when no id is sent — it is a fallback for
|
|
18
|
+
* id-less registration, not the identity rule. The id assigned at first
|
|
19
|
+
* registration is permanent (capture-vocabulary.spec.md §6).
|
|
20
|
+
*/
|
|
21
|
+
export function captureRecordId(filename, episodePath) {
|
|
22
|
+
return episodePath ? `capture:${episodePath}:${filename}` : `capture:${filename}`;
|
|
23
|
+
}
|
|
24
|
+
/** True when a capture record satisfies every provided filter. */
|
|
25
|
+
export function matchesCaptureFilters(record, filters) {
|
|
26
|
+
if (filters.episode_path !== undefined && record.episode_path !== filters.episode_path) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (filters.episode_number !== undefined && record.episode_number !== filters.episode_number) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (filters.composition !== undefined && record.composition !== filters.composition) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (filters.kind !== undefined && record.kind !== filters.kind) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (filters.origin !== undefined && record.origin !== filters.origin) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (filters.device !== undefined && record.device !== filters.device) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (filters.filename !== undefined && record.filename !== filters.filename) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Filter a batch of capture rows, dropping anything that did not survive the
|
|
51
|
+
* read (a Postgres payload that fails to parse arrives here as null), then
|
|
52
|
+
* order newest registration first.
|
|
53
|
+
*/
|
|
54
|
+
export function filterAndOrderCaptures(records, filters = {}) {
|
|
55
|
+
return records
|
|
56
|
+
.filter((record) => record !== null &&
|
|
57
|
+
record !== undefined &&
|
|
58
|
+
typeof record === 'object' &&
|
|
59
|
+
matchesCaptureFilters(record, filters))
|
|
60
|
+
.sort((left, right) => Date.parse(right.registered_at) - Date.parse(left.registered_at));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Upsert merge: the incoming record wins wherever it speaks, the existing
|
|
64
|
+
* record fills wherever it stays silent. Provenance is observed, not demanded
|
|
65
|
+
* — so a later registration that learned less (no device, no sha256) must not
|
|
66
|
+
* erase what an earlier one knew. The first registration timestamp survives,
|
|
67
|
+
* matching the plan-perspective merge contract.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeCaptureRecords(existing, incoming) {
|
|
70
|
+
const merged = { ...existing };
|
|
71
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
72
|
+
if (value !== undefined)
|
|
73
|
+
merged[key] = value;
|
|
74
|
+
}
|
|
75
|
+
merged.registered_at = existing.registered_at;
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,12 +15,14 @@
|
|
|
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, CeremonialPhase, DiaryEntryType, DiaryEntryLocation, DiaryEntryMetadata, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventKind, CeremonyEventParticipant, CeremonyEventRecord, CeremonyEventFilters, ProviderType, NodePatch, EdgePatch, } 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, CaptureKind, CaptureOrigin, CaptureRecord, CaptureFilters, ProviderType, NodePatch, EdgePatch, } from './interface.js';
|
|
19
|
+
export { CAPTURE_KINDS, CAPTURE_ORIGINS } from './interface.js';
|
|
19
20
|
export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
20
21
|
export { mergePlanPerspectiveRecords, unionPlanPerspectiveEpisodes, matchesPlanPerspectiveFilters, } from './plan-perspectives.js';
|
|
21
22
|
export { matchesInquiryWeaveFilters, filterInquiryWeaves, } from './inquiry-weaves.js';
|
|
22
23
|
export { matchesDiaryEntryFilters, filterAndOrderDiaryEntries, } from './diary-records.js';
|
|
23
24
|
export { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
25
|
+
export { captureRecordId, matchesCaptureFilters, filterAndOrderCaptures, mergeCaptureRecords, } from './capture-records.js';
|
|
24
26
|
export { createProvider, detectProvider } from './factory.js';
|
|
25
27
|
export { JsonlProvider } from './jsonl.js';
|
|
26
28
|
export { NeonProvider } from './neon.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* const node = await store.getNode('elder-1');
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
+
// Capture vocabulary as values — checkable from plain JavaScript
|
|
19
|
+
export { CAPTURE_KINDS, CAPTURE_ORIGINS } from './interface.js';
|
|
18
20
|
// Typed errors (thrown by update/delete operations)
|
|
19
21
|
export { NodeNotFoundError, EdgeNotFoundError, NodeHasRelationsError, } from './interface.js';
|
|
20
22
|
// Plan Perspective merge/filter semantics (shared across providers)
|
|
@@ -25,6 +27,8 @@ export { matchesInquiryWeaveFilters, filterInquiryWeaves, } from './inquiry-weav
|
|
|
25
27
|
export { matchesDiaryEntryFilters, filterAndOrderDiaryEntries, } from './diary-records.js';
|
|
26
28
|
// Ceremony Event filter semantics (shared across providers)
|
|
27
29
|
export { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
30
|
+
// Capture id/filter/merge semantics (shared across providers)
|
|
31
|
+
export { captureRecordId, matchesCaptureFilters, filterAndOrderCaptures, mergeCaptureRecords, } from './capture-records.js';
|
|
28
32
|
// Factory
|
|
29
33
|
export { createProvider, detectProvider } from './factory.js';
|
|
30
34
|
// Providers (for direct instantiation)
|
package/dist/interface.d.ts
CHANGED
|
@@ -218,6 +218,70 @@ export interface CeremonyEventFilters {
|
|
|
218
218
|
direction?: DirectionName;
|
|
219
219
|
repository?: string;
|
|
220
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Kinds are published as a value, not only a type — a plain-JavaScript edge
|
|
223
|
+
* device cannot import a TypeScript union, so the array is the checkable
|
|
224
|
+
* contract and the type derives FROM it.
|
|
225
|
+
*/
|
|
226
|
+
export declare const CAPTURE_KINDS: readonly ["audio", "video", "midi", "other"];
|
|
227
|
+
export type CaptureKind = (typeof CAPTURE_KINDS)[number];
|
|
228
|
+
/**
|
|
229
|
+
* How a capture came to exist — the axis that says what must never be lost.
|
|
230
|
+
* `captured` is unrepeatable: a device, an instant, one chance. `derived` is
|
|
231
|
+
* regenerable from a source still held. `authored` was written rather than
|
|
232
|
+
* captured or generated. Flattening these loses the only property that
|
|
233
|
+
* distinguishes an irreplaceable take from a file rebuildable on demand.
|
|
234
|
+
*/
|
|
235
|
+
export declare const CAPTURE_ORIGINS: readonly ["captured", "derived", "authored"];
|
|
236
|
+
export type CaptureOrigin = (typeof CAPTURE_ORIGINS)[number];
|
|
237
|
+
export interface CaptureRecord extends Record<string, unknown> {
|
|
238
|
+
/**
|
|
239
|
+
* Stable upsert key. Convention: `capture:<episode_path>:<filename>` when
|
|
240
|
+
* episode-bound, `capture:<filename>` otherwise — see captureRecordId().
|
|
241
|
+
*/
|
|
242
|
+
id: string;
|
|
243
|
+
/** The file's own name, as the capture service or author named it. */
|
|
244
|
+
filename: string;
|
|
245
|
+
kind: CaptureKind;
|
|
246
|
+
origin: CaptureOrigin;
|
|
247
|
+
/**
|
|
248
|
+
* Where the bytes live — a URI or path the capture service answers for.
|
|
249
|
+
* The registry stores this pointer and nothing behind it.
|
|
250
|
+
*/
|
|
251
|
+
uri: string;
|
|
252
|
+
/** The device that made the capture, as it named itself. */
|
|
253
|
+
device?: string;
|
|
254
|
+
/** ISO 8601 instant the capture began. */
|
|
255
|
+
started_at?: string;
|
|
256
|
+
/** ISO 8601 instant the capture finished. */
|
|
257
|
+
stopped_at?: string;
|
|
258
|
+
/** Duration of the finished take, in seconds. */
|
|
259
|
+
duration_seconds?: number;
|
|
260
|
+
/** Size of the finished file in bytes — a count, never the bytes themselves. */
|
|
261
|
+
bytes?: number;
|
|
262
|
+
sha256?: string;
|
|
263
|
+
mimetype?: string;
|
|
264
|
+
/** Episode directory path relative to the chronicle root. */
|
|
265
|
+
episode_path?: string;
|
|
266
|
+
episode_number?: number;
|
|
267
|
+
/** Composition slug this capture belongs to, when known. */
|
|
268
|
+
composition?: string;
|
|
269
|
+
/** For derived captures: the filename or record id this one was made from. */
|
|
270
|
+
source_artifact?: string;
|
|
271
|
+
/** ISO 8601 registration timestamp; the first registration's stamp survives upserts. */
|
|
272
|
+
registered_at: string;
|
|
273
|
+
/** Free-form origin-of-registration, e.g. '@miadi/capture' or 'gmtermux'. */
|
|
274
|
+
source?: string;
|
|
275
|
+
}
|
|
276
|
+
export interface CaptureFilters {
|
|
277
|
+
episode_path?: string;
|
|
278
|
+
episode_number?: number;
|
|
279
|
+
composition?: string;
|
|
280
|
+
kind?: CaptureKind;
|
|
281
|
+
origin?: CaptureOrigin;
|
|
282
|
+
device?: string;
|
|
283
|
+
filename?: string;
|
|
284
|
+
}
|
|
221
285
|
export interface StorageProvider {
|
|
222
286
|
readonly name: string;
|
|
223
287
|
connect(): Promise<void>;
|
|
@@ -259,5 +323,8 @@ export interface StorageProvider {
|
|
|
259
323
|
registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
|
|
260
324
|
getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
|
|
261
325
|
listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
|
|
326
|
+
registerCapture(record: CaptureRecord): Promise<CaptureRecord>;
|
|
327
|
+
getCapture(id: string): Promise<CaptureRecord | null>;
|
|
328
|
+
listCaptures(filters?: CaptureFilters): Promise<CaptureRecord[]>;
|
|
262
329
|
}
|
|
263
330
|
export type ProviderType = 'jsonl' | 'neon' | 'redis' | 'auto';
|
package/dist/interface.js
CHANGED
|
@@ -41,3 +41,23 @@ export class NodeHasRelationsError extends Error {
|
|
|
41
41
|
this.name = 'NodeHasRelationsError';
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
// ── Capture Records ──
|
|
45
|
+
// A registry of captures: records and URIs only, never bytes. The bytes stay
|
|
46
|
+
// behind the capture service that made them (@miadi/capture and the gmtermux
|
|
47
|
+
// edge); this registry makes captures queryable for chronicle surfaces.
|
|
48
|
+
// Vocabulary aligns with @miadi/episodic-memory-schema's artifact axes and is
|
|
49
|
+
// ruled by capture-vocabulary.spec.md (§8, the registry noun ruling).
|
|
50
|
+
/**
|
|
51
|
+
* Kinds are published as a value, not only a type — a plain-JavaScript edge
|
|
52
|
+
* device cannot import a TypeScript union, so the array is the checkable
|
|
53
|
+
* contract and the type derives FROM it.
|
|
54
|
+
*/
|
|
55
|
+
export const CAPTURE_KINDS = ['audio', 'video', 'midi', 'other'];
|
|
56
|
+
/**
|
|
57
|
+
* How a capture came to exist — the axis that says what must never be lost.
|
|
58
|
+
* `captured` is unrepeatable: a device, an instant, one chance. `derived` is
|
|
59
|
+
* regenerable from a source still held. `authored` was written rather than
|
|
60
|
+
* captured or generated. Flattening these loses the only property that
|
|
61
|
+
* distinguishes an irreplaceable take from a file rebuildable on demand.
|
|
62
|
+
*/
|
|
63
|
+
export const CAPTURE_ORIGINS = ['captured', 'derived', 'authored'];
|
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, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, NodePatch, EdgePatch } from './interface.js';
|
|
2
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, CaptureRecord, CaptureFilters, NodePatch, EdgePatch } from './interface.js';
|
|
3
3
|
export declare class JsonlProvider implements StorageProvider {
|
|
4
4
|
readonly name = "jsonl";
|
|
5
5
|
readonly dataDir: string;
|
|
@@ -10,6 +10,7 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
10
10
|
private readonly planPerspectivesFile;
|
|
11
11
|
private readonly diaryEntriesFile;
|
|
12
12
|
private readonly ceremonyEventsFile;
|
|
13
|
+
private readonly capturesFile;
|
|
13
14
|
constructor(dataDir?: string);
|
|
14
15
|
connect(): Promise<void>;
|
|
15
16
|
disconnect(): Promise<void>;
|
|
@@ -46,6 +47,9 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
46
47
|
registerCeremonyEvent(record: CeremonyEventRecord): Promise<CeremonyEventRecord>;
|
|
47
48
|
getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
|
|
48
49
|
listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
|
|
50
|
+
registerCapture(record: CaptureRecord): Promise<CaptureRecord>;
|
|
51
|
+
getCapture(id: string): Promise<CaptureRecord | null>;
|
|
52
|
+
listCaptures(filters?: CaptureFilters): Promise<CaptureRecord[]>;
|
|
49
53
|
private readNodes;
|
|
50
54
|
private readEdges;
|
|
51
55
|
private readCeremonies;
|
|
@@ -53,6 +57,7 @@ export declare class JsonlProvider implements StorageProvider {
|
|
|
53
57
|
private readPlanPerspectives;
|
|
54
58
|
private readDiaryEntries;
|
|
55
59
|
private readCeremonyEvents;
|
|
60
|
+
private readCaptures;
|
|
56
61
|
private upsertById;
|
|
57
62
|
private upsertEdge;
|
|
58
63
|
private parseNode;
|
package/dist/jsonl.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './p
|
|
|
6
6
|
import { filterInquiryWeaves } from './inquiry-weaves.js';
|
|
7
7
|
import { filterAndOrderDiaryEntries } from './diary-records.js';
|
|
8
8
|
import { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
9
|
+
import { mergeCaptureRecords, filterAndOrderCaptures, } from './capture-records.js';
|
|
9
10
|
export class JsonlProvider {
|
|
10
11
|
name = 'jsonl';
|
|
11
12
|
dataDir;
|
|
@@ -16,6 +17,7 @@ export class JsonlProvider {
|
|
|
16
17
|
planPerspectivesFile;
|
|
17
18
|
diaryEntriesFile;
|
|
18
19
|
ceremonyEventsFile;
|
|
20
|
+
capturesFile;
|
|
19
21
|
constructor(dataDir) {
|
|
20
22
|
this.dataDir = path.resolve(dataDir ?? resolveProjectDataDir());
|
|
21
23
|
this.nodesFile = path.join(this.dataDir, 'nodes.jsonl');
|
|
@@ -25,6 +27,7 @@ export class JsonlProvider {
|
|
|
25
27
|
this.planPerspectivesFile = path.join(this.dataDir, 'plan-perspectives.jsonl');
|
|
26
28
|
this.diaryEntriesFile = path.join(this.dataDir, 'diary-entries.jsonl');
|
|
27
29
|
this.ceremonyEventsFile = path.join(this.dataDir, 'ceremony-events.jsonl');
|
|
30
|
+
this.capturesFile = path.join(this.dataDir, 'captures.jsonl');
|
|
28
31
|
}
|
|
29
32
|
async connect() {
|
|
30
33
|
ensureDirectory(this.dataDir);
|
|
@@ -254,6 +257,24 @@ export class JsonlProvider {
|
|
|
254
257
|
.filter((record) => matchesCeremonyEventFilters(record, filters))
|
|
255
258
|
.sort(sortByNewest('timestamp'));
|
|
256
259
|
}
|
|
260
|
+
async registerCapture(record) {
|
|
261
|
+
let merged = record;
|
|
262
|
+
await withWriteLock(this.capturesFile, () => {
|
|
263
|
+
const records = readJsonl(this.capturesFile);
|
|
264
|
+
const existing = records.find((candidate) => candidate.id === record.id);
|
|
265
|
+
merged = existing ? mergeCaptureRecords(existing, record) : record;
|
|
266
|
+
const nextRecords = records.filter((candidate) => candidate.id !== record.id);
|
|
267
|
+
nextRecords.push(merged);
|
|
268
|
+
writeJsonl(this.capturesFile, nextRecords);
|
|
269
|
+
});
|
|
270
|
+
return merged;
|
|
271
|
+
}
|
|
272
|
+
async getCapture(id) {
|
|
273
|
+
return this.readCaptures().find((record) => record.id === id) ?? null;
|
|
274
|
+
}
|
|
275
|
+
async listCaptures(filters = {}) {
|
|
276
|
+
return filterAndOrderCaptures(this.readCaptures(), filters);
|
|
277
|
+
}
|
|
257
278
|
readNodes() {
|
|
258
279
|
return readJsonl(this.nodesFile);
|
|
259
280
|
}
|
|
@@ -275,6 +296,9 @@ export class JsonlProvider {
|
|
|
275
296
|
readCeremonyEvents() {
|
|
276
297
|
return readJsonl(this.ceremonyEventsFile);
|
|
277
298
|
}
|
|
299
|
+
readCaptures() {
|
|
300
|
+
return readJsonl(this.capturesFile);
|
|
301
|
+
}
|
|
278
302
|
async upsertById(filePath, item) {
|
|
279
303
|
await withWriteLock(filePath, () => {
|
|
280
304
|
const nextRecords = readJsonl(filePath).filter((candidate) => candidate.id !== item.id);
|
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, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, NodePatch, EdgePatch } from './interface.js';
|
|
5
|
+
import type { StorageProvider, RelationalNode, RelationalEdge, CeremonyLog, WeaveRecord, InquiryWeaveFilters, PlanPerspectiveRecord, PlanPerspectiveFilters, DiaryEntryRecord, DiaryEntryFilters, CeremonyEventRecord, CeremonyEventFilters, CaptureRecord, CaptureFilters, NodePatch, EdgePatch } from './interface.js';
|
|
6
6
|
export declare class NeonProvider implements StorageProvider {
|
|
7
7
|
readonly name = "neon";
|
|
8
8
|
private sql;
|
|
@@ -50,4 +50,8 @@ export declare class NeonProvider implements StorageProvider {
|
|
|
50
50
|
getCeremonyEvent(id: string): Promise<CeremonyEventRecord | null>;
|
|
51
51
|
listCeremonyEvents(filters?: CeremonyEventFilters): Promise<CeremonyEventRecord[]>;
|
|
52
52
|
private ensureCeremonyEventsTable;
|
|
53
|
+
registerCapture(record: CaptureRecord): Promise<CaptureRecord>;
|
|
54
|
+
getCapture(id: string): Promise<CaptureRecord | null>;
|
|
55
|
+
listCaptures(filters?: CaptureFilters): Promise<CaptureRecord[]>;
|
|
56
|
+
private ensureCapturesTable;
|
|
53
57
|
}
|
package/dist/neon.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mergePlanPerspectiveRecords, matchesPlanPerspectiveFilters, } from './p
|
|
|
6
6
|
import { filterInquiryWeaves } from './inquiry-weaves.js';
|
|
7
7
|
import { filterAndOrderDiaryEntries } from './diary-records.js';
|
|
8
8
|
import { matchesCeremonyEventFilters } from './ceremony-events.js';
|
|
9
|
+
import { mergeCaptureRecords, filterAndOrderCaptures, } from './capture-records.js';
|
|
9
10
|
export class NeonProvider {
|
|
10
11
|
name = 'neon';
|
|
11
12
|
sql = null;
|
|
@@ -19,6 +20,7 @@ export class NeonProvider {
|
|
|
19
20
|
await this.ensurePlanPerspectivesTable();
|
|
20
21
|
await this.ensureDiaryEntriesTable();
|
|
21
22
|
await this.ensureCeremonyEventsTable();
|
|
23
|
+
await this.ensureCapturesTable();
|
|
22
24
|
}
|
|
23
25
|
async disconnect() {
|
|
24
26
|
this.sql = null;
|
|
@@ -489,6 +491,70 @@ export class NeonProvider {
|
|
|
489
491
|
await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_phase ON ceremony_events(phase)`;
|
|
490
492
|
await this.db `CREATE INDEX IF NOT EXISTS idx_ceremony_events_repository ON ceremony_events(repository)`;
|
|
491
493
|
}
|
|
494
|
+
// ── Capture Operations (records + URIs, never bytes) ──
|
|
495
|
+
async registerCapture(record) {
|
|
496
|
+
const existing = await this.getCapture(record.id);
|
|
497
|
+
const merged = existing ? mergeCaptureRecords(existing, record) : record;
|
|
498
|
+
await this.db `
|
|
499
|
+
INSERT INTO captures (id, payload, filename, kind, origin, episode_path, episode_number, composition, device, registered_at, updated_at)
|
|
500
|
+
VALUES (
|
|
501
|
+
${merged.id},
|
|
502
|
+
${JSON.stringify(merged)},
|
|
503
|
+
${merged.filename},
|
|
504
|
+
${merged.kind},
|
|
505
|
+
${merged.origin},
|
|
506
|
+
${merged.episode_path ?? null},
|
|
507
|
+
${merged.episode_number ?? null},
|
|
508
|
+
${merged.composition ?? null},
|
|
509
|
+
${merged.device ?? null},
|
|
510
|
+
${merged.registered_at},
|
|
511
|
+
NOW()
|
|
512
|
+
)
|
|
513
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
514
|
+
payload = EXCLUDED.payload,
|
|
515
|
+
filename = EXCLUDED.filename,
|
|
516
|
+
kind = EXCLUDED.kind,
|
|
517
|
+
origin = EXCLUDED.origin,
|
|
518
|
+
episode_path = EXCLUDED.episode_path,
|
|
519
|
+
episode_number = EXCLUDED.episode_number,
|
|
520
|
+
composition = EXCLUDED.composition,
|
|
521
|
+
device = EXCLUDED.device,
|
|
522
|
+
registered_at = EXCLUDED.registered_at,
|
|
523
|
+
updated_at = NOW()
|
|
524
|
+
`;
|
|
525
|
+
return merged;
|
|
526
|
+
}
|
|
527
|
+
async getCapture(id) {
|
|
528
|
+
const rows = await this.db `SELECT payload FROM captures WHERE id = ${id}`;
|
|
529
|
+
if (rows.length === 0)
|
|
530
|
+
return null;
|
|
531
|
+
return parseJsonValue(rows[0].payload, null);
|
|
532
|
+
}
|
|
533
|
+
async listCaptures(filters = {}) {
|
|
534
|
+
const rows = await this.db `SELECT payload FROM captures ORDER BY updated_at DESC`;
|
|
535
|
+
return filterAndOrderCaptures(rows.map((row) => parseJsonValue(row.payload, null)), filters);
|
|
536
|
+
}
|
|
537
|
+
async ensureCapturesTable() {
|
|
538
|
+
await this.db `
|
|
539
|
+
CREATE TABLE IF NOT EXISTS captures (
|
|
540
|
+
id TEXT PRIMARY KEY,
|
|
541
|
+
payload JSONB NOT NULL,
|
|
542
|
+
filename TEXT NOT NULL,
|
|
543
|
+
kind TEXT NOT NULL,
|
|
544
|
+
origin TEXT NOT NULL,
|
|
545
|
+
episode_path TEXT,
|
|
546
|
+
episode_number INTEGER,
|
|
547
|
+
composition TEXT,
|
|
548
|
+
device TEXT,
|
|
549
|
+
registered_at TIMESTAMPTZ,
|
|
550
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
551
|
+
)
|
|
552
|
+
`;
|
|
553
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_captures_episode_path ON captures(episode_path)`;
|
|
554
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_captures_episode_number ON captures(episode_number)`;
|
|
555
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_captures_composition ON captures(composition)`;
|
|
556
|
+
await this.db `CREATE INDEX IF NOT EXISTS idx_captures_kind ON captures(kind)`;
|
|
557
|
+
}
|
|
492
558
|
}
|
|
493
559
|
function getConnectionString() {
|
|
494
560
|
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.5.
|
|
3
|
+
"version": "0.5.10",
|
|
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.5.
|
|
19
|
+
"@medicine-wheel/ontology-core": "^0.5.10"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.7.0"
|