@x12i/static-memorix 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +278 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +109 -0
- package/dist/engine/aliases.d.ts +32 -0
- package/dist/engine/aliases.js +74 -0
- package/dist/engine/associations.d.ts +26 -0
- package/dist/engine/associations.js +60 -0
- package/dist/engine/identity.d.ts +22 -0
- package/dist/engine/identity.js +50 -0
- package/dist/engine/inventory.d.ts +29 -0
- package/dist/engine/inventory.js +133 -0
- package/dist/engine/lists.d.ts +18 -0
- package/dist/engine/lists.js +62 -0
- package/dist/engine/narratives.d.ts +16 -0
- package/dist/engine/narratives.js +67 -0
- package/dist/engine/objectTypes.d.ts +11 -0
- package/dist/engine/objectTypes.js +71 -0
- package/dist/engine/query.d.ts +38 -0
- package/dist/engine/query.js +191 -0
- package/dist/engine/records.d.ts +23 -0
- package/dist/engine/records.js +152 -0
- package/dist/engine/snapshots.d.ts +17 -0
- package/dist/engine/snapshots.js +110 -0
- package/dist/engine/write.d.ts +30 -0
- package/dist/engine/write.js +163 -0
- package/dist/routes/helpers.d.ts +4 -0
- package/dist/routes/helpers.js +29 -0
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +402 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +130 -0
- package/dist/storage/InMemoryStore.d.ts +31 -0
- package/dist/storage/InMemoryStore.js +242 -0
- package/dist/storage/fs.d.ts +6 -0
- package/dist/storage/fs.js +54 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +37 -0
- package/guides/demo-app.md +527 -0
- package/guides/managing-json-files.md +384 -0
- package/guides/running-the-service.md +269 -0
- package/guides/using-the-api.md +265 -0
- package/mocks/data/admin/snapshots.json +52 -0
- package/mocks/data/assets/analysis.json +14 -0
- package/mocks/data/assets/decisions.json +14 -0
- package/mocks/data/assets/events.json +4 -0
- package/mocks/data/assets/snapshots.json +49 -0
- package/mocks/data/findings/analysis.json +4 -0
- package/mocks/data/findings/decisions.json +4 -0
- package/mocks/data/findings/events.json +3 -0
- package/mocks/data/findings/snapshots.json +21 -0
- package/mocks/data/marketing/snapshots.json +75 -0
- package/mocks/data/memory/memory.json +31 -0
- package/mocks/data/product/snapshots.json +84 -0
- package/mocks/data/system/inventory.json +8 -0
- package/mocks/data/users/snapshots.json +5 -0
- package/mocks/demo.html +828 -0
- package/mocks/metadata/agents.json +7 -0
- package/mocks/metadata/lists/assets-default.json +22 -0
- package/mocks/metadata/lists/backlog.json +10 -0
- package/mocks/metadata/lists/findings-default.json +9 -0
- package/mocks/metadata/lists/my-plate.json +10 -0
- package/mocks/metadata/lists/this-week.json +10 -0
- package/mocks/metadata/narratives/admin.json +7 -0
- package/mocks/metadata/narratives/assets.json +17 -0
- package/mocks/metadata/narratives/findings.json +7 -0
- package/mocks/metadata/narratives/marketing.json +17 -0
- package/mocks/metadata/narratives/product.json +12 -0
- package/mocks/metadata/object-types/admin.json +6 -0
- package/mocks/metadata/object-types/assets.json +9 -0
- package/mocks/metadata/object-types/findings.json +8 -0
- package/mocks/metadata/object-types/marketing.json +6 -0
- package/mocks/metadata/object-types/memory.json +6 -0
- package/mocks/metadata/object-types/product.json +6 -0
- package/mocks/metadata/object-types/users.json +13 -0
- package/mocks/metadata/write-descriptors/admin-task-write.json +20 -0
- package/mocks/metadata/write-descriptors/assets-analysis-write.json +14 -0
- package/mocks/metadata/write-descriptors/marketing-task-write.json +20 -0
- package/mocks/metadata/write-descriptors/memory-write.json +14 -0
- package/mocks/metadata/write-descriptors/product-task-write.json +20 -0
- package/package.json +67 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the primary identity field for a given object type / content type.
|
|
3
|
+
* `memory` collections are keyed by `memoryId`; everything else falls back to
|
|
4
|
+
* the per-document heuristic used elsewhere.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveIdField(ct, sample) {
|
|
7
|
+
if (ct === "memory")
|
|
8
|
+
return "memoryId";
|
|
9
|
+
if (sample) {
|
|
10
|
+
if (ct === "events" && "eventId" in sample)
|
|
11
|
+
return "eventId";
|
|
12
|
+
if ("recordId" in sample)
|
|
13
|
+
return "recordId";
|
|
14
|
+
if ("entityId" in sample)
|
|
15
|
+
return "entityId";
|
|
16
|
+
if ("eventId" in sample)
|
|
17
|
+
return "eventId";
|
|
18
|
+
if ("id" in sample)
|
|
19
|
+
return "id";
|
|
20
|
+
}
|
|
21
|
+
if (ct === "events")
|
|
22
|
+
return "eventId";
|
|
23
|
+
return "recordId";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mock of the production `MemorixRecordArraySchema`. Validates that each record
|
|
27
|
+
* in a collection array carries the collection's canonical identity key.
|
|
28
|
+
* Additional identity-shaped fields are allowed because they can represent
|
|
29
|
+
* relationships (for example a memory has `memoryId` and may reference an
|
|
30
|
+
* `entityId` or `eventId`).
|
|
31
|
+
*/
|
|
32
|
+
export function validateRecordArray(objectType, ct, records) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
const expected = resolveIdField(ct, records[0]);
|
|
35
|
+
for (const rec of records) {
|
|
36
|
+
if (rec?.[expected] === undefined) {
|
|
37
|
+
errors.push(`${objectType}/${ct}: record missing identity key (expected ${expected})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { ok: errors.length === 0, errors };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find a record by any of the supported identity keys.
|
|
44
|
+
*/
|
|
45
|
+
export function findByIdentity(records, identity) {
|
|
46
|
+
const entries = Object.entries(identity).filter(([, v]) => v != null);
|
|
47
|
+
if (entries.length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
return records.find((d) => entries.every(([k, v]) => String(d[k]) === String(v)));
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ExplorerQueryParams } from "../types.js";
|
|
2
|
+
export interface InventoryRow {
|
|
3
|
+
objectType: string;
|
|
4
|
+
contentType: string;
|
|
5
|
+
collection: string;
|
|
6
|
+
count: number;
|
|
7
|
+
status: "populated" | "empty" | "orphan";
|
|
8
|
+
provenance: {
|
|
9
|
+
source: "catalog" | "orphan";
|
|
10
|
+
};
|
|
11
|
+
missingCollections?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function listObjectTypes(): string[];
|
|
14
|
+
export declare function listDataObjectTypes(): string[];
|
|
15
|
+
export declare function computeInventory(params: ExplorerQueryParams): InventoryRow[];
|
|
16
|
+
export declare function computeInventorySummary(rows: InventoryRow[], params: ExplorerQueryParams): Record<string, unknown>;
|
|
17
|
+
export declare function computeInventoryIssues(rows: InventoryRow[]): InventoryRow[];
|
|
18
|
+
export declare function computeGraph(rows: InventoryRow[]): {
|
|
19
|
+
nodes: {
|
|
20
|
+
id: string;
|
|
21
|
+
objectType: string;
|
|
22
|
+
populated: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
edges: {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
relationType?: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { store } from "../storage/InMemoryStore.js";
|
|
3
|
+
import { CONTENT_TYPES } from "../types.js";
|
|
4
|
+
import { DATA_DIR } from "../config.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { isDir } from "../storage/fs.js";
|
|
7
|
+
export function listObjectTypes() {
|
|
8
|
+
return store.getMetaKeys()
|
|
9
|
+
.filter((key) => key.startsWith("object-types/"))
|
|
10
|
+
.map((key) => key.slice("object-types/".length));
|
|
11
|
+
}
|
|
12
|
+
export function listDataObjectTypes() {
|
|
13
|
+
if (!isDir(DATA_DIR))
|
|
14
|
+
return [];
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(DATA_DIR)
|
|
17
|
+
.filter((d) => isDir(path.join(DATA_DIR, d)) && d !== "system");
|
|
18
|
+
}
|
|
19
|
+
export function computeInventory(params) {
|
|
20
|
+
const sourceLens = params.sourceLens || "catalox-first";
|
|
21
|
+
const target = params.target;
|
|
22
|
+
const rows = [];
|
|
23
|
+
const catalogTypes = new Set(listObjectTypes());
|
|
24
|
+
const dataTypes = new Set(listDataObjectTypes());
|
|
25
|
+
// Determine which content types to scan. `target=memory` restricts to the
|
|
26
|
+
// memory tier; otherwise include all tiers (entities/events/knowledge/memory).
|
|
27
|
+
const contentTypes = target === "memory"
|
|
28
|
+
? ["memory"]
|
|
29
|
+
: CONTENT_TYPES;
|
|
30
|
+
if (sourceLens === "catalox-first") {
|
|
31
|
+
const objectTypes = target === "memory"
|
|
32
|
+
? ["memory"]
|
|
33
|
+
: [...catalogTypes, ...dataTypes];
|
|
34
|
+
for (const ot of objectTypes) {
|
|
35
|
+
for (const ct of contentTypes) {
|
|
36
|
+
const data = store.getCollection(ot, ct);
|
|
37
|
+
const isCatalog = catalogTypes.has(ot);
|
|
38
|
+
rows.push({
|
|
39
|
+
objectType: ot,
|
|
40
|
+
contentType: ct,
|
|
41
|
+
collection: `${ot}/${ct}`,
|
|
42
|
+
count: data.length,
|
|
43
|
+
status: data.length > 0 ? "populated" : "empty",
|
|
44
|
+
provenance: { source: isCatalog || ot === "memory" ? "catalog" : "orphan" },
|
|
45
|
+
missingCollections: !isCatalog && ot !== "memory",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// db-first: scan all array keys in store.data
|
|
52
|
+
const keys = store.getCollectionKeys().filter((k) => !k.startsWith("system/"));
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const [ot, ct] = key.split("/");
|
|
55
|
+
if (target === "memory" && ct !== "memory")
|
|
56
|
+
continue;
|
|
57
|
+
const data = store.getCollection(ot, ct);
|
|
58
|
+
const mapped = catalogTypes.has(ot);
|
|
59
|
+
rows.push({
|
|
60
|
+
objectType: ot,
|
|
61
|
+
contentType: ct,
|
|
62
|
+
collection: key,
|
|
63
|
+
count: data.length,
|
|
64
|
+
status: data.length > 0 ? "populated" : "empty",
|
|
65
|
+
provenance: { source: mapped ? "catalog" : "orphan" },
|
|
66
|
+
missingCollections: !mapped,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
export function computeInventorySummary(rows, params) {
|
|
73
|
+
const declared = rows.length;
|
|
74
|
+
const populated = rows.filter((r) => r.status === "populated").length;
|
|
75
|
+
const empty = rows.filter((r) => r.status === "empty").length;
|
|
76
|
+
const orphans = rows.filter((r) => r.provenance.source === "orphan").length;
|
|
77
|
+
const summary = {
|
|
78
|
+
declaredCollections: declared,
|
|
79
|
+
populatedCollections: populated,
|
|
80
|
+
emptyCollections: empty,
|
|
81
|
+
orphanCollections: orphans,
|
|
82
|
+
};
|
|
83
|
+
if (params.includeExactCounts) {
|
|
84
|
+
summary.perObjectType = rows.reduce((acc, r) => {
|
|
85
|
+
acc[r.objectType] = acc[r.objectType] || {
|
|
86
|
+
populated: 0,
|
|
87
|
+
empty: 0,
|
|
88
|
+
total: 0,
|
|
89
|
+
};
|
|
90
|
+
acc[r.objectType].total++;
|
|
91
|
+
if (r.status === "populated")
|
|
92
|
+
acc[r.objectType].populated++;
|
|
93
|
+
else
|
|
94
|
+
acc[r.objectType].empty++;
|
|
95
|
+
return acc;
|
|
96
|
+
}, {});
|
|
97
|
+
}
|
|
98
|
+
return summary;
|
|
99
|
+
}
|
|
100
|
+
export function computeInventoryIssues(rows) {
|
|
101
|
+
return rows.filter((r) => r.provenance.source === "orphan" || r.missingCollections === true);
|
|
102
|
+
}
|
|
103
|
+
export function computeGraph(rows) {
|
|
104
|
+
const nodes = [];
|
|
105
|
+
const edges = [];
|
|
106
|
+
const nodeSet = new Set();
|
|
107
|
+
for (const r of rows) {
|
|
108
|
+
if (r.status !== "populated")
|
|
109
|
+
continue;
|
|
110
|
+
const nodeId = r.objectType;
|
|
111
|
+
if (!nodeSet.has(nodeId)) {
|
|
112
|
+
nodeSet.add(nodeId);
|
|
113
|
+
nodes.push({ id: nodeId, objectType: nodeId, populated: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const ot of nodeSet) {
|
|
117
|
+
const meta = store.getMeta(`object-types/${ot}`);
|
|
118
|
+
if (!meta)
|
|
119
|
+
continue;
|
|
120
|
+
const relations = meta.catalogRelations || [];
|
|
121
|
+
for (const rel of relations) {
|
|
122
|
+
const target = rel.targetObject;
|
|
123
|
+
if (!target || !nodeSet.has(target))
|
|
124
|
+
continue;
|
|
125
|
+
edges.push({
|
|
126
|
+
from: ot,
|
|
127
|
+
to: target,
|
|
128
|
+
relationType: rel.relationType || "related",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { nodes, edges };
|
|
133
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ListDescriptor, type ExplorerQueryParams } from "../types.js";
|
|
2
|
+
export declare function listListDescriptors(entityName?: string): ListDescriptor[];
|
|
3
|
+
export declare function getListDescriptor(listId: string): ListDescriptor | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Executes a list descriptor query pipeline against local data arrays.
|
|
6
|
+
*/
|
|
7
|
+
export declare function executeListDescriptor(desc: ListDescriptor, params?: ExplorerQueryParams): Promise<any>;
|
|
8
|
+
/**
|
|
9
|
+
* Suggest extensions / analytics for unjoined content types.
|
|
10
|
+
*/
|
|
11
|
+
export declare function suggestListExtensions(entityName: string): {
|
|
12
|
+
entityName: string;
|
|
13
|
+
suggestions: {
|
|
14
|
+
contentType: "analysis" | "decisions" | "events" | "memory";
|
|
15
|
+
collection: string;
|
|
16
|
+
suggestion: string;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { store } from "../storage/InMemoryStore.js";
|
|
2
|
+
import { CONTENT_TYPES } from "../types.js";
|
|
3
|
+
import { queryMockCollection } from "../engine/query.js";
|
|
4
|
+
export function listListDescriptors(entityName) {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const key of store.getMetaKeys().filter((item) => item.startsWith("lists/"))) {
|
|
7
|
+
const desc = store.getMeta(key);
|
|
8
|
+
if (desc && (!entityName || desc.entity === entityName || desc.entityName === entityName)) {
|
|
9
|
+
out.push(desc);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export function getListDescriptor(listId) {
|
|
15
|
+
return store.getMeta(`lists/${listId}`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Executes a list descriptor query pipeline against local data arrays.
|
|
19
|
+
*/
|
|
20
|
+
export async function executeListDescriptor(desc, params = {}) {
|
|
21
|
+
const entity = desc.entity || desc.entityName;
|
|
22
|
+
if (!entity) {
|
|
23
|
+
return { rows: [], page: { offset: 0, limit: 50 } };
|
|
24
|
+
}
|
|
25
|
+
// Gather base collection (snapshots by default).
|
|
26
|
+
const baseCt = desc.baseContentType || "snapshots";
|
|
27
|
+
let records = store.getCollection(entity, baseCt);
|
|
28
|
+
// Apply descriptor query if present
|
|
29
|
+
if (desc.query) {
|
|
30
|
+
const mingo = (await import("mingo")).default;
|
|
31
|
+
const query = new mingo.Query(desc.query);
|
|
32
|
+
records = query.find(records).all();
|
|
33
|
+
}
|
|
34
|
+
const result = queryMockCollection(records, { ...params, ...(desc.query ? { filter: undefined } : {}) });
|
|
35
|
+
// Apply projections / field selection
|
|
36
|
+
const fields = desc.fields || (desc.projections ? Object.keys(desc.projections) : null);
|
|
37
|
+
if (fields && fields.length) {
|
|
38
|
+
result.rows = result.rows.map((r) => {
|
|
39
|
+
const proj = { id: r.id ?? r.recordId };
|
|
40
|
+
for (const f of fields) {
|
|
41
|
+
if (f in r)
|
|
42
|
+
proj[f] = r[f];
|
|
43
|
+
}
|
|
44
|
+
return proj;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Suggest extensions / analytics for unjoined content types.
|
|
51
|
+
*/
|
|
52
|
+
export function suggestListExtensions(entityName) {
|
|
53
|
+
const available = CONTENT_TYPES.filter((ct) => store.getCollection(entityName, ct).length > 0);
|
|
54
|
+
const suggestions = available
|
|
55
|
+
.filter((ct) => ct !== "snapshots")
|
|
56
|
+
.map((ct) => ({
|
|
57
|
+
contentType: ct,
|
|
58
|
+
collection: `${entityName}/${ct}`,
|
|
59
|
+
suggestion: `Add extension join for ${ct} records into the entity view.`,
|
|
60
|
+
}));
|
|
61
|
+
return { entityName, suggestions };
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type NarrativeDefinition } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Discover narrative keys tagged on records via doc.narratives.*.
|
|
4
|
+
*/
|
|
5
|
+
export declare function discoverRecordNarrativeTags(entityName: string): string[];
|
|
6
|
+
export declare function mergeCatalogWithSignals(authored: Record<string, NarrativeDefinition>, recordTags: string[]): Array<NarrativeDefinition & {
|
|
7
|
+
signaled: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function getMergedNarratives(entityName?: string): (NarrativeDefinition & {
|
|
10
|
+
signaled: boolean;
|
|
11
|
+
})[] | Record<string, (NarrativeDefinition & {
|
|
12
|
+
signaled: boolean;
|
|
13
|
+
})[]>;
|
|
14
|
+
export declare function getRawNarratives(entityName: string): Record<string, NarrativeDefinition>;
|
|
15
|
+
export declare function queryNarrativeRecords(entityName: string, key: string): any[];
|
|
16
|
+
export declare function listNarrativeEntities(): string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { store } from "../storage/InMemoryStore.js";
|
|
2
|
+
import { CONTENT_TYPES } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Discover narrative keys tagged on records via doc.narratives.*.
|
|
5
|
+
*/
|
|
6
|
+
export function discoverRecordNarrativeTags(entityName) {
|
|
7
|
+
const tags = new Set();
|
|
8
|
+
for (const ct of CONTENT_TYPES) {
|
|
9
|
+
const records = store.getCollection(entityName, ct);
|
|
10
|
+
for (const doc of records) {
|
|
11
|
+
const narratives = doc.narratives || {};
|
|
12
|
+
for (const key of Object.keys(narratives))
|
|
13
|
+
tags.add(key);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...tags];
|
|
17
|
+
}
|
|
18
|
+
export function mergeCatalogWithSignals(authored, recordTags) {
|
|
19
|
+
const result = [];
|
|
20
|
+
const authoredKeys = Object.keys(authored || {});
|
|
21
|
+
for (const key of authoredKeys) {
|
|
22
|
+
result.push({ ...authored[key], key, signaled: recordTags.includes(key) });
|
|
23
|
+
}
|
|
24
|
+
for (const tag of recordTags) {
|
|
25
|
+
if (!authoredKeys.includes(tag)) {
|
|
26
|
+
result.push({ key: tag, signaled: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
export function getMergedNarratives(entityName) {
|
|
32
|
+
if (entityName) {
|
|
33
|
+
const authored = store.getMeta(`narratives/${entityName}`) || {};
|
|
34
|
+
const recordTags = discoverRecordNarrativeTags(entityName);
|
|
35
|
+
return mergeCatalogWithSignals(authored, recordTags);
|
|
36
|
+
}
|
|
37
|
+
// global across all entities
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const metaKey of store.getMetaKeys().filter((key) => key.startsWith("narratives/"))) {
|
|
40
|
+
const name = metaKey.slice("narratives/".length);
|
|
41
|
+
const authored = store.getMeta(metaKey) || {};
|
|
42
|
+
const recordTags = discoverRecordNarrativeTags(name);
|
|
43
|
+
out[name] = mergeCatalogWithSignals(authored, recordTags);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function getRawNarratives(entityName) {
|
|
48
|
+
return (store.getMeta(`narratives/${entityName}`) || {});
|
|
49
|
+
}
|
|
50
|
+
export function queryNarrativeRecords(entityName, key) {
|
|
51
|
+
const results = [];
|
|
52
|
+
for (const ct of CONTENT_TYPES) {
|
|
53
|
+
const records = store.getCollection(entityName, ct);
|
|
54
|
+
for (const doc of records) {
|
|
55
|
+
const narratives = doc.narratives || {};
|
|
56
|
+
const val = narratives[key];
|
|
57
|
+
if (val === true || val === key)
|
|
58
|
+
results.push(doc);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
export function listNarrativeEntities() {
|
|
64
|
+
return store.getMetaKeys()
|
|
65
|
+
.filter((key) => key.startsWith("narratives/"))
|
|
66
|
+
.map((key) => key.slice("narratives/".length));
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RootPropertyEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
occurrencePct: number;
|
|
4
|
+
types: string[];
|
|
5
|
+
sampleValues: unknown[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Full-scan all record arrays in ./data/{name}/*.json, compute property
|
|
9
|
+
* occurrence percentages, data types, and paths.
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeRootPropertyCatalog(objectType: string): RootPropertyEntry[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { store } from "../storage/InMemoryStore.js";
|
|
2
|
+
import { CONTENT_TYPES } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Full-scan all record arrays in ./data/{name}/*.json, compute property
|
|
5
|
+
* occurrence percentages, data types, and paths.
|
|
6
|
+
*/
|
|
7
|
+
export function computeRootPropertyCatalog(objectType) {
|
|
8
|
+
const counts = new Map();
|
|
9
|
+
const typesMap = new Map();
|
|
10
|
+
const samples = new Map();
|
|
11
|
+
let totalDocs = 0;
|
|
12
|
+
for (const ct of CONTENT_TYPES) {
|
|
13
|
+
const records = store.getCollection(objectType, ct);
|
|
14
|
+
for (const doc of records) {
|
|
15
|
+
totalDocs++;
|
|
16
|
+
const flat = flatten(doc);
|
|
17
|
+
for (const [p, v] of Object.entries(flat)) {
|
|
18
|
+
counts.set(p, (counts.get(p) || 0) + 1);
|
|
19
|
+
const t = typeOf(v);
|
|
20
|
+
if (!typesMap.has(p))
|
|
21
|
+
typesMap.set(p, new Set());
|
|
22
|
+
typesMap.get(p).add(t);
|
|
23
|
+
if (!samples.has(p))
|
|
24
|
+
samples.set(p, []);
|
|
25
|
+
const arr = samples.get(p);
|
|
26
|
+
if (arr.length < 3 && !arr.includes(v))
|
|
27
|
+
arr.push(v);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const entries = [];
|
|
32
|
+
for (const [p, c] of counts.entries()) {
|
|
33
|
+
entries.push({
|
|
34
|
+
path: p,
|
|
35
|
+
occurrencePct: totalDocs ? Math.round((c / totalDocs) * 1000) / 10 : 0,
|
|
36
|
+
types: [...(typesMap.get(p) || [])],
|
|
37
|
+
sampleValues: samples.get(p) || [],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
entries.sort((a, b) => b.occurrencePct - a.occurrencePct);
|
|
41
|
+
// Persist back to metadata
|
|
42
|
+
const meta = store.getMeta(`object-types/${objectType}`) || { name: objectType };
|
|
43
|
+
meta.rootPropertyCatalog = entries;
|
|
44
|
+
store.setMeta(`object-types/${objectType}`, meta);
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
function flatten(obj, prefix = "") {
|
|
48
|
+
const out = {};
|
|
49
|
+
if (obj == null || typeof obj !== "object") {
|
|
50
|
+
if (prefix)
|
|
51
|
+
out[prefix] = obj;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
55
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
56
|
+
if (v != null && typeof v === "object" && !Array.isArray(v)) {
|
|
57
|
+
Object.assign(out, flatten(v, key));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
out[key] = v;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function typeOf(v) {
|
|
66
|
+
if (v === null)
|
|
67
|
+
return "null";
|
|
68
|
+
if (Array.isArray(v))
|
|
69
|
+
return "array";
|
|
70
|
+
return typeof v;
|
|
71
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExplorerQueryParams } from "../types.js";
|
|
2
|
+
export type FilterOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "exists" | "regex";
|
|
3
|
+
export interface ParsedFilter {
|
|
4
|
+
field: string;
|
|
5
|
+
op: FilterOp;
|
|
6
|
+
value: unknown;
|
|
7
|
+
raw: string;
|
|
8
|
+
isVirtual?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseFilterToken(token: string): ParsedFilter | null;
|
|
11
|
+
export declare function coerce(value: string): unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Parse the filter param. Supports repeated values (array) and `;` separators.
|
|
14
|
+
* Returns virtual narrative filters separately so the caller can rewrite them.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseExplorerFilters(filter?: string | string[]): {
|
|
17
|
+
criteria: Record<string, unknown>;
|
|
18
|
+
virtual: ParsedFilter[];
|
|
19
|
+
};
|
|
20
|
+
export declare function parseSortParams(sort?: string | string[]): Record<string, 1 | -1>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve virtual narrative filters into a predicate that checks
|
|
23
|
+
* doc.narratives[key] presence / truthiness.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyVirtualNarrativeFilters(records: any[], virtual: ParsedFilter[]): any[];
|
|
26
|
+
export interface QueryResult {
|
|
27
|
+
rows: any[];
|
|
28
|
+
page: {
|
|
29
|
+
offset: number;
|
|
30
|
+
limit: number;
|
|
31
|
+
total?: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute a query against an in-memory record array using mingo, with
|
|
36
|
+
* full-text search, virtual narrative filters, sorting, and pagination.
|
|
37
|
+
*/
|
|
38
|
+
export declare function queryMockCollection(records: any[], queryParams: ExplorerQueryParams): QueryResult;
|