@x12i/memorix-client-api 0.17.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 +14 -3
- 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 +3 -3
- package/dist/peers.d.ts.map +1 -1
- package/dist/peers.js +5 -5
- 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 +23 -31
- package/dist/scoper.d.ts.map +1 -1
- package/dist/scoper.js +55 -250
- 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 -67
package/HANDOVER.md
CHANGED
|
@@ -38,19 +38,20 @@ The listen port comes from `attachCoreFastify` (`MEMORIX_CLIENT_API_PORT`, defau
|
|
|
38
38
|
| `MEMORIX_AUTH` | `header` | `header` takes the org from `x-memorix-org-id`. `authix` takes it from `Authorization: Bearer` via `@x12i/authix-sdk`. |
|
|
39
39
|
| `MGC_MCP_URL` | | MGC MCP endpoint. Wins over the source service URL. |
|
|
40
40
|
| `MGC_SOURCE_SERVICE_URL` or `MGC_URL` | | MGC source host. This process calls `{url}/mcp` when `MGC_MCP_URL` is unset. When both are unset, it reads `genericConnectors.mcpUrl` from Memorix. |
|
|
41
|
-
| `SCOPER_REASONER` | scripted, or ai-dispatcher when `OPENROUTER_API_KEY` is set | Reasoner for the hosted `@x12i/memorix-scoper@
|
|
41
|
+
| `SCOPER_REASONER` | scripted, or ai-dispatcher when `OPENROUTER_API_KEY` is set | Reasoner for the hosted `@x12i/memorix-scoper@3.0.0`. |
|
|
42
42
|
| `SCOPER_AI_MODEL` | `openai/gpt-6-luna` | Model when the reasoner is `ai-dispatcher`. Runs on `@x12i/ai-dispatcher@3.2.0`. |
|
|
43
43
|
| `SCOPER_FUNCTIONS_DIR` | | Pagx directory of saved query and scope functions, passed to `createScoper({ functions })`. |
|
|
44
44
|
| `SCOPER_FUNCTIONS_PACKAGE` | | Parent package name for that directory. |
|
|
45
45
|
| `SCOPER_FUNCTIONS_LEARN` | off | `true` asks the coder to write a function on a miss. Needs `@x12i/coder-dispatcher`. |
|
|
46
|
-
| `SCOPER_MODE` | `auto` | `auto` hosts Scoper here unless `SCOPER_URL` is healthy and `
|
|
46
|
+
| `SCOPER_MODE` | `auto` | `auto` hosts Scoper here unless `SCOPER_URL` is healthy and `3.x`. `external` requires that peer and does not load the Scoper packages. `embedded` does not probe. See `docs/scoper-downstream.md`. |
|
|
47
47
|
| `SCOPER_URL` | | Base URL of a Scoper peer. Proxied for the process lifetime when selected. |
|
|
48
48
|
| `SKILLS_MODE` | `auto` | `auto` probes skill-api and hosts agent functions here when that probe fails. `external` requires the peer. `embedded` does not probe. |
|
|
49
49
|
| `SKILL_API_URL` | `http://127.0.0.1:7180` in `auto` | Skill-api base. A selected peer answers list, run, save, and delete. |
|
|
50
50
|
| `SKILLS_PACK_DIR` | | Pack directory with `functions/catalog.json` and `functions/run.mjs`. Used when functions are hosted here. |
|
|
51
51
|
| `AGENTS_DIR` | | Agents root. A folder named for the agent, or the first pack with functions, is the catalog. |
|
|
52
52
|
| `MEMORIX_AGENT_IDS` | | Content agents that use the process pack. `PROCESS_AGENT_ID` is the fallback. |
|
|
53
|
-
| `ASSISTANT_MONGO_URI` or `MONGO_URI` | | Assistants database
|
|
53
|
+
| `ASSISTANT_MONGO_URI` or `MONGO_URI` | | Assistants database `{orgId}-memorix-assistants`: function overrides (only while functions are hosted here) and planning items (collection `planning_items`). Unset: pack functions run, save returns `503`, and planning routes are `501 CAPABILITY_UNAVAILABLE`. |
|
|
54
|
+
| `PLANNING_ASSIGNEES` | | JSON object of org id to `[{ "id", "name", "kind"? }]`. Planning assignee options. Unset: only unassigned. |
|
|
54
55
|
| `AUTHIX_APP_ID` | | Required with `MEMORIX_AUTH=authix`. |
|
|
55
56
|
| `AUTHIX_APP_SECRET_KEY` | | Required with `MEMORIX_AUTH=authix`. |
|
|
56
57
|
| `AUTHIX_INTROSPECT_URL` | | Optional revocation check, for example `http://127.0.0.1:3100/v1/tokens/introspect`. |
|
|
@@ -63,6 +64,16 @@ npm run typecheck
|
|
|
63
64
|
npm run build && npm start
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
The `visualizer` contract (`docs/visualizer-api.md`) is served from `src/v1/`. Every `/api/v1` route checks the requested agents against `GET /api/metadata/agents?scope=org` (reused for 30 seconds): `*` is the org's deployed agents, and an agent the org has not deployed is `403 AGENT_NOT_DEPLOYED`. Against a Memorix without the org agent record, agent ids pass through unchanged.
|
|
68
|
+
|
|
69
|
+
## Release
|
|
70
|
+
|
|
71
|
+
1. Bump `package.json` and `package-lock.json` (`npm version <v> --no-git-tag-version`), `openapi/openapi.json` `info.version`, and the version lines in the root `README.md`. The read client also bumps `clientInfo.version` in `src/index.ts`. `memorix-client` adds a `CHANGELOG.md` entry.
|
|
72
|
+
2. In each package: `npm test`, `npm run typecheck`, `npm run build`. Check that `npm view <package>@<version>` is a 404, then `npm pack --dry-run`.
|
|
73
|
+
3. Publish `memorix-client` first, then this server (which depends on it), then the read client: `npm publish --access public`.
|
|
74
|
+
4. Commit and push.
|
|
75
|
+
5. On the host, stop the listener on 5120 and start `npm run dev` in this folder again. Check `/health`, `/ready`, `/api/openapi.json` (`info.version`), and `/api/v1/capabilities` (401 without a token).
|
|
76
|
+
|
|
66
77
|
## What must already be on the Memorix host
|
|
67
78
|
|
|
68
79
|
These stay in the Memorix repo. This API calls them over HTTP.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Upstream } from "./upstream.js";
|
|
2
|
+
export type AgentEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
ok: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
objectTypes: Array<{
|
|
7
|
+
id: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
contentTypes: string[];
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export type AgentDirectory = {
|
|
13
|
+
/**
|
|
14
|
+
* Agents recorded for the org (`GET /api/metadata/agents?scope=org`).
|
|
15
|
+
* `null` means Memorix has no org agent record (service older than 3.18.0).
|
|
16
|
+
*/
|
|
17
|
+
deployed(orgId: string, caller: string): Promise<AgentEntry[] | null>;
|
|
18
|
+
/** Every pack installed on the platform. */
|
|
19
|
+
installed(orgId: string, caller: string): Promise<AgentEntry[]>;
|
|
20
|
+
};
|
|
21
|
+
export declare function agentsFromListing(body: unknown): AgentEntry[];
|
|
22
|
+
/** A Memorix without an org agent record: agent ids are sent as requested and `*` is not expanded. */
|
|
23
|
+
export declare const NO_ORG_AGENT_RECORD: AgentDirectory;
|
|
24
|
+
export declare function createAgentDirectory(upstream: Upstream, ttlMs?: number): AgentDirectory;
|
|
25
|
+
//# sourceMappingURL=agent-directory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-directory.d.ts","sourceRoot":"","sources":["../src/agent-directory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAExD,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3E,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC;IACtE,4CAA4C;IAC5C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CACjE,CAAC;AAIF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,EAAE,CA2B7D;AAED,sGAAsG;AACtG,eAAO,MAAM,mBAAmB,EAAE,cAGjC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,SAAiB,GAAG,cAAc,CAoC/F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { UpstreamError } from "./errors.js";
|
|
2
|
+
import { asRecord } from "./upstream.js";
|
|
3
|
+
const DEFAULT_TTL_MS = 30_000;
|
|
4
|
+
export function agentsFromListing(body) {
|
|
5
|
+
const agents = asRecord(body)?.agents;
|
|
6
|
+
if (!Array.isArray(agents))
|
|
7
|
+
return [];
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const item of agents) {
|
|
10
|
+
const row = asRecord(item);
|
|
11
|
+
if (!row || typeof row.id !== "string" || !row.id)
|
|
12
|
+
continue;
|
|
13
|
+
const objectTypes = Array.isArray(row.objectTypes)
|
|
14
|
+
? row.objectTypes
|
|
15
|
+
.map((entry) => asRecord(entry))
|
|
16
|
+
.filter((entry) => entry !== null && typeof entry.id === "string")
|
|
17
|
+
.map((entry) => ({
|
|
18
|
+
id: entry.id,
|
|
19
|
+
...(typeof entry.name === "string" ? { name: entry.name } : {}),
|
|
20
|
+
contentTypes: Array.isArray(entry.contentTypes)
|
|
21
|
+
? entry.contentTypes.filter((ct) => typeof ct === "string")
|
|
22
|
+
: [],
|
|
23
|
+
}))
|
|
24
|
+
: [];
|
|
25
|
+
out.push({
|
|
26
|
+
id: row.id,
|
|
27
|
+
ok: row.ok !== false,
|
|
28
|
+
error: typeof row.error === "string" ? row.error : null,
|
|
29
|
+
objectTypes,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
33
|
+
}
|
|
34
|
+
/** A Memorix without an org agent record: agent ids are sent as requested and `*` is not expanded. */
|
|
35
|
+
export const NO_ORG_AGENT_RECORD = {
|
|
36
|
+
deployed: async () => null,
|
|
37
|
+
installed: async () => [],
|
|
38
|
+
};
|
|
39
|
+
export function createAgentDirectory(upstream, ttlMs = DEFAULT_TTL_MS) {
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
function cached(key, load) {
|
|
42
|
+
const hit = cache.get(key);
|
|
43
|
+
if (hit && Date.now() - hit.at < ttlMs)
|
|
44
|
+
return hit.value;
|
|
45
|
+
const value = load();
|
|
46
|
+
cache.set(key, { at: Date.now(), value });
|
|
47
|
+
value.catch(() => cache.delete(key));
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function listingScope(orgId, caller) {
|
|
51
|
+
return { orgId, agentIds: ["*"], caller };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
deployed(orgId, caller) {
|
|
55
|
+
return cached(`org:${orgId}`, async () => {
|
|
56
|
+
try {
|
|
57
|
+
const body = await upstream.get("/api/metadata/agents?scope=org", listingScope(orgId, caller));
|
|
58
|
+
if (!Array.isArray(asRecord(body)?.agents))
|
|
59
|
+
return null;
|
|
60
|
+
return agentsFromListing(body);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof UpstreamError && (err.statusCode === 404 || err.statusCode === 501))
|
|
64
|
+
return null;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
async installed(orgId, caller) {
|
|
70
|
+
const value = await cached("platform", async () => agentsFromListing(await upstream.get("/api/metadata/agents", listingScope(orgId, caller))));
|
|
71
|
+
return value ?? [];
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=agent-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-directory.js","sourceRoot":"","sources":["../src/agent-directory.ts"],"names":[],"mappings":"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"}
|
package/dist/app.d.ts
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import { type FastifyInstance } from "fastify";
|
|
2
2
|
import { type ScoperReasonerKind } from "./config.js";
|
|
3
3
|
import type { PeerChoice } from "./peers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { type ScoperHost } from "./scoper.js";
|
|
5
|
+
import type { createScoperHost } from "./scoper-embedded.js";
|
|
5
6
|
import { type EmbeddedSkillsOptions, type SkillsHost } from "./skills.js";
|
|
7
|
+
import { type AgentDirectory } from "./agent-directory.js";
|
|
8
|
+
import type { TokenClaims } from "./authix.js";
|
|
9
|
+
import { type PlanningStore } from "./v1/planning-store.js";
|
|
6
10
|
export type ClientApiAuth = {
|
|
7
11
|
mode: "header";
|
|
8
12
|
} | {
|
|
9
13
|
mode: "authix";
|
|
10
|
-
verify: (token: string) => Promise<
|
|
11
|
-
organizationIds: string[];
|
|
12
|
-
}>;
|
|
14
|
+
verify: (token: string) => Promise<TokenClaims>;
|
|
13
15
|
};
|
|
14
16
|
export type ClientApiOptions = {
|
|
15
17
|
memorixUrl: string;
|
|
16
18
|
fetchImpl?: typeof fetch;
|
|
17
19
|
/** Default `header`: org comes from `x-memorix-org-id`. */
|
|
18
20
|
auth?: ClientApiAuth;
|
|
21
|
+
/** Upstream timeout. Default 30000 ms. */
|
|
22
|
+
upstreamTimeoutMs?: number;
|
|
23
|
+
/** How long the org's deployed agent list is reused. Default 30000 ms. */
|
|
24
|
+
agentCacheMs?: number;
|
|
25
|
+
/** Deployed and installed agents. Default reads `GET /api/metadata/agents` on Memorix. */
|
|
26
|
+
agentDirectory?: AgentDirectory;
|
|
27
|
+
/** How long a census is reused per org and agent set. Default 60000 ms. */
|
|
28
|
+
censusCacheMs?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Planning items. `store` is a test double. `mongoUri` keeps items in `{orgId}-memorix-assistants.planning_items`.
|
|
31
|
+
* Neither means planning routes are `501 CAPABILITY_UNAVAILABLE`.
|
|
32
|
+
*/
|
|
33
|
+
planning?: {
|
|
34
|
+
store?: PlanningStore;
|
|
35
|
+
mongoUri?: string;
|
|
36
|
+
/** Assignee options per org. Absent means only unassigned is valid. */
|
|
37
|
+
assignees?: Record<string, Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
kind?: string;
|
|
41
|
+
}>>;
|
|
42
|
+
};
|
|
19
43
|
/** Listen port. Default `MEMORIX_CLIENT_API_PORT` or 5120. */
|
|
20
44
|
port?: number;
|
|
21
45
|
/**
|
|
@@ -24,8 +48,9 @@ export type ClientApiOptions = {
|
|
|
24
48
|
*/
|
|
25
49
|
mgcMcpUrl?: string | null;
|
|
26
50
|
/**
|
|
27
|
-
* Scoper
|
|
28
|
-
* and no saved functions. The CLI passes the process env.
|
|
51
|
+
* Scoper 3.0.0 hosted in this process, or a peer chosen at startup. The library default is the
|
|
52
|
+
* scripted reasoner and no saved functions. The CLI passes the process env.
|
|
53
|
+
* The Scoper packages are loaded only when this process hosts Scoper.
|
|
29
54
|
*/
|
|
30
55
|
scoper?: {
|
|
31
56
|
reasoner?: ScoperReasonerKind;
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,eAAe,EAAuB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,eAAe,EAAuB,MAAM,SAAS,CAAC;AAY7E,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AActE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAkD,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAWrB,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAMtF,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,0CAA0C;IAC1C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,aAAa,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,uEAAuE;QACvE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC,CAAC;KAChF,CAAC;IACF,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,kBAAkB,CAAC;QAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;QACxB,2EAA2E;QAC3E,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,mEAAmE;QACnE,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,8CAA8C;QAC9C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;QACzB,0FAA0F;QAC1F,UAAU,CAAC,EAAE,OAAO,gBAAgB,CAAC;KACtC,CAAC;IACF;;;OAGG;IACH,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAC;KACtD,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAsK3D,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CA4dnF"}
|
package/dist/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import { attachCoreFastify } from "@x12i/core-service/fastify";
|
|
6
|
-
import { catalogFromKinds, findContentType, findObjectType, } from "./catalog.js";
|
|
6
|
+
import { catalogFromKinds, findContentType, findObjectType, isConflicted, } from "./catalog.js";
|
|
7
7
|
import { contentTypeIsDerived } from "./derived.js";
|
|
8
8
|
import { resolveApiPort } from "./config.js";
|
|
9
9
|
import { ApiError, UpstreamError } from "./errors.js";
|
|
@@ -12,14 +12,22 @@ import { clientApiLiveRing, memorixFetch } from "./live.js";
|
|
|
12
12
|
import { withClientApiLiveNav } from "./live-ops-nav.js";
|
|
13
13
|
import { registerProcessedRoutes } from "./processed.js";
|
|
14
14
|
import { projectContents } from "./project.js";
|
|
15
|
-
import { collectReferenceTargets, loadResolvedReferences, mergeReferenceTargets } from "./refs.js";
|
|
15
|
+
import { collectReferenceTargets, loadResolvedReferences, mergeReferenceTargets, } from "./refs.js";
|
|
16
16
|
import { registerReadsLive } from "./reads-live-routes.js";
|
|
17
|
-
import {
|
|
17
|
+
import { createExternalScoperHost, registerScoperRoutes } from "./scoper.js";
|
|
18
18
|
import { createEmbeddedSkillsHost, createExternalSkillsHost, registerSkillRoutes, } from "./skills.js";
|
|
19
19
|
import { attachRequestCountex, registerRequestStatsRoute } from "./request-countex.js";
|
|
20
20
|
import { registerRefRoutes } from "./shaped-refs.js";
|
|
21
|
-
import { orgFromToken, scopeBody, scopeFromHeaders } from "./scope.js";
|
|
21
|
+
import { orgFromToken, resolveAgentSelection, scopeBody, scopeFromHeaders, } from "./scope.js";
|
|
22
22
|
import { asRecord, createUpstream, subjectDirectoryPath } from "./upstream.js";
|
|
23
|
+
import { createAgentDirectory } from "./agent-directory.js";
|
|
24
|
+
import { defaultRetryable } from "./errors.js";
|
|
25
|
+
import { createCensusService } from "./v1/census.js";
|
|
26
|
+
import { stateOf } from "./v1/context.js";
|
|
27
|
+
import { decodeSegments, rawPathAfter, requestIdFrom } from "./v1/http.js";
|
|
28
|
+
import { createMongoPlanningStore } from "./v1/planning-store.js";
|
|
29
|
+
import { registerV1Routes } from "./v1/routes.js";
|
|
30
|
+
import { recordsListExtras, refsModeOf, wrapReferenceStatus } from "./v1/records.js";
|
|
23
31
|
const openApiPath = resolve(dirname(fileURLToPath(import.meta.url)), "../openapi/openapi.json");
|
|
24
32
|
function queryOf(req) {
|
|
25
33
|
return (req.query ?? {});
|
|
@@ -62,6 +70,22 @@ function wildcard(req) {
|
|
|
62
70
|
const params = req.params;
|
|
63
71
|
return (params["*"] ?? params.path ?? "").trim();
|
|
64
72
|
}
|
|
73
|
+
function assertResolved(objectType) {
|
|
74
|
+
if (!isConflicted(objectType))
|
|
75
|
+
return;
|
|
76
|
+
const agents = (objectType.alternates ?? []).map((alt) => alt.definedByAgentId).join(", ");
|
|
77
|
+
throw new ApiError(409, "METADATA_CONFLICT", `Agents ${agents} define object type ${objectType.id} differently. Select one of them, or read the bodies on metadata/kinds/object-types.`);
|
|
78
|
+
}
|
|
79
|
+
function withReferenceStatus(contents, fieldMap, resolved, attempted) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [contentType, list] of Object.entries(contents)) {
|
|
82
|
+
const fields = fieldMap.get(contentType) ?? [];
|
|
83
|
+
out[contentType] = list.map((instance) => instance.json
|
|
84
|
+
? { ...instance, json: { data: wrapReferenceStatus(instance.json.data, fields, resolved, attempted) } }
|
|
85
|
+
: instance);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
65
89
|
function fieldsByType(objectType, contentTypes) {
|
|
66
90
|
const map = new Map();
|
|
67
91
|
for (const contentType of contentTypes) {
|
|
@@ -74,9 +98,19 @@ async function projectRead(upstream, scope, catalog, contents, fieldMap, represe
|
|
|
74
98
|
const resolved = await loadResolvedReferences(upstream, scope, catalog, targets);
|
|
75
99
|
return projectContents(contents, fieldMap, representation, shape, resolved);
|
|
76
100
|
}
|
|
77
|
-
async function
|
|
101
|
+
async function loadObjectTypeKinds(upstream, scope) {
|
|
78
102
|
const body = await upstream.get("/api/metadata/kinds/object-types", scope);
|
|
79
|
-
|
|
103
|
+
const items = asRecord(body)?.items;
|
|
104
|
+
return {
|
|
105
|
+
items: Array.isArray(items)
|
|
106
|
+
? items.filter((item) => asRecord(item) !== null)
|
|
107
|
+
: [],
|
|
108
|
+
objectTypes: catalogFromKinds(body),
|
|
109
|
+
readAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function loadCatalog(upstream, scope) {
|
|
113
|
+
return (await loadObjectTypeKinds(upstream, scope)).objectTypes;
|
|
80
114
|
}
|
|
81
115
|
async function loadSubject(upstream, scope, recordId) {
|
|
82
116
|
const body = asRecord(await upstream.get(subjectDirectoryPath(recordId), scope));
|
|
@@ -114,14 +148,27 @@ function bearerToken(headerValue) {
|
|
|
114
148
|
const match = /^Bearer\s+(\S+)\s*$/i.exec(raw.trim());
|
|
115
149
|
return match?.[1] ?? "";
|
|
116
150
|
}
|
|
151
|
+
async function loadEmbeddedScoper() {
|
|
152
|
+
try {
|
|
153
|
+
return (await import("./scoper-embedded.js")).createScoperHost;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
157
|
+
throw new Error(`Scoper cannot be hosted in this process (${message}). Set SCOPER_URL to a Scoper peer and SCOPER_MODE=external, or install @x12i/memorix-scoper.`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
117
160
|
export async function createClientApi(options) {
|
|
118
|
-
const upstream = createUpstream(options.memorixUrl, options.fetchImpl ?? memorixFetch);
|
|
161
|
+
const upstream = createUpstream(options.memorixUrl, options.fetchImpl ?? memorixFetch, options.upstreamTimeoutMs);
|
|
119
162
|
const auth = options.auth ?? { mode: "header" };
|
|
163
|
+
const directory = options.agentDirectory ?? createAgentDirectory(upstream, options.agentCacheMs);
|
|
164
|
+
const census = createCensusService(upstream, options.censusCacheMs);
|
|
165
|
+
const planning = options.planning?.store ??
|
|
166
|
+
(options.planning?.mongoUri ? createMongoPlanningStore(options.planning.mongoUri) : null);
|
|
120
167
|
const scoperPeer = options.scoper?.peer;
|
|
121
168
|
const scoperHost = options.scoper?.host ??
|
|
122
169
|
(scoperPeer?.mode === "external"
|
|
123
170
|
? createExternalScoperHost(scoperPeer, options.scoper?.fetchImpl ?? fetch)
|
|
124
|
-
: await (options.scoper?.createHost ??
|
|
171
|
+
: await (options.scoper?.createHost ?? (await loadEmbeddedScoper()))({
|
|
125
172
|
memorixUrl: options.memorixUrl,
|
|
126
173
|
reasoner: options.scoper?.reasoner ?? "scripted",
|
|
127
174
|
functionsDir: options.scoper?.functionsDir,
|
|
@@ -151,29 +198,35 @@ export async function createClientApi(options) {
|
|
|
151
198
|
disableRequestLogging: true,
|
|
152
199
|
});
|
|
153
200
|
app.addHook("onRequest", async (req, reply) => {
|
|
201
|
+
const requestId = requestIdFrom(req.headers["x-request-id"]);
|
|
202
|
+
stateOf(req).requestId = requestId;
|
|
203
|
+
reply.header("x-request-id", requestId);
|
|
154
204
|
const origin = req.headers.origin;
|
|
155
205
|
if (typeof origin === "string" && origin) {
|
|
156
206
|
reply.header("access-control-allow-origin", origin);
|
|
157
207
|
reply.header("vary", "Origin");
|
|
158
|
-
reply.header("access-control-allow-headers", "content-type, authorization, x-memorix-org-id, x-memorix-agent-ids, x-memorix-caller, x-trace-id");
|
|
159
|
-
reply.header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
208
|
+
reply.header("access-control-allow-headers", "content-type, authorization, x-memorix-org-id, x-memorix-agent-ids, x-memorix-caller, x-trace-id, x-request-id, if-match, if-none-match");
|
|
209
|
+
reply.header("access-control-allow-methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
210
|
+
reply.header("access-control-expose-headers", "etag, x-request-id, retry-after");
|
|
160
211
|
}
|
|
161
212
|
if (req.method === "OPTIONS") {
|
|
162
213
|
await reply.code(204).send();
|
|
163
214
|
}
|
|
164
215
|
});
|
|
165
|
-
async function
|
|
216
|
+
async function verifiedClaims(req) {
|
|
217
|
+
const state = stateOf(req);
|
|
218
|
+
if (state.clientClaims !== undefined)
|
|
219
|
+
return state.clientClaims;
|
|
166
220
|
if (auth.mode === "header") {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return scope;
|
|
221
|
+
state.clientClaims = null;
|
|
222
|
+
return null;
|
|
170
223
|
}
|
|
171
224
|
const token = bearerToken(req.headers.authorization);
|
|
172
225
|
if (!token)
|
|
173
226
|
throw new ApiError(401, "UNAUTHORIZED", "Authorization bearer token is required");
|
|
174
|
-
let
|
|
227
|
+
let claims;
|
|
175
228
|
try {
|
|
176
|
-
|
|
229
|
+
claims = await auth.verify(token);
|
|
177
230
|
}
|
|
178
231
|
catch (err) {
|
|
179
232
|
if (err instanceof ApiError)
|
|
@@ -181,24 +234,91 @@ export async function createClientApi(options) {
|
|
|
181
234
|
const message = err instanceof Error ? err.message : "invalid token";
|
|
182
235
|
throw new ApiError(401, "UNAUTHORIZED", message);
|
|
183
236
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
237
|
+
state.clientClaims = claims;
|
|
238
|
+
return claims;
|
|
239
|
+
}
|
|
240
|
+
async function authenticate(req) {
|
|
241
|
+
const claims = await verifiedClaims(req);
|
|
242
|
+
const raw = req.headers["x-memorix-caller"];
|
|
243
|
+
const caller = (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? "";
|
|
244
|
+
if (!caller)
|
|
245
|
+
throw new ApiError(400, "SCOPE_INVALID", "x-memorix-caller is required");
|
|
246
|
+
return { claims, caller };
|
|
247
|
+
}
|
|
248
|
+
async function scopeFor(req, scopeOptions = {}) {
|
|
249
|
+
const agents = scopeOptions.agents ?? "required";
|
|
250
|
+
const claims = await verifiedClaims(req);
|
|
251
|
+
let base;
|
|
252
|
+
if (!claims) {
|
|
253
|
+
base = scopeFromHeaders(req.headers, undefined, agents);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const requested = req.headers["x-memorix-org-id"];
|
|
257
|
+
const requestedOrg = (Array.isArray(requested) ? requested[0] : requested)?.trim() ?? "";
|
|
258
|
+
base = scopeFromHeaders(req.headers, orgFromToken(claims.organizationIds, requestedOrg), agents);
|
|
259
|
+
}
|
|
260
|
+
const scope = await resolveAgentSelection(base, directory, agents);
|
|
261
|
+
stateOf(req).clientScope = scope;
|
|
188
262
|
return scope;
|
|
189
263
|
}
|
|
190
|
-
app.setErrorHandler((err,
|
|
264
|
+
app.setErrorHandler((err, req, reply) => {
|
|
265
|
+
const requestId = stateOf(req).requestId;
|
|
191
266
|
if (err instanceof ApiError) {
|
|
267
|
+
for (const [name, value] of Object.entries(err.extra.headers ?? {}))
|
|
268
|
+
reply.header(name, value);
|
|
192
269
|
return reply.status(err.statusCode).send({
|
|
193
|
-
error: {
|
|
270
|
+
error: {
|
|
271
|
+
code: err.code,
|
|
272
|
+
message: err.message,
|
|
273
|
+
requestId,
|
|
274
|
+
retryable: err.extra.retryable ?? defaultRetryable(err.statusCode),
|
|
275
|
+
...(err.extra.feature ? { feature: err.extra.feature } : {}),
|
|
276
|
+
...(err.extra.details ? { details: err.extra.details } : {}),
|
|
277
|
+
...(err.extra.current !== undefined ? { current: err.extra.current } : {}),
|
|
278
|
+
},
|
|
194
279
|
});
|
|
195
280
|
}
|
|
196
281
|
if (err instanceof UpstreamError) {
|
|
197
|
-
|
|
282
|
+
if (err.statusCode === 429) {
|
|
283
|
+
if (err.retryAfter)
|
|
284
|
+
reply.header("retry-after", err.retryAfter);
|
|
285
|
+
return reply.status(429).send({
|
|
286
|
+
error: { code: "UPSTREAM_RATE_LIMITED", message: "Memorix limited the call", requestId, retryable: true },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const body = asRecord(err.body);
|
|
290
|
+
const error = asRecord(body?.error);
|
|
291
|
+
if (body && error) {
|
|
292
|
+
return reply.status(err.statusCode).send({
|
|
293
|
+
...body,
|
|
294
|
+
error: {
|
|
295
|
+
...error,
|
|
296
|
+
requestId,
|
|
297
|
+
retryable: typeof error.retryable === "boolean" ? error.retryable : defaultRetryable(err.statusCode),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return reply.status(err.statusCode).send(err.body ?? {
|
|
302
|
+
error: { code: "UPSTREAM", message: err.message, requestId, retryable: defaultRetryable(err.statusCode) },
|
|
303
|
+
});
|
|
198
304
|
}
|
|
305
|
+
const statusCode = err.statusCode;
|
|
199
306
|
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
-
|
|
307
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 500) {
|
|
308
|
+
return reply.status(statusCode).send({
|
|
309
|
+
error: { code: statusCode === 404 ? "NOT_FOUND" : "VALIDATION", message, requestId, retryable: false },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return reply.status(500).send({ error: { code: "INTERNAL", message, requestId, retryable: false } });
|
|
201
313
|
});
|
|
314
|
+
app.setNotFoundHandler((req, reply) => reply.status(404).send({
|
|
315
|
+
error: {
|
|
316
|
+
code: "NOT_FOUND",
|
|
317
|
+
message: `No route ${req.method} ${req.url.split("?")[0]}`,
|
|
318
|
+
requestId: stateOf(req).requestId,
|
|
319
|
+
retryable: false,
|
|
320
|
+
},
|
|
321
|
+
}));
|
|
202
322
|
const core = await attachCoreFastify(app, {
|
|
203
323
|
// 5120 is outside the memorix zone, so the published port is passed in.
|
|
204
324
|
port: options.port ?? resolveApiPort(),
|
|
@@ -276,6 +396,7 @@ export async function createClientApi(options) {
|
|
|
276
396
|
const objectType = findObjectType(await loadCatalog(upstream, scope), id);
|
|
277
397
|
if (!objectType)
|
|
278
398
|
throw new ApiError(404, "READ_NOT_FOUND", `Unknown object type ${id}`);
|
|
399
|
+
assertResolved(objectType);
|
|
279
400
|
const view = findContentType(objectType, contentType);
|
|
280
401
|
if (!view)
|
|
281
402
|
throw new ApiError(404, "READ_NOT_FOUND", `Unknown content type ${contentType}`);
|
|
@@ -289,6 +410,7 @@ export async function createClientApi(options) {
|
|
|
289
410
|
const objectType = findObjectType(catalog, id);
|
|
290
411
|
if (!objectType)
|
|
291
412
|
throw new ApiError(404, "READ_NOT_FOUND", `Unknown object type ${id}`);
|
|
413
|
+
assertResolved(objectType);
|
|
292
414
|
const leadingContentType = typeof query.leadingContentType === "string" && query.leadingContentType.trim()
|
|
293
415
|
? query.leadingContentType.trim()
|
|
294
416
|
: objectType.leadingContentType;
|
|
@@ -316,6 +438,7 @@ export async function createClientApi(options) {
|
|
|
316
438
|
const pageInfo = asRecord(listed?.pageInfo);
|
|
317
439
|
const shape = representation === "json" ? "data" : "envelope";
|
|
318
440
|
const fieldMap = fieldsByType(objectType, contentTypes);
|
|
441
|
+
const refs = refsModeOf(query.refs, "expand");
|
|
319
442
|
const rows = items.map((item) => {
|
|
320
443
|
const row = asRecord(item);
|
|
321
444
|
return {
|
|
@@ -326,7 +449,16 @@ export async function createClientApi(options) {
|
|
|
326
449
|
const targets = new Map();
|
|
327
450
|
for (const row of rows)
|
|
328
451
|
mergeReferenceTargets(targets, collectReferenceTargets(row.contents, fieldMap, shape));
|
|
329
|
-
const
|
|
452
|
+
const attempted = new Set();
|
|
453
|
+
const resolved = await loadResolvedReferences(upstream, scope, catalog, targets, attempted);
|
|
454
|
+
const nextCursor = typeof pageInfo?.nextCursor === "string" && pageInfo.nextCursor ? pageInfo.nextCursor : null;
|
|
455
|
+
const extras = recordsListExtras(id, nextCursor, census.peek(scope));
|
|
456
|
+
const projected = rows.map((row) => ({
|
|
457
|
+
recordId: row.recordId,
|
|
458
|
+
contents: refs === "status"
|
|
459
|
+
? withReferenceStatus(projectContents(row.contents, fieldMap, representation, shape), fieldMap, resolved, attempted)
|
|
460
|
+
: projectContents(row.contents, fieldMap, representation, shape, resolved),
|
|
461
|
+
}));
|
|
330
462
|
return {
|
|
331
463
|
scope: scopeBody(scope),
|
|
332
464
|
objectType: id,
|
|
@@ -334,21 +466,29 @@ export async function createClientApi(options) {
|
|
|
334
466
|
leadingSource: query.leadingContentType && String(query.leadingContentType).trim()
|
|
335
467
|
? "request"
|
|
336
468
|
: objectType.leadingSource,
|
|
337
|
-
|
|
469
|
+
total: extras.total,
|
|
470
|
+
totalStatus: extras.totalStatus,
|
|
471
|
+
truncated: false,
|
|
472
|
+
records: projected.map((row) => ({
|
|
338
473
|
recordId: row.recordId,
|
|
339
|
-
|
|
474
|
+
objectType: id,
|
|
475
|
+
dataCategory: objectType.dataCategory,
|
|
476
|
+
contentTypes: Object.keys(row.contents),
|
|
477
|
+
instances: Object.entries(row.contents).flatMap(([contentType, list]) => list.map((instance) => ({ contentType, ...instance }))),
|
|
340
478
|
})),
|
|
479
|
+
items: projected,
|
|
341
480
|
pageInfo: {
|
|
342
|
-
|
|
481
|
+
complete: extras.complete,
|
|
482
|
+
nextCursor,
|
|
343
483
|
limit: typeof pageInfo?.limit === "number" ? pageInfo.limit : limit,
|
|
344
484
|
},
|
|
345
485
|
};
|
|
346
486
|
});
|
|
347
487
|
app.get("/api/v1/records/*", async (req) => {
|
|
348
488
|
const scope = await scopeFor(req);
|
|
349
|
-
const
|
|
350
|
-
const catalogOnly =
|
|
351
|
-
const recordId = (catalogOnly ?
|
|
489
|
+
const rawPath = rawPathAfter(req, "/api/v1/records/") ?? wildcard(req);
|
|
490
|
+
const catalogOnly = rawPath.endsWith("/content-types");
|
|
491
|
+
const recordId = decodeSegments((catalogOnly ? rawPath.slice(0, -"/content-types".length) : rawPath).replace(/\/$/, ""));
|
|
352
492
|
if (!recordId)
|
|
353
493
|
throw new ApiError(400, "VALIDATION", "recordId required");
|
|
354
494
|
const subject = await loadSubject(upstream, scope, recordId);
|
|
@@ -358,20 +498,31 @@ export async function createClientApi(options) {
|
|
|
358
498
|
}
|
|
359
499
|
const catalog = await loadCatalog(upstream, scope);
|
|
360
500
|
const objectType = findObjectType(catalog, subject.objectType);
|
|
501
|
+
const storedTypes = subject.contentTypes.filter((ct) => ct !== "relationships");
|
|
502
|
+
const status = storedTypes.length === 0 ? "directory-only" : "stored";
|
|
361
503
|
if (catalogOnly) {
|
|
504
|
+
const stored = new Set(subject.contentTypes);
|
|
362
505
|
return {
|
|
363
506
|
scope: scopeBody(scope),
|
|
364
507
|
recordId: subject.recordId,
|
|
365
508
|
objectType: subject.objectType,
|
|
366
509
|
dataCategory: subject.dataCategory,
|
|
510
|
+
status,
|
|
367
511
|
leadingContentType: objectType?.leadingContentType ?? null,
|
|
368
512
|
leadingSource: objectType?.leadingSource ?? "default",
|
|
369
|
-
contentTypes: contentTypeViews(objectType, subject.contentTypes)
|
|
513
|
+
contentTypes: contentTypeViews(objectType, subject.contentTypes).map((view) => ({
|
|
514
|
+
...view,
|
|
515
|
+
presence: "stored",
|
|
516
|
+
})),
|
|
517
|
+
declaredAbsent: (objectType?.contentTypes ?? [])
|
|
518
|
+
.filter((view) => !stored.has(view.id) && view.id !== "relationships")
|
|
519
|
+
.map((view) => ({ ...view, presence: "declared-absent" })),
|
|
370
520
|
};
|
|
371
521
|
}
|
|
372
522
|
const requested = csv(query.contentTypes);
|
|
373
523
|
const contentTypes = (requested ?? subject.contentTypes).filter((ct) => ct !== "relationships");
|
|
374
524
|
const representation = representationOf(query.representation);
|
|
525
|
+
const refs = refsModeOf(query.refs, "expand");
|
|
375
526
|
let contents = {};
|
|
376
527
|
let dataCategory = subject.dataCategory;
|
|
377
528
|
if (contentTypes.length > 0) {
|
|
@@ -386,15 +537,51 @@ export async function createClientApi(options) {
|
|
|
386
537
|
const composition = asRecord(composed?.composition);
|
|
387
538
|
contents = asRecord(composition?.contents);
|
|
388
539
|
}
|
|
540
|
+
const fieldMap = fieldsByType(objectType, contentTypes);
|
|
541
|
+
let projected;
|
|
542
|
+
if (refs === "status") {
|
|
543
|
+
const attempted = new Set();
|
|
544
|
+
const resolved = await loadResolvedReferences(upstream, scope, catalog, collectReferenceTargets(contents, fieldMap, "record"), attempted);
|
|
545
|
+
projected = withReferenceStatus(projectContents(contents, fieldMap, representation, "record"), fieldMap, resolved, attempted);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
projected = await projectRead(upstream, scope, catalog, contents, fieldMap, representation, "record");
|
|
549
|
+
}
|
|
389
550
|
return {
|
|
390
551
|
scope: scopeBody(scope),
|
|
391
552
|
recordId: subject.recordId,
|
|
392
553
|
objectType: subject.objectType,
|
|
554
|
+
...(objectType && isConflicted(objectType) ? { objectTypeResolution: "conflict" } : {}),
|
|
393
555
|
dataCategory,
|
|
556
|
+
status,
|
|
394
557
|
contentTypes,
|
|
395
|
-
contents:
|
|
558
|
+
contents: projected,
|
|
559
|
+
...(status === "directory-only" ? { instances: [] } : {}),
|
|
396
560
|
};
|
|
397
561
|
});
|
|
562
|
+
const memorixBase = options.memorixUrl.replace(/\/+$/, "");
|
|
563
|
+
const probeFetch = options.fetchImpl ?? memorixFetch;
|
|
564
|
+
registerV1Routes(app, {
|
|
565
|
+
upstream,
|
|
566
|
+
directory,
|
|
567
|
+
census,
|
|
568
|
+
planning,
|
|
569
|
+
authMode: auth.mode,
|
|
570
|
+
scopeFor,
|
|
571
|
+
authenticate,
|
|
572
|
+
loadCatalog,
|
|
573
|
+
loadObjectTypeKinds,
|
|
574
|
+
assignees: (orgId) => options.planning?.assignees?.[orgId] ?? null,
|
|
575
|
+
probeMemorix: async () => {
|
|
576
|
+
try {
|
|
577
|
+
const res = await probeFetch(`${memorixBase}/health`, { signal: AbortSignal.timeout(3000) });
|
|
578
|
+
return res.ok;
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
});
|
|
398
585
|
registerProcessedRoutes(app, { upstream, scopeFor, loadCatalog });
|
|
399
586
|
registerRefRoutes(app, { upstream, scopeFor, loadCatalog });
|
|
400
587
|
registerFetchRoutes(app, {
|