@x12i/memorix-client-api 0.18.0 → 0.19.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/HANDOVER.md +13 -2
- package/dist/agent-directory.d.ts +25 -0
- package/dist/agent-directory.d.ts.map +1 -0
- package/dist/agent-directory.js +75 -0
- package/dist/agent-directory.js.map +1 -0
- package/dist/app.d.ts +31 -6
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +220 -33
- package/dist/app.js.map +1 -1
- package/dist/authix.d.ts +28 -2
- package/dist/authix.d.ts.map +1 -1
- package/dist/authix.js +55 -4
- package/dist/authix.js.map +1 -1
- package/dist/catalog.d.ts +33 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +83 -4
- package/dist/catalog.js.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +16 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -2
- package/dist/errors.js.map +1 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +27 -3
- package/dist/fetch.js.map +1 -1
- package/dist/peers.d.ts +2 -2
- package/dist/peers.d.ts.map +1 -1
- package/dist/peers.js +4 -4
- package/dist/peers.js.map +1 -1
- package/dist/refs.d.ts +7 -1
- package/dist/refs.d.ts.map +1 -1
- package/dist/refs.js +36 -26
- package/dist/refs.js.map +1 -1
- package/dist/scope.d.ts +25 -2
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +71 -4
- package/dist/scope.js.map +1 -1
- package/dist/scoper-embedded.d.ts +29 -0
- package/dist/scoper-embedded.d.ts.map +1 -0
- package/dist/scoper-embedded.js +239 -0
- package/dist/scoper-embedded.js.map +1 -0
- package/dist/scoper.d.ts +19 -27
- package/dist/scoper.d.ts.map +1 -1
- package/dist/scoper.js +51 -254
- package/dist/scoper.js.map +1 -1
- package/dist/upstream.d.ts +6 -1
- package/dist/upstream.d.ts.map +1 -1
- package/dist/upstream.js +22 -4
- package/dist/upstream.js.map +1 -1
- package/dist/v1/census.d.ts +59 -0
- package/dist/v1/census.d.ts.map +1 -0
- package/dist/v1/census.js +146 -0
- package/dist/v1/census.js.map +1 -0
- package/dist/v1/content.d.ts +48 -0
- package/dist/v1/content.d.ts.map +1 -0
- package/dist/v1/content.js +292 -0
- package/dist/v1/content.js.map +1 -0
- package/dist/v1/context.d.ts +50 -0
- package/dist/v1/context.d.ts.map +1 -0
- package/dist/v1/context.js +4 -0
- package/dist/v1/context.js.map +1 -0
- package/dist/v1/discovery.d.ts +24 -0
- package/dist/v1/discovery.d.ts.map +1 -0
- package/dist/v1/discovery.js +163 -0
- package/dist/v1/discovery.js.map +1 -0
- package/dist/v1/gaps.d.ts +18 -0
- package/dist/v1/gaps.d.ts.map +1 -0
- package/dist/v1/gaps.js +2 -0
- package/dist/v1/gaps.js.map +1 -0
- package/dist/v1/http.d.ts +18 -0
- package/dist/v1/http.d.ts.map +1 -0
- package/dist/v1/http.js +105 -0
- package/dist/v1/http.js.map +1 -0
- package/dist/v1/mapping.d.ts +155 -0
- package/dist/v1/mapping.d.ts.map +1 -0
- package/dist/v1/mapping.js +380 -0
- package/dist/v1/mapping.js.map +1 -0
- package/dist/v1/planning-store.d.ts +74 -0
- package/dist/v1/planning-store.d.ts.map +1 -0
- package/dist/v1/planning-store.js +192 -0
- package/dist/v1/planning-store.js.map +1 -0
- package/dist/v1/planning.d.ts +5 -0
- package/dist/v1/planning.d.ts.map +1 -0
- package/dist/v1/planning.js +297 -0
- package/dist/v1/planning.js.map +1 -0
- package/dist/v1/records.d.ts +18 -0
- package/dist/v1/records.d.ts.map +1 -0
- package/dist/v1/records.js +42 -0
- package/dist/v1/records.js.map +1 -0
- package/dist/v1/relationships.d.ts +72 -0
- package/dist/v1/relationships.d.ts.map +1 -0
- package/dist/v1/relationships.js +436 -0
- package/dist/v1/relationships.js.map +1 -0
- package/dist/v1/routes.d.ts +5 -0
- package/dist/v1/routes.d.ts.map +1 -0
- package/dist/v1/routes.js +16 -0
- package/dist/v1/routes.js.map +1 -0
- package/dist/v1/sources.d.ts +62 -0
- package/dist/v1/sources.d.ts.map +1 -0
- package/dist/v1/sources.js +218 -0
- package/dist/v1/sources.js.map +1 -0
- package/openapi/openapi.json +3587 -480
- package/package.json +74 -74
package/dist/upstream.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { ApiError, UpstreamError } from "./errors.js";
|
|
2
|
+
export const DEFAULT_UPSTREAM_TIMEOUT_MS = 30_000;
|
|
2
3
|
export function subjectDirectoryPath(recordId) {
|
|
3
4
|
const encoded = recordId.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
4
5
|
return `/api/data/subjects/${encoded}`;
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
+
function isAbort(err) {
|
|
8
|
+
if (!err || typeof err !== "object")
|
|
9
|
+
return false;
|
|
10
|
+
const name = err.name;
|
|
11
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
12
|
+
}
|
|
13
|
+
export function createUpstream(memorixUrl, fetchImpl = fetch, timeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS) {
|
|
7
14
|
const base = memorixUrl.replace(/\/$/, "");
|
|
8
15
|
async function call(method, path, scope, body, init) {
|
|
16
|
+
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
9
17
|
let res;
|
|
10
18
|
try {
|
|
11
19
|
res = await fetchImpl(`${base}${path}`, {
|
|
@@ -18,12 +26,17 @@ export function createUpstream(memorixUrl, fetchImpl = fetch) {
|
|
|
18
26
|
"x-memorix-caller": scope.caller,
|
|
19
27
|
},
|
|
20
28
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
21
|
-
signal
|
|
29
|
+
signal,
|
|
22
30
|
});
|
|
23
31
|
}
|
|
24
32
|
catch (err) {
|
|
33
|
+
if (!init?.signal && isAbort(err)) {
|
|
34
|
+
throw new ApiError(504, "UPSTREAM_TIMEOUT", `Memorix did not answer ${method} ${path.split("?")[0]} within ${timeoutMs}ms`, {
|
|
35
|
+
retryable: true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
25
38
|
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
-
throw new UpstreamError(
|
|
39
|
+
throw new UpstreamError(503, { error: { code: "UPSTREAM_UNAVAILABLE", message, retryable: true } });
|
|
27
40
|
}
|
|
28
41
|
const text = await res.text();
|
|
29
42
|
let parsed = null;
|
|
@@ -36,7 +49,7 @@ export function createUpstream(memorixUrl, fetchImpl = fetch) {
|
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
if (!res.ok)
|
|
39
|
-
throw new UpstreamError(res.status, parsed);
|
|
52
|
+
throw new UpstreamError(res.status, parsed, res.headers.get("retry-after") ?? undefined);
|
|
40
53
|
return parsed;
|
|
41
54
|
}
|
|
42
55
|
return {
|
|
@@ -55,4 +68,9 @@ export function requireRecord(value, message) {
|
|
|
55
68
|
throw new ApiError(502, "UPSTREAM", message);
|
|
56
69
|
return record;
|
|
57
70
|
}
|
|
71
|
+
/** Memorix error code on an upstream body, when it sent one. */
|
|
72
|
+
export function upstreamCode(err) {
|
|
73
|
+
const error = asRecord(asRecord(err.body)?.error);
|
|
74
|
+
return typeof error?.code === "string" ? error.code : undefined;
|
|
75
|
+
}
|
|
58
76
|
//# sourceMappingURL=upstream.js.map
|
package/dist/upstream.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upstream.js","sourceRoot":"","sources":["../src/upstream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"upstream.js","sourceRoot":"","sources":["../src/upstream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ClientScope } from "../scope.js";
|
|
2
|
+
import { type Upstream } from "../upstream.js";
|
|
3
|
+
export type ExactCount = {
|
|
4
|
+
status: "exact";
|
|
5
|
+
value: number;
|
|
6
|
+
deletedExcluded?: boolean;
|
|
7
|
+
versionSemantics?: "current-head" | "directory" | "stored-links";
|
|
8
|
+
method: string;
|
|
9
|
+
observedAt?: string;
|
|
10
|
+
aggregated?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type UnavailableCount = {
|
|
13
|
+
status: "unavailable";
|
|
14
|
+
value: null;
|
|
15
|
+
reason: string;
|
|
16
|
+
retryable: boolean;
|
|
17
|
+
method?: string;
|
|
18
|
+
observedAt?: string;
|
|
19
|
+
aggregated?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type CountView = ExactCount | UnavailableCount;
|
|
22
|
+
export type CensusContentType = {
|
|
23
|
+
contentType: string;
|
|
24
|
+
rows: CountView;
|
|
25
|
+
};
|
|
26
|
+
export type CensusObjectType = {
|
|
27
|
+
objectType: string;
|
|
28
|
+
dataCategory: string | null;
|
|
29
|
+
resolution?: "conflict";
|
|
30
|
+
records: CountView;
|
|
31
|
+
recordsWithoutContent: CountView;
|
|
32
|
+
observedAt?: string;
|
|
33
|
+
contentTypes: CensusContentType[];
|
|
34
|
+
};
|
|
35
|
+
export type CensusSnapshot = {
|
|
36
|
+
observedAt: string;
|
|
37
|
+
ok: boolean;
|
|
38
|
+
partial: boolean;
|
|
39
|
+
unresolvedAgentIds: string[];
|
|
40
|
+
issues: Array<{
|
|
41
|
+
code: string;
|
|
42
|
+
message?: string;
|
|
43
|
+
agentId?: string;
|
|
44
|
+
}>;
|
|
45
|
+
objectTypes: CensusObjectType[];
|
|
46
|
+
};
|
|
47
|
+
export type CensusService = {
|
|
48
|
+
/** Census for the scope's org and agents. Reused for `ttl` unless `refresh`. */
|
|
49
|
+
get(scope: ClientScope, options?: {
|
|
50
|
+
refresh?: boolean;
|
|
51
|
+
}): Promise<CensusSnapshot>;
|
|
52
|
+
/** Cached census for the scope, without reading Memorix. */
|
|
53
|
+
peek(scope: ClientScope): CensusSnapshot | null;
|
|
54
|
+
};
|
|
55
|
+
export declare function unavailable(reason: string, retryable: boolean, extra?: Partial<UnavailableCount>): UnavailableCount;
|
|
56
|
+
/** Normalize `GET /api/data/census` (service 3.19.1). */
|
|
57
|
+
export declare function censusFromMemorix(body: unknown): CensusSnapshot;
|
|
58
|
+
export declare function createCensusService(upstream: Upstream, ttlMs?: number): CensusService;
|
|
59
|
+
//# sourceMappingURL=census.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"census.d.ts","sourceRoot":"","sources":["../../src/v1/census.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAEvE,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,gBAAgB,CAAC,EAAE,cAAc,GAAG,WAAW,GAAG,cAAc,CAAC;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAEtD,MAAM,MAAM,iBAAiB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEzE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,EAAE,SAAS,CAAC;IACnB,qBAAqB,EAAE,SAAS,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,iBAAiB,EAAE,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,WAAW,EAAE,gBAAgB,EAAE,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,gFAAgF;IAChF,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAClF,4DAA4D;IAC5D,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc,GAAG,IAAI,CAAC;CACjD,CAAC;AAIF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,GAAE,OAAO,CAAC,gBAAgB,CAAM,GAAG,gBAAgB,CAEvH;AA0CD,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CA0C/D;AAMD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,SAAiB,GAAG,aAAa,CA6C7F"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ApiError, UpstreamError } from "../errors.js";
|
|
2
|
+
import { asRecord, upstreamCode } from "../upstream.js";
|
|
3
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
4
|
+
export function unavailable(reason, retryable, extra = {}) {
|
|
5
|
+
return { status: "unavailable", value: null, reason, retryable, ...extra };
|
|
6
|
+
}
|
|
7
|
+
function directoryCount(raw, observedAt) {
|
|
8
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
9
|
+
return {
|
|
10
|
+
status: "exact",
|
|
11
|
+
value: raw,
|
|
12
|
+
deletedExcluded: true,
|
|
13
|
+
versionSemantics: "directory",
|
|
14
|
+
method: "record-directory",
|
|
15
|
+
...(observedAt ? { observedAt } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const body = asRecord(raw);
|
|
19
|
+
if (body?.status === "unavailable") {
|
|
20
|
+
return unavailable(typeof body.reason === "string" ? body.reason : "DIRECTORY_UNAVAILABLE", body.retryable === true, {
|
|
21
|
+
method: "record-directory",
|
|
22
|
+
...(typeof body.observedAt === "string" ? { observedAt: body.observedAt } : observedAt ? { observedAt } : {}),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return unavailable("NOT_REPORTED", false, { method: "record-directory" });
|
|
26
|
+
}
|
|
27
|
+
function rowsCount(raw, observedAt) {
|
|
28
|
+
const at = typeof raw.observedAt === "string" ? raw.observedAt : observedAt;
|
|
29
|
+
if (raw.status === "unavailable") {
|
|
30
|
+
return unavailable(typeof raw.reason === "string" ? raw.reason : "COUNT_UNAVAILABLE", raw.retryable === true, {
|
|
31
|
+
method: "live-content",
|
|
32
|
+
...(at ? { observedAt: at } : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (typeof raw.rows !== "number")
|
|
36
|
+
return unavailable("NOT_REPORTED", false, { method: "live-content" });
|
|
37
|
+
return {
|
|
38
|
+
status: "exact",
|
|
39
|
+
value: raw.rows,
|
|
40
|
+
deletedExcluded: raw.deletedExcluded !== false,
|
|
41
|
+
versionSemantics: "current-head",
|
|
42
|
+
method: raw.estimated === true ? "estimate" : "live-content",
|
|
43
|
+
...(at ? { observedAt: at } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Normalize `GET /api/data/census` (service 3.19.1). */
|
|
47
|
+
export function censusFromMemorix(body) {
|
|
48
|
+
const root = asRecord(body) ?? {};
|
|
49
|
+
const observedAt = typeof root.observedAt === "string" ? root.observedAt : new Date().toISOString();
|
|
50
|
+
const objectTypes = [];
|
|
51
|
+
for (const item of Array.isArray(root.objectTypes) ? root.objectTypes : []) {
|
|
52
|
+
const entry = asRecord(item);
|
|
53
|
+
if (!entry || typeof entry.objectType !== "string")
|
|
54
|
+
continue;
|
|
55
|
+
const at = typeof entry.observedAt === "string" ? entry.observedAt : observedAt;
|
|
56
|
+
const contentTypes = [];
|
|
57
|
+
for (const raw of Array.isArray(entry.contentTypes) ? entry.contentTypes : []) {
|
|
58
|
+
const ct = asRecord(raw);
|
|
59
|
+
if (!ct || typeof ct.contentType !== "string")
|
|
60
|
+
continue;
|
|
61
|
+
contentTypes.push({ contentType: ct.contentType, rows: rowsCount(ct, at) });
|
|
62
|
+
}
|
|
63
|
+
objectTypes.push({
|
|
64
|
+
objectType: entry.objectType,
|
|
65
|
+
dataCategory: typeof entry.dataCategory === "string" ? entry.dataCategory : null,
|
|
66
|
+
...(entry.resolution === "conflict" ? { resolution: "conflict" } : {}),
|
|
67
|
+
records: directoryCount(entry.records, at),
|
|
68
|
+
recordsWithoutContent: directoryCount(entry.recordsWithoutContent, at),
|
|
69
|
+
observedAt: at,
|
|
70
|
+
contentTypes,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const issues = (Array.isArray(root.issues) ? root.issues : [])
|
|
74
|
+
.map((raw) => asRecord(raw))
|
|
75
|
+
.filter((raw) => raw !== null && typeof raw.code === "string")
|
|
76
|
+
.map((raw) => ({
|
|
77
|
+
code: raw.code,
|
|
78
|
+
...(typeof raw.message === "string" ? { message: raw.message } : {}),
|
|
79
|
+
...(typeof raw.agentId === "string" ? { agentId: raw.agentId } : {}),
|
|
80
|
+
}));
|
|
81
|
+
return {
|
|
82
|
+
observedAt,
|
|
83
|
+
ok: root.ok !== false,
|
|
84
|
+
partial: root.partial === true,
|
|
85
|
+
unresolvedAgentIds: Array.isArray(root.unresolvedAgentIds)
|
|
86
|
+
? root.unresolvedAgentIds.filter((id) => typeof id === "string")
|
|
87
|
+
: [],
|
|
88
|
+
issues,
|
|
89
|
+
objectTypes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function cacheKey(scope) {
|
|
93
|
+
return `${scope.orgId}\u0000${[...scope.agentIds].sort().join(",")}`;
|
|
94
|
+
}
|
|
95
|
+
export function createCensusService(upstream, ttlMs = DEFAULT_TTL_MS) {
|
|
96
|
+
const cache = new Map();
|
|
97
|
+
async function load(scope) {
|
|
98
|
+
try {
|
|
99
|
+
return censusFromMemorix(await upstream.get("/api/data/census", scope));
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err instanceof UpstreamError && (err.statusCode === 404 || err.statusCode === 501)) {
|
|
103
|
+
throw new ApiError(501, "CAPABILITY_UNAVAILABLE", "Memorix does not serve GET /api/data/census", {
|
|
104
|
+
feature: "census",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (err instanceof UpstreamError && upstreamCode(err) === "CANONICAL_PORT_MISSING") {
|
|
108
|
+
throw new ApiError(503, "UPSTREAM_UNAVAILABLE", "Memorix has no database for this org", { retryable: true });
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
async get(scope, options = {}) {
|
|
115
|
+
const key = cacheKey(scope);
|
|
116
|
+
const hit = cache.get(key);
|
|
117
|
+
if (!options.refresh && hit) {
|
|
118
|
+
if (hit.pending)
|
|
119
|
+
return hit.pending;
|
|
120
|
+
if (hit.snapshot && Date.now() - hit.at < ttlMs)
|
|
121
|
+
return hit.snapshot;
|
|
122
|
+
}
|
|
123
|
+
const pending = load(scope);
|
|
124
|
+
cache.set(key, { at: Date.now(), pending, ...(hit?.snapshot ? { snapshot: hit.snapshot } : {}) });
|
|
125
|
+
try {
|
|
126
|
+
const snapshot = await pending;
|
|
127
|
+
cache.set(key, { at: Date.now(), snapshot });
|
|
128
|
+
return snapshot;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (hit?.snapshot)
|
|
132
|
+
cache.set(key, { at: hit.at, snapshot: hit.snapshot });
|
|
133
|
+
else
|
|
134
|
+
cache.delete(key);
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
peek(scope) {
|
|
139
|
+
const hit = cache.get(cacheKey(scope));
|
|
140
|
+
if (!hit?.snapshot || Date.now() - hit.at >= ttlMs)
|
|
141
|
+
return null;
|
|
142
|
+
return hit.snapshot;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=census.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"census.js","sourceRoot":"","sources":["../../src/v1/census.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAiB,MAAM,gBAAgB,CAAC;AAoDvE,MAAM,cAAc,GAAG,MAAM,CAAC;AAE9B,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,SAAkB,EAAE,QAAmC,EAAE;IACnG,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC;AAC7E,CAAC;AAED,SAAS,cAAc,CAAC,GAAY,EAAE,UAA8B;IAClE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACpD,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,GAAG;YACV,eAAe,EAAE,IAAI;YACrB,gBAAgB,EAAE,WAAW;YAC7B,MAAM,EAAE,kBAAkB;YAC1B,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtC,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,IAAI,EAAE,MAAM,KAAK,aAAa,EAAE,CAAC;QACnC,OAAO,WAAW,CAAC,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,uBAAuB,EAAE,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE;YACnH,MAAM,EAAE,kBAAkB;YAC1B,GAAG,CAAC,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9G,CAAC,CAAC;IACL,CAAC;IACD,OAAO,WAAW,CAAC,cAAc,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,SAAS,CAAC,GAA4B,EAAE,UAA8B;IAC7E,MAAM,EAAE,GAAG,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;IAC5E,IAAI,GAAG,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;QACjC,OAAO,WAAW,CAAC,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,mBAAmB,EAAE,GAAG,CAAC,SAAS,KAAK,IAAI,EAAE;YAC5G,MAAM,EAAE,cAAc;YACtB,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,WAAW,CAAC,cAAc,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IACxG,OAAO;QACL,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,GAAG,CAAC,IAAI;QACf,eAAe,EAAE,GAAG,CAAC,eAAe,KAAK,KAAK;QAC9C,gBAAgB,EAAE,cAAc;QAChC,MAAM,EAAE,GAAG,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc;QAC5D,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpG,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC3E,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;YAAE,SAAS;QAC7D,MAAM,EAAE,GAAG,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAChF,MAAM,YAAY,GAAwB,EAAE,CAAC;QAC7C,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC9E,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,CAAC,WAAW,KAAK,QAAQ;gBAAE,SAAS;YACxD,YAAY,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9E,CAAC;QACD,WAAW,CAAC,IAAI,CAAC;YACf,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,YAAY,EAAE,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI;YAChF,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,UAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/E,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1C,qBAAqB,EAAE,cAAc,CAAC,KAAK,CAAC,qBAAqB,EAAE,EAAE,CAAC;YACtE,UAAU,EAAE,EAAE;YACd,YAAY;SACb,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;SAC3B,MAAM,CAAC,CAAC,GAAG,EAAkC,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC;SAC7F,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACb,IAAI,EAAE,GAAG,CAAC,IAAc;QACxB,GAAG,CAAC,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,GAAG,CAAC,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACrE,CAAC,CAAC,CAAC;IACN,OAAO;QACL,UAAU;QACV,EAAE,EAAE,IAAI,CAAC,EAAE,KAAK,KAAK;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO,KAAK,IAAI;QAC9B,kBAAkB,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC;YACxD,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC;YAC9E,CAAC,CAAC,EAAE;QACN,MAAM;QACN,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAkB;IAClC,OAAO,GAAG,KAAK,CAAC,KAAK,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,QAAkB,EAAE,KAAK,GAAG,cAAc;IAC5E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwF,CAAC;IAE9G,KAAK,UAAU,IAAI,CAAC,KAAkB;QACpC,IAAI,CAAC;YACH,OAAO,iBAAiB,CAAC,MAAM,QAAQ,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,aAAa,IAAI,CAAC,GAAG,CAAC,UAAU,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,CAAC,EAAE,CAAC;gBACvF,MAAM,IAAI,QAAQ,CAAC,GAAG,EAAE,wBAAwB,EAAE,6CAA6C,EAAE;oBAC/F,OAAO,EAAE,QAAQ;iBAClB,CAAC,CAAC;YACL,CAAC;YACD,IAAI,GAAG,YAAY,aAAa,IAAI,YAAY,CAAC,GAAG,CAAC,KAAK,wBAAwB,EAAE,CAAC;gBACnF,MAAM,IAAI,QAAQ,CAAC,GAAG,EAAE,sBAAsB,EAAE,sCAAsC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/G,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,EAAE;YAC3B,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC5B,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;gBAC5B,IAAI,GAAG,CAAC,OAAO;oBAAE,OAAO,GAAG,CAAC,OAAO,CAAC;gBACpC,IAAI,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,KAAK;oBAAE,OAAO,GAAG,CAAC,QAAQ,CAAC;YACvE,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YAClG,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;gBAC/B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC7C,OAAO,QAAQ,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,EAAE,QAAQ;oBAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;;oBACrE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACvB,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QACD,IAAI,CAAC,KAAK;YACR,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;YACvC,IAAI,CAAC,GAAG,EAAE,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,KAAK;gBAAE,OAAO,IAAI,CAAC;YAChE,OAAO,GAAG,CAAC,QAAQ,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { type ContentTypeView } from "../catalog.js";
|
|
3
|
+
import type { V1Deps } from "./context.js";
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
export declare function projectContentRow(row: Row, ct: ContentTypeView, data: unknown, include: Set<string>): {
|
|
6
|
+
redactedMetadata: string[];
|
|
7
|
+
descriptorCoverage: import("../project.js").DescriptorCoverage;
|
|
8
|
+
undeclaredPaths: string[];
|
|
9
|
+
metadata?: Record<string, unknown> | undefined;
|
|
10
|
+
data: unknown;
|
|
11
|
+
source: {
|
|
12
|
+
sources: any[];
|
|
13
|
+
recordedAt?: string | undefined;
|
|
14
|
+
basis?: string | undefined;
|
|
15
|
+
ingress?: string | undefined;
|
|
16
|
+
service?: string | undefined;
|
|
17
|
+
status: "declared";
|
|
18
|
+
} | {
|
|
19
|
+
status: "unavailable";
|
|
20
|
+
};
|
|
21
|
+
timestamps: {
|
|
22
|
+
writtenAt?: string | undefined;
|
|
23
|
+
modifiedAt?: string | undefined;
|
|
24
|
+
createdAt?: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
state: "deleted" | "active";
|
|
27
|
+
deletion: {
|
|
28
|
+
deletedAt?: string | undefined;
|
|
29
|
+
} | null;
|
|
30
|
+
history: {
|
|
31
|
+
items?: any[] | undefined;
|
|
32
|
+
status: "present";
|
|
33
|
+
entries: number;
|
|
34
|
+
} | {
|
|
35
|
+
status: "unspecified";
|
|
36
|
+
};
|
|
37
|
+
discriminatorValue?: string | null | undefined;
|
|
38
|
+
revision: number | null;
|
|
39
|
+
head: boolean;
|
|
40
|
+
dataCategory?: string | undefined;
|
|
41
|
+
format: string;
|
|
42
|
+
recordId: string;
|
|
43
|
+
objectType: string | undefined;
|
|
44
|
+
contentType: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function registerContentRoutes(app: FastifyInstance, deps: V1Deps): void;
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/v1/content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAC/D,OAAO,EAIL,KAAK,eAAe,EAGrB,MAAM,eAAe,CAAC;AAMvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAI3C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAsCnC,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,EACR,EAAE,EAAE,eAAe,EACnB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDrB;AAiFD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAoI9E"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { findContentType, findObjectType, isConflicted, } from "../catalog.js";
|
|
2
|
+
import { ApiError, UpstreamError } from "../errors.js";
|
|
3
|
+
import { descriptorCoverage, undeclaredPaths } from "../project.js";
|
|
4
|
+
import { collectReferenceTargets, expandReferenceData, loadResolvedReferences } from "../refs.js";
|
|
5
|
+
import { scopeBody } from "../scope.js";
|
|
6
|
+
import { asRecord, upstreamCode } from "../upstream.js";
|
|
7
|
+
import { boolQuery, csv, decodeSegments, jsonQuery, limitOf, queryOf, rawPathAfter, text } from "./http.js";
|
|
8
|
+
import { refsModeOf, wrapReferenceStatus } from "./records.js";
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function readPath(root, path) {
|
|
13
|
+
let current = root;
|
|
14
|
+
for (const segment of path.split(".").filter(Boolean)) {
|
|
15
|
+
if (!isRecord(current))
|
|
16
|
+
return undefined;
|
|
17
|
+
current = current[segment];
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
function resolveTypes(catalog, objectType, contentType) {
|
|
22
|
+
const view = findObjectType(catalog, objectType);
|
|
23
|
+
if (!view)
|
|
24
|
+
throw new ApiError(404, "READ_NOT_FOUND", `Unknown object type ${objectType}`);
|
|
25
|
+
if (isConflicted(view)) {
|
|
26
|
+
const agents = (view.alternates ?? []).map((alt) => alt.definedByAgentId).join(", ");
|
|
27
|
+
throw new ApiError(409, "METADATA_CONFLICT", `Agents ${agents} define object type ${objectType} differently. Select one of them.`);
|
|
28
|
+
}
|
|
29
|
+
const ct = findContentType(view, contentType);
|
|
30
|
+
if (!ct)
|
|
31
|
+
throw new ApiError(404, "READ_NOT_FOUND", `Unknown content type ${contentType} on ${objectType}`);
|
|
32
|
+
return { view, ct };
|
|
33
|
+
}
|
|
34
|
+
/** Normalize a filter to the `{ field, op, value }[]` form. */
|
|
35
|
+
function filterList(raw) {
|
|
36
|
+
if (raw === undefined)
|
|
37
|
+
return [];
|
|
38
|
+
if (Array.isArray(raw))
|
|
39
|
+
return raw.filter(isRecord);
|
|
40
|
+
if (isRecord(raw)) {
|
|
41
|
+
if (typeof raw.field === "string")
|
|
42
|
+
return [raw];
|
|
43
|
+
return Object.entries(raw).map(([field, value]) => ({ field, op: "eq", value }));
|
|
44
|
+
}
|
|
45
|
+
throw new ApiError(400, "VALIDATION", "filter must be an array of { field, op, value }");
|
|
46
|
+
}
|
|
47
|
+
export function projectContentRow(row, ct, data, include) {
|
|
48
|
+
const system = isRecord(row._system) ? row._system : {};
|
|
49
|
+
const datasource = isRecord(system._datasource) ? system._datasource : null;
|
|
50
|
+
const metadata = isRecord(row._metadata) ? row._metadata : null;
|
|
51
|
+
const history = Array.isArray(row._history) ? row._history : null;
|
|
52
|
+
const fields = ct.fields;
|
|
53
|
+
const undeclared = undeclaredPaths(row.data, fields);
|
|
54
|
+
const deleted = system.state === "deleted";
|
|
55
|
+
const discriminatorValue = ct.discriminator ? readPath(row, ct.discriminator.path) : undefined;
|
|
56
|
+
const redacted = (ct.privateMetadata ?? []).filter((key) => !metadata || !(key in metadata));
|
|
57
|
+
return {
|
|
58
|
+
format: typeof row.format === "string" ? row.format : "memorix-record/2",
|
|
59
|
+
recordId: typeof row.recordId === "string" ? row.recordId : "",
|
|
60
|
+
objectType: typeof row.objectType === "string" ? row.objectType : undefined,
|
|
61
|
+
contentType: typeof row.contentType === "string" ? row.contentType : ct.id,
|
|
62
|
+
...(typeof row.dataCategory === "string" ? { dataCategory: row.dataCategory } : {}),
|
|
63
|
+
revision: typeof row.revision === "number" ? row.revision : null,
|
|
64
|
+
head: true,
|
|
65
|
+
...(ct.discriminator
|
|
66
|
+
? { discriminatorValue: typeof discriminatorValue === "string" ? discriminatorValue : null }
|
|
67
|
+
: {}),
|
|
68
|
+
data,
|
|
69
|
+
source: datasource
|
|
70
|
+
? {
|
|
71
|
+
status: "declared",
|
|
72
|
+
...(typeof datasource.service === "string" ? { service: datasource.service } : {}),
|
|
73
|
+
...(typeof datasource.ingress === "string" ? { ingress: datasource.ingress } : {}),
|
|
74
|
+
...(typeof datasource.basis === "string" ? { basis: datasource.basis } : {}),
|
|
75
|
+
...(typeof datasource.recordedAt === "string" ? { recordedAt: datasource.recordedAt } : {}),
|
|
76
|
+
sources: Array.isArray(datasource.sources) ? datasource.sources : [],
|
|
77
|
+
}
|
|
78
|
+
: { status: "unavailable" },
|
|
79
|
+
timestamps: {
|
|
80
|
+
...(typeof system.createdAt === "string" ? { createdAt: system.createdAt } : {}),
|
|
81
|
+
...(typeof system.modifiedAt === "string" ? { modifiedAt: system.modifiedAt } : {}),
|
|
82
|
+
...(typeof system.writtenAt === "string" ? { writtenAt: system.writtenAt } : {}),
|
|
83
|
+
},
|
|
84
|
+
state: deleted ? "deleted" : "active",
|
|
85
|
+
deletion: deleted
|
|
86
|
+
? {
|
|
87
|
+
...(typeof system.deletedAt === "string" ? { deletedAt: system.deletedAt } : {}),
|
|
88
|
+
...(isRecord(system.deletion) ? system.deletion : {}),
|
|
89
|
+
}
|
|
90
|
+
: null,
|
|
91
|
+
history: history
|
|
92
|
+
? { status: "present", entries: history.length, ...(include.has("history") ? { items: history } : {}) }
|
|
93
|
+
: { status: "unspecified" },
|
|
94
|
+
...(metadata ? { metadata } : {}),
|
|
95
|
+
redactedMetadata: redacted,
|
|
96
|
+
descriptorCoverage: descriptorCoverage(fields, undeclared),
|
|
97
|
+
undeclaredPaths: undeclared,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function projectRows(deps, scope, catalog, ct, rows, refs, include) {
|
|
101
|
+
const fieldMap = new Map([[ct.id, ct.fields]]);
|
|
102
|
+
const attempted = new Set();
|
|
103
|
+
const resolved = await loadResolvedReferences(deps.upstream, scope, catalog, collectReferenceTargets({ [ct.id]: rows }, fieldMap, "envelope"), attempted);
|
|
104
|
+
return rows.map((row) => projectContentRow(row, ct, refs === "status"
|
|
105
|
+
? wrapReferenceStatus(row.data ?? null, ct.fields, resolved, attempted)
|
|
106
|
+
: expandReferenceData(row.data ?? null, ct.fields, resolved), include));
|
|
107
|
+
}
|
|
108
|
+
function includeSet(raw) {
|
|
109
|
+
return new Set(csv(raw) ?? []);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Every current head of one record and content type, ordered by discriminator value.
|
|
113
|
+
* Memorix refuses a `recordId` filter on a discriminated list, so the record is composed.
|
|
114
|
+
*/
|
|
115
|
+
async function recordHeads(upstream, scope, ct, recordId) {
|
|
116
|
+
let body;
|
|
117
|
+
try {
|
|
118
|
+
body = asRecord(await upstream.post("/api/data/compose", scope, { explicit: true, recordId, contentTypes: [ct.id] }));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err instanceof UpstreamError && err.statusCode === 404)
|
|
122
|
+
return [];
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
const rows = asRecord(asRecord(body?.composition)?.contents)?.[ct.id];
|
|
126
|
+
const heads = (Array.isArray(rows) ? rows : []).filter(isRecord);
|
|
127
|
+
if (!ct.discriminator)
|
|
128
|
+
return heads;
|
|
129
|
+
const path = ct.discriminator.path;
|
|
130
|
+
return heads.sort((a, b) => String(readPath(a, path) ?? "").localeCompare(String(readPath(b, path) ?? "")));
|
|
131
|
+
}
|
|
132
|
+
async function storedDiscriminatorValues(upstream, scope, ct, recordId) {
|
|
133
|
+
if (!ct.discriminator)
|
|
134
|
+
return [];
|
|
135
|
+
try {
|
|
136
|
+
return (await recordHeads(upstream, scope, ct, recordId))
|
|
137
|
+
.map((row) => readPath(row, ct.discriminator.path))
|
|
138
|
+
.filter((value) => typeof value === "string");
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function singlePath(req) {
|
|
145
|
+
const raw = rawPathAfter(req, "/api/v1/content/") ?? "";
|
|
146
|
+
const [objectType, contentType, ...rest] = raw.split("/");
|
|
147
|
+
const recordId = decodeSegments(rest.join("/").replace(/\/$/, ""));
|
|
148
|
+
if (!objectType || !contentType || !recordId) {
|
|
149
|
+
throw new ApiError(400, "VALIDATION", "path is /api/v1/content/{objectType}/{contentType}/{recordId}");
|
|
150
|
+
}
|
|
151
|
+
return { objectType: decodeSegments(objectType), contentType: decodeSegments(contentType), recordId };
|
|
152
|
+
}
|
|
153
|
+
export function registerContentRoutes(app, deps) {
|
|
154
|
+
app.get("/api/v1/content", async (req) => {
|
|
155
|
+
const scope = await deps.scopeFor(req);
|
|
156
|
+
const query = queryOf(req);
|
|
157
|
+
const objectType = text(query.objectType);
|
|
158
|
+
const contentType = text(query.contentType);
|
|
159
|
+
if (!objectType || !contentType)
|
|
160
|
+
throw new ApiError(400, "VALIDATION", "objectType and contentType are required");
|
|
161
|
+
const catalog = await deps.loadCatalog(deps.upstream, scope);
|
|
162
|
+
const { ct } = resolveTypes(catalog, objectType, contentType);
|
|
163
|
+
const recordId = text(query.recordId);
|
|
164
|
+
const discriminatorValue = text(query.discriminatorValue);
|
|
165
|
+
const includeDeleted = boolQuery(query.includeDeleted, "includeDeleted");
|
|
166
|
+
const filters = filterList(jsonQuery(query.filter, "filter"));
|
|
167
|
+
const sort = jsonQuery(query.sort, "sort");
|
|
168
|
+
const counting = boolQuery(query.count, "count");
|
|
169
|
+
if (recordId && !counting) {
|
|
170
|
+
if (filters.length > 0 || sort !== undefined || text(query.cursor)) {
|
|
171
|
+
throw new ApiError(400, "VALIDATION", "filter, sort, and cursor are not allowed with recordId; use discriminatorValue");
|
|
172
|
+
}
|
|
173
|
+
let heads = await recordHeads(deps.upstream, scope, ct, recordId);
|
|
174
|
+
if (discriminatorValue && ct.discriminator) {
|
|
175
|
+
heads = heads.filter((row) => readPath(row, ct.discriminator.path) === discriminatorValue);
|
|
176
|
+
}
|
|
177
|
+
if (!includeDeleted)
|
|
178
|
+
heads = heads.filter((row) => !isRecord(row._system) || row._system.state !== "deleted");
|
|
179
|
+
return {
|
|
180
|
+
scope: scopeBody(scope),
|
|
181
|
+
objectType,
|
|
182
|
+
contentType,
|
|
183
|
+
addressing: {
|
|
184
|
+
orgId: scope.orgId,
|
|
185
|
+
objectType,
|
|
186
|
+
contentType,
|
|
187
|
+
recordId,
|
|
188
|
+
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
189
|
+
...(ct.discriminator ? { discriminator: ct.discriminator.path } : {}),
|
|
190
|
+
},
|
|
191
|
+
pageInfo: { complete: true, limit: heads.length },
|
|
192
|
+
truncated: false,
|
|
193
|
+
rows: await projectRows(deps, scope, catalog, ct, heads, refsModeOf(query.refs, "status"), includeSet(query.include)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (recordId)
|
|
197
|
+
filters.push({ field: "recordId", op: "in", value: [recordId] });
|
|
198
|
+
const params = new URLSearchParams({ objectType, contentType });
|
|
199
|
+
if (filters.length > 0)
|
|
200
|
+
params.set("filter", JSON.stringify(filters));
|
|
201
|
+
if (includeDeleted)
|
|
202
|
+
params.set("includeDeleted", "true");
|
|
203
|
+
if (discriminatorValue)
|
|
204
|
+
params.set("discriminatorValue", discriminatorValue);
|
|
205
|
+
const addressing = {
|
|
206
|
+
orgId: scope.orgId,
|
|
207
|
+
objectType,
|
|
208
|
+
contentType,
|
|
209
|
+
recordId: recordId ?? null,
|
|
210
|
+
...(discriminatorValue ? { discriminatorValue } : {}),
|
|
211
|
+
...(ct.discriminator ? { discriminator: ct.discriminator.path } : {}),
|
|
212
|
+
};
|
|
213
|
+
if (counting) {
|
|
214
|
+
params.set("count", "true");
|
|
215
|
+
try {
|
|
216
|
+
const body = asRecord(await deps.upstream.get(`/api/data/content?${params.toString()}`, scope));
|
|
217
|
+
const count = typeof body?.count === "number" ? body.count : null;
|
|
218
|
+
return {
|
|
219
|
+
scope: scopeBody(scope),
|
|
220
|
+
count,
|
|
221
|
+
countStatus: count === null ? "unavailable" : "exact",
|
|
222
|
+
...(count === null ? { reason: "NOT_REPORTED" } : {}),
|
|
223
|
+
addressing,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (!(err instanceof UpstreamError) || err.statusCode >= 500)
|
|
228
|
+
throw err;
|
|
229
|
+
const code = upstreamCode(err) ?? `HTTP_${err.statusCode}`;
|
|
230
|
+
if (err.statusCode === 400 && code !== "NOT_INDEXED" && !/INDEX/.test(code))
|
|
231
|
+
throw err;
|
|
232
|
+
return { scope: scopeBody(scope), count: null, countStatus: "unavailable", reason: "NOT_INDEXED", addressing };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const limit = limitOf(query.limit);
|
|
236
|
+
params.set("limit", String(limit));
|
|
237
|
+
params.set("sort", JSON.stringify(sort ?? [{ field: "recordId", direction: "asc" }]));
|
|
238
|
+
const cursor = text(query.cursor);
|
|
239
|
+
if (cursor)
|
|
240
|
+
params.set("cursor", cursor);
|
|
241
|
+
const body = asRecord(await deps.upstream.get(`/api/data/content?${params.toString()}`, scope));
|
|
242
|
+
const rows = (Array.isArray(body?.records) ? body.records : []).filter(isRecord);
|
|
243
|
+
const nextCursor = asRecord(body?.pageInfo)?.nextCursor;
|
|
244
|
+
const next = typeof nextCursor === "string" && nextCursor ? nextCursor : null;
|
|
245
|
+
return {
|
|
246
|
+
scope: scopeBody(scope),
|
|
247
|
+
objectType,
|
|
248
|
+
contentType,
|
|
249
|
+
addressing,
|
|
250
|
+
pageInfo: { complete: next === null, ...(next ? { nextCursor: next } : {}), limit },
|
|
251
|
+
truncated: false,
|
|
252
|
+
rows: await projectRows(deps, scope, catalog, ct, rows, refsModeOf(query.refs, "status"), includeSet(query.include)),
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
app.get("/api/v1/content/:objectType/:contentType/*", async (req) => {
|
|
256
|
+
const scope = await deps.scopeFor(req);
|
|
257
|
+
const { objectType, contentType, recordId } = singlePath(req);
|
|
258
|
+
const query = queryOf(req);
|
|
259
|
+
const catalog = await deps.loadCatalog(deps.upstream, scope);
|
|
260
|
+
const { ct } = resolveTypes(catalog, objectType, contentType);
|
|
261
|
+
const discriminatorValue = text(query.discriminatorValue);
|
|
262
|
+
if (ct.discriminator && !discriminatorValue && !ct.discriminator.defaultValue) {
|
|
263
|
+
const stored = await storedDiscriminatorValues(deps.upstream, scope, ct, recordId);
|
|
264
|
+
throw new ApiError(400, "VALIDATION", `discriminatorValue is required for ${contentType} (${ct.discriminator.path})`, {
|
|
265
|
+
details: { parameter: "discriminatorValue", storedValues: stored },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const params = new URLSearchParams({ objectType, contentType, recordId });
|
|
269
|
+
if (discriminatorValue)
|
|
270
|
+
params.set("discriminatorValue", discriminatorValue);
|
|
271
|
+
if (boolQuery(query.includeDeleted, "includeDeleted"))
|
|
272
|
+
params.set("includeDeleted", "true");
|
|
273
|
+
let record = null;
|
|
274
|
+
try {
|
|
275
|
+
const body = asRecord(await deps.upstream.get(`/api/data/content?${params.toString()}`, scope));
|
|
276
|
+
record = asRecord(body?.record);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (!(err instanceof UpstreamError) || err.statusCode !== 404)
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
if (!record) {
|
|
283
|
+
const stored = await storedDiscriminatorValues(deps.upstream, scope, ct, recordId);
|
|
284
|
+
throw new ApiError(404, "READ_NOT_FOUND", stored.length > 0
|
|
285
|
+
? `No ${contentType} head for ${recordId} with ${ct.discriminator?.path} = ${discriminatorValue}. Stored values: ${stored.join(", ")}`
|
|
286
|
+
: `No ${contentType} head for ${recordId}`, stored.length > 0 ? { details: { storedValues: stored } } : {});
|
|
287
|
+
}
|
|
288
|
+
const [row] = await projectRows(deps, scope, catalog, ct, [record], refsModeOf(query.refs, "status"), includeSet(query.include));
|
|
289
|
+
return { scope: scopeBody(scope), row };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=content.js.map
|