mikser-io 8.0.0 → 8.2.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/README.md +17 -9
- package/index.js +4 -4
- package/package.json +5 -6
- package/src/catalog.js +165 -7
- package/src/constants.js +24 -2
- package/src/engine.js +36 -177
- package/src/journal.js +88 -97
- package/src/logger.js +228 -0
- package/src/plugins/api.js +99 -467
- package/src/plugins/layouts.js +94 -96
- package/src/plugins/preview.js +19 -470
- package/src/refs.js +9 -143
- package/src/render.js +206 -0
- package/src/runtime.js +6 -5
- package/src/server.js +148 -0
- package/src/subscriptions.js +157 -0
- package/src/utils.js +117 -1
- package/src/api.js +0 -263
- package/src/mcp.js +0 -627
- package/src/tracking.js +0 -75
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The honest caveat: this advantage is real on **content-shaped work** — adding
|
|
|
79
79
|
|
|
80
80
|
## Control mikser from your AI agent
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Install the [`mikser-io-mcp`](https://github.com/almero-digital-marketing/mikser-io-mcp) plugin and any MCP-speaking client — Claude Desktop, Claude Code, ChatGPT, custom agents — connects to the running engine. From inside a chat, your AI can:
|
|
83
83
|
|
|
84
84
|
- read every entity in the catalog
|
|
85
85
|
- write new content files (markdown, layouts, configuration) — writes land on disk and the next cycle picks them up
|
|
@@ -90,8 +90,16 @@ Add `--mcp` to your mikser command and any MCP-speaking client — Claude Deskto
|
|
|
90
90
|
|
|
91
91
|
Plugins extend the tool surface the same way they mount HTTP routes; install the plugin, the agent gets new verbs. No glue code, no per-project agent wiring.
|
|
92
92
|
|
|
93
|
+
```js
|
|
94
|
+
// mikser.config.js
|
|
95
|
+
export default {
|
|
96
|
+
plugins: ['mcp', /* … */],
|
|
97
|
+
// optional: mcp: { path: '/mcp', endpoints: { … } }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
93
101
|
```bash
|
|
94
|
-
mikser --server
|
|
102
|
+
mikser --server # MCP mounts at /mcp on the same port
|
|
95
103
|
```
|
|
96
104
|
|
|
97
105
|
What that feels like in practice: *"draft three hero-section variants and show me previews"* — three layouts written, three previews returned inline, one chat turn. *"Why did the build break?"* — the agent reads the rolling log buffer and answers from the same view your terminal sees. *"Update this article's tone and show me the preview"* — the agent edits the file and surfaces the rendered article inline; you click Approve or Reject, the agent acts on your choice. Operator, AI, and any observer dashboard share the same engine because mikser is single-tenant by design.
|
|
@@ -109,7 +117,7 @@ When an AI agent edits ten files, the next question is: *did it do what I asked?
|
|
|
109
117
|
|
|
110
118
|
The shift this enables: AI review stops being *"read every change"* and becomes *"spot-check the agent's confidence."* The agent verifies its own work; the human samples and approves. That's the workflow that lets a content team actually use AI at scale — change the tone across the entire site in a morning, ship it after a coffee.
|
|
111
119
|
|
|
112
|
-
Full tool reference and twelve worked scenarios in [
|
|
120
|
+
Full tool reference and twelve worked scenarios in the [`mikser-io-mcp` plugin docs](https://github.com/almero-digital-marketing/mikser-io-mcp#readme).
|
|
113
121
|
|
|
114
122
|
## Plugins on top of the engine
|
|
115
123
|
|
|
@@ -132,14 +140,14 @@ The engine is what stays stable — the lifecycle, the catalog, the file-based c
|
|
|
132
140
|
|---|---|
|
|
133
141
|
| `data` | JSON snapshots of entities / context / catalog, written to disk for static serving |
|
|
134
142
|
| `api` | REST endpoints with sift-backed queries, per-endpoint tokens, optional render, opt-in [per-query disk cache](./documentation/caching.md) for reverse-proxy failover |
|
|
135
|
-
| `preview` | In-memory render cache + `GET /preview/:filename` route. Companion to the [`
|
|
143
|
+
| `preview` | In-memory render cache + `GET /preview/:filename` route. Companion to the `mikser_preview_render` MCP tool (in [`mikser-io-mcp`](https://github.com/almero-digital-marketing/mikser-io-mcp)) — transient render bytes served at a clickable URL, no filesystem footprint |
|
|
136
144
|
|
|
137
145
|
**Integrations:**
|
|
138
146
|
|
|
139
147
|
| Plugin | What it does |
|
|
140
148
|
|---|---|
|
|
141
149
|
| [`mikser-io-vector`](https://github.com/almero-digital-marketing/mikser-io-vector) | OpenAI embeddings + semantic search (sqlite-vec or pgvector) |
|
|
142
|
-
| [`mikser-io-
|
|
150
|
+
| [`mikser-io-schemas`](https://github.com/almero-digital-marketing/mikser-io-schemas) | Zod-backed entity validation + auto-generated TypeScript declarations for the SDK. Auto-detects `$`-keyed references and warns on broken ones — see [ADR-0007](./documentation/decisions/0007-references-declaration-and-expansion.md) |
|
|
143
151
|
| [`mikser-io-archive`](https://github.com/almero-digital-marketing/mikser-io-archive) | Persist matching entities to YAML — audit trail, versioned content history, downstream export |
|
|
144
152
|
| `mapper` | Run config-supplied transforms over matched entities each cycle (in-core, generic transformation layer) |
|
|
145
153
|
| [`mikser-io-live`](https://github.com/almero-digital-marketing/mikser-io-live) | Lightweight dev server with browser auto-refresh — pair with `--watch` for the classic save→reload loop |
|
|
@@ -162,7 +170,7 @@ The `api`, `vector`, and `schemas` plugins are paired with client-side SDKs so a
|
|
|
162
170
|
| [`mikser-io-sdk-api`](https://github.com/almero-digital-marketing/mikser-io-sdk-api) | `api` | `entities(name).list / query / urlFor / pages / update / delete / render / live` — Mongo-style filter operators backed by sift, sort, projection, pagination, SSE-driven live subscriptions, and `expand: [...]` to inline-resolve `$`-keyed references in one round-trip (multi-hop chains, `*` array iteration) |
|
|
163
171
|
| [`mikser-io-sdk-vector`](https://github.com/almero-digital-marketing/mikser-io-sdk-vector) | `vector` | `vector(storeName).findSimilar(text, { limit })` — semantic search hits with the original mapped object attached |
|
|
164
172
|
|
|
165
|
-
**Framework integrations** — all three wrap `mikser-io-sdk-api` in framework-idiomatic shapes. Same surface: `useDocument` / `useDocuments` live data, multilingual `useHref` / `useAlternates`, asset resolution via `useAsset`, generic on entity type so `mikser-io-
|
|
173
|
+
**Framework integrations** — all three wrap `mikser-io-sdk-api` in framework-idiomatic shapes. Same surface: `useDocument` / `useDocuments` live data, multilingual `useHref` / `useAlternates`, asset resolution via `useAsset`, generic on entity type so `mikser-io-schemas`-emitted types compose:
|
|
166
174
|
|
|
167
175
|
| Package | Framework | Notes |
|
|
168
176
|
|---|---|---|
|
|
@@ -170,7 +178,7 @@ The `api`, `vector`, and `schemas` plugins are paired with client-side SDKs so a
|
|
|
170
178
|
| [`mikser-io-sdk-react`](https://github.com/almero-digital-marketing/mikser-io-sdk-react) | React 18+ / 19+ | Hooks, `<MikserProvider>` Context, React Router v6+ integration via `useMikserRoutes` → `useRoutes()`. |
|
|
171
179
|
| [`mikser-io-sdk-svelte`](https://github.com/almero-digital-marketing/mikser-io-sdk-svelte) | Svelte 5 (runes) | `$state` / `$effect` reactives, SvelteKit-friendly `generateMikserRoutes` for `entries()` prerender, `useMikserPages` for live nav. |
|
|
172
180
|
|
|
173
|
-
Each SDK ships TypeScript declarations so client projects get autocomplete on filters, envelopes, and the `MikserError` thrown on non-2xx responses. Pair any of the framework SDKs with the `entities.d.ts` emitted by `mikser-io-
|
|
181
|
+
Each SDK ships TypeScript declarations so client projects get autocomplete on filters, envelopes, and the `MikserError` thrown on non-2xx responses. Pair any of the framework SDKs with the `entities.d.ts` emitted by `mikser-io-schemas` for typed entity meta per layout. Install only the one(s) a project needs.
|
|
174
182
|
|
|
175
183
|
## Quick Start
|
|
176
184
|
|
|
@@ -201,7 +209,7 @@ The shape mikser fits cleanly:
|
|
|
201
209
|
|
|
202
210
|
- **Marketing sites with editorial teams** — content authors work in files (via their editor, a Git client, or `mikser-io-decap`), engineers ship features without negotiating with a CMS schema, the site stays portable.
|
|
203
211
|
- **Multilingual publishing platforms** — the `useHref()` / `useAlternates()` pattern in `sdk-vue` decouples logical references from per-locale URLs. One source tree, many language deployments.
|
|
204
|
-
- **Content-heavy product catalogues** — `documents` + `mikser-io-
|
|
212
|
+
- **Content-heavy product catalogues** — `documents` + `mikser-io-schemas` + `data` plugin + a Vue frontend = typed product listings with live updates, semantic search via `vector`, and static-CDN-friendly JSON snapshots all at once.
|
|
205
213
|
- **AI-augmented media pipelines** — `assets` plugin presets call out to Replicate / OpenAI / local models to upscale images, transcribe audio, transcode video. The pipeline is JS code, so anything Node can do is in scope.
|
|
206
214
|
- **Mixed-output publishing** — the same source document renders to HTML, PDF (via `post-pdf`), MJML email (via `post-mjml`), and JSON snapshots. One catalog, many output formats, all concurrent.
|
|
207
215
|
- **Headless backends for static frontends** — pair the `api` plugin with `sdk-api` for SSE-driven live frontends; pair the `data` plugin output with any static host for pre-rendered consumption.
|
|
@@ -235,7 +243,7 @@ Mikser itself has a previous chapter: the [legacy 7.x line](https://github.com/a
|
|
|
235
243
|
| [Entities](./documentation/entities.md) | Users & Developers | Entity model, operations, journal, catalog |
|
|
236
244
|
| [Rendering](./documentation/rendering.md) | Users & Developers | Render pipeline, render plugins, render modes |
|
|
237
245
|
| [Watch Mode](./documentation/watch-mode.md) | Users | File watching, scheduled tasks, incremental builds |
|
|
238
|
-
| [MCP](
|
|
246
|
+
| [MCP](https://github.com/almero-digital-marketing/mikser-io-mcp#readme) | Users | The `mikser-io-mcp` plugin — tool surface, `mikser://` resources, twelve worked AI-driven scenarios |
|
|
239
247
|
| [Caching](./documentation/caching.md) | Users (production) | The `cache: true` disk cache + working nginx config for reverse-proxy failover |
|
|
240
248
|
| [Architecture](./documentation/architecture.md) | Developers | Module-level reference — what's in each file |
|
|
241
249
|
| [API Reference](./documentation/api-reference.md) | Developers | Complete public API reference |
|
package/index.js
CHANGED
|
@@ -5,11 +5,11 @@ export * from './src/journal.js'
|
|
|
5
5
|
export * from './src/lifecycle.js'
|
|
6
6
|
export * from './src/catalog.js'
|
|
7
7
|
export * from './src/refs.js'
|
|
8
|
+
export * from './src/subscriptions.js'
|
|
8
9
|
export * from './src/config.js'
|
|
9
10
|
export * from './src/plugins.js'
|
|
10
11
|
export * from './src/manager.js'
|
|
11
|
-
export * from './src/
|
|
12
|
+
export * from './src/logger.js'
|
|
12
13
|
export * from './src/engine.js'
|
|
13
|
-
export * from './src/
|
|
14
|
-
export * from './src/source.js'
|
|
15
|
-
export * from './src/mcp.js'
|
|
14
|
+
export * from './src/render.js'
|
|
15
|
+
export * from './src/source.js'
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"debug": "node --no-warnings app.js --
|
|
7
|
+
"debug": "node --no-warnings app.js --server --watch --working-folder test/fixture",
|
|
8
8
|
"test:unit": "node --test --test-reporter=spec 'test/unit/**/*.test.js'",
|
|
9
9
|
"test:smoke": "node --no-warnings app.js --working-folder test/fixture",
|
|
10
|
+
"test:perf": "node test/perf/generate.js && node --no-warnings app.js --working-folder test/perf",
|
|
10
11
|
"test": "npm run test:unit && npm run test:smoke"
|
|
11
12
|
},
|
|
12
13
|
"bin": {
|
|
@@ -21,22 +22,21 @@
|
|
|
21
22
|
"license": "ISC",
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@budibase/handlebars-helpers": "^0.14.3",
|
|
24
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
25
|
"await-semaphore": "^0.1.3",
|
|
26
26
|
"axios": "^1.17.0",
|
|
27
27
|
"chokidar": "^5.0.0",
|
|
28
|
-
"cli-progress": "^3.12.0",
|
|
29
28
|
"commander": "^15.0.0",
|
|
29
|
+
"cors": "^2.8.5",
|
|
30
30
|
"dayjs": "^1.11.21",
|
|
31
31
|
"deepdash": "^5.3.9",
|
|
32
32
|
"escape-string-regexp": "^5.0.0",
|
|
33
33
|
"execa": "^9.6.1",
|
|
34
34
|
"front-matter": "^4.0.2",
|
|
35
|
+
"gauge": "^5.0.2",
|
|
35
36
|
"globby": "^16.2.0",
|
|
36
37
|
"handlebars": "^4.7.9",
|
|
37
38
|
"hasha": "^7.0.0",
|
|
38
39
|
"is-url": "^1.2.4",
|
|
39
|
-
"knex": "^3.2.10",
|
|
40
40
|
"line-reader": "^0.4.0",
|
|
41
41
|
"lodash": "^4.18.1",
|
|
42
42
|
"lowdb": "^7.0.1",
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"pino-pretty": "^13.1.3",
|
|
49
49
|
"piscina": "^5.1.4",
|
|
50
50
|
"sift": "^17.1.3",
|
|
51
|
-
"sqlite3": "^6.0.1",
|
|
52
51
|
"truncate-stream": "^1.0.2",
|
|
53
52
|
"yaml": "^2.9.0"
|
|
54
53
|
},
|
package/src/catalog.js
CHANGED
|
@@ -7,8 +7,8 @@ import { Low } from 'lowdb'
|
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import { JSONFile } from 'lowdb/node'
|
|
9
9
|
import _ from 'lodash'
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import sift from 'sift'
|
|
11
|
+
import { expandEntity, projectMeta } from './utils.js'
|
|
12
12
|
|
|
13
13
|
// Same reasoning as journal.js — initialize the catalog in
|
|
14
14
|
// onInitialized so every plugin hook from onLoad onwards can safely
|
|
@@ -16,9 +16,14 @@ let catalog
|
|
|
16
16
|
// catalog.js imports useJournal from journal.js, so journal.js's
|
|
17
17
|
// onInitialized registers first within the phase — its hook runs
|
|
18
18
|
// before this one.
|
|
19
|
+
//
|
|
20
|
+
// The catalog lives at `runtime.catalog` — single source of truth.
|
|
21
|
+
// All operations below read through that property so tests can stub
|
|
22
|
+
// `runtime.catalog` with an in-memory equivalent and the same call
|
|
23
|
+
// shapes work without code changes.
|
|
19
24
|
onInitialized(async () => {
|
|
20
25
|
const adapter = new JSONFile(path.join(runtime.options.runtimeFolder, `catalog.json`))
|
|
21
|
-
catalog = new Low(adapter, {
|
|
26
|
+
const catalog = new Low(adapter, {
|
|
22
27
|
entities: [],
|
|
23
28
|
})
|
|
24
29
|
catalog.chain = _.chain(catalog).get('data')
|
|
@@ -27,6 +32,7 @@ onInitialized(async () => {
|
|
|
27
32
|
|
|
28
33
|
onPersist(async () => {
|
|
29
34
|
const logger = useLogger()
|
|
35
|
+
const catalog = runtime.catalog
|
|
30
36
|
for await (let { operation, entity } of useJournal('Catalog')) {
|
|
31
37
|
switch (operation) {
|
|
32
38
|
case OPERATION.CREATE:
|
|
@@ -57,17 +63,169 @@ onPersist(async () => {
|
|
|
57
63
|
})
|
|
58
64
|
|
|
59
65
|
onFinalized(async () => {
|
|
60
|
-
await catalog.write()
|
|
66
|
+
await runtime.catalog.write()
|
|
61
67
|
})
|
|
62
68
|
|
|
63
69
|
export async function findEntity(query) {
|
|
64
70
|
if (!query) return
|
|
65
|
-
return catalog.chain.get('entities').find(query).value()
|
|
71
|
+
return runtime.catalog.chain.get('entities').find(query).value()
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
export async function findEntities(query) {
|
|
69
75
|
if (!query) {
|
|
70
|
-
return catalog.chain.get('entities').value()
|
|
76
|
+
return runtime.catalog.chain.get('entities').value()
|
|
77
|
+
}
|
|
78
|
+
return runtime.catalog.chain.get('entities').filter(query).value()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// High-level CRUD-with-query surface — sift filters, sort, pagination,
|
|
82
|
+
// dotted-path projection, plus optional inline-expand of $-keyed
|
|
83
|
+
// references (ADR-0007). Used by the api plugin's HTTP handlers, the
|
|
84
|
+
// mikser-io-mcp plugin's tools, and any library-mode caller.
|
|
85
|
+
//
|
|
86
|
+
// `findEntity`/`findEntities` above are raw catalog access (no filter,
|
|
87
|
+
// no pagination, callback-style query). The queryEntities / readEntity
|
|
88
|
+
// pair below are the higher-level operations that compose findEntities
|
|
89
|
+
// with sift + expand + projection.
|
|
90
|
+
//
|
|
91
|
+
// Lives in catalog.js (not in the api plugin) because catalog operations
|
|
92
|
+
// are engine-level: render workers, library embedders, and other plugins
|
|
93
|
+
// all need to query the catalog with the same semantics. The api plugin
|
|
94
|
+
// is one consumer that adds HTTP routing + an on-disk cache on top.
|
|
95
|
+
|
|
96
|
+
// Match the same heuristic the schemas plugin uses for ref-existence
|
|
97
|
+
// checks — id, meta.href, or stripped-extension id. Centralised so the
|
|
98
|
+
// expand walker resolves refs the same way everywhere.
|
|
99
|
+
async function findRef(ref) {
|
|
100
|
+
if (!ref || typeof ref !== 'string') return null
|
|
101
|
+
const matches = await findEntities(e =>
|
|
102
|
+
!!e && (
|
|
103
|
+
e.id === ref ||
|
|
104
|
+
e.meta?.href === ref ||
|
|
105
|
+
(typeof e.id === 'string' && e.id.replace(/\.[^./]+$/, '') === ref)
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
return matches[0] ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Per-call expansion caps. Plumbed from mikser.config.js:
|
|
112
|
+
// catalog: { expand: { maxDepth, maxPaths, maxResolved } }
|
|
113
|
+
// Defaults match ADR-0007 B7. Centralised here so every expand-aware
|
|
114
|
+
// operation reads the same numbers. Private — callers reach it via
|
|
115
|
+
// `assertExpand` (pre-flight validation) or via queryEntities (which
|
|
116
|
+
// applies the same caps inside the walker).
|
|
117
|
+
function expandLimits() {
|
|
118
|
+
const cfg = runtime?.config?.catalog?.expand ?? {}
|
|
119
|
+
return {
|
|
120
|
+
maxDepth: typeof cfg.maxDepth === 'number' ? cfg.maxDepth : 5,
|
|
121
|
+
maxPaths: typeof cfg.maxPaths === 'number' ? cfg.maxPaths : 20,
|
|
122
|
+
maxResolved: typeof cfg.maxResolved === 'number' ? cfg.maxResolved : 100,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate an `expand` spec against the configured caps. Throws on
|
|
127
|
+
// violation; returns nothing on success. Used by `subscribe()` at
|
|
128
|
+
// registration time to reject bad expand before opening any transport
|
|
129
|
+
// channels. The same caps are enforced inside the walker, so this is
|
|
130
|
+
// purely about early/clean error surfacing.
|
|
131
|
+
export function assertExpand(expand) {
|
|
132
|
+
if (!expand?.length) return
|
|
133
|
+
const { maxPaths, maxDepth } = expandLimits()
|
|
134
|
+
if (expand.length > maxPaths) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`expand has ${expand.length} paths, exceeds maxPaths (${maxPaths})`,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
for (const p of expand) {
|
|
140
|
+
const parts = p.split('.').filter(Boolean)
|
|
141
|
+
if (parts.length > maxDepth) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Path '${p}' has length ${parts.length}, exceeds maxDepth (${maxDepth})`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Apply expand-then-project to one entity. When `expand` has entries
|
|
150
|
+
// the walker inlines resolved refs first; in all cases the final meta
|
|
151
|
+
// is normalized (`$`-keys stripped) so the wire shape matches what the
|
|
152
|
+
// SDK consumers and templates expect per ADR-0007 A3. Private — the
|
|
153
|
+
// only callers are queryEntities (per-item) and readEntity (via
|
|
154
|
+
// queryEntities). External code expands by going through one of those.
|
|
155
|
+
async function expandAndProject(entity, expand) {
|
|
156
|
+
let result = entity
|
|
157
|
+
if (expand?.length) {
|
|
158
|
+
result = await expandEntity(entity, expand, {
|
|
159
|
+
findRef,
|
|
160
|
+
...expandLimits(),
|
|
161
|
+
})
|
|
71
162
|
}
|
|
72
|
-
|
|
163
|
+
if (!result?.meta) return result
|
|
164
|
+
return { ...result, meta: projectMeta(result.meta) }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Paginated, sorted, filtered, projected query over the catalog. The
|
|
168
|
+
// `scope` arg is an optional pre-filter predicate (used by the api
|
|
169
|
+
// plugin's per-endpoint allowedEntities lambda); library callers
|
|
170
|
+
// usually pass nothing. Returns `{ items, total, skip, limit, hasNext }`.
|
|
171
|
+
export async function queryEntities({
|
|
172
|
+
filter, sort, fields, skip, limit, expand, scope,
|
|
173
|
+
} = {}) {
|
|
174
|
+
const effectiveLimit = Math.min(100, Math.max(1, limit ?? 25))
|
|
175
|
+
const effectiveSkip = Math.max(0, skip ?? 0)
|
|
176
|
+
|
|
177
|
+
let all = await findEntities()
|
|
178
|
+
if (scope) all = all.filter(scope)
|
|
179
|
+
|
|
180
|
+
if (filter && Object.keys(filter).length) {
|
|
181
|
+
const match = sift(filter)
|
|
182
|
+
all = all.filter(match)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const total = all.length
|
|
186
|
+
|
|
187
|
+
if (sort && Object.keys(sort).length) {
|
|
188
|
+
const entries = Object.entries(sort)
|
|
189
|
+
all.sort((a, b) => {
|
|
190
|
+
for (const [key, dir] of entries) {
|
|
191
|
+
const av = _.get(a, key)
|
|
192
|
+
const bv = _.get(b, key)
|
|
193
|
+
if (av == null && bv == null) continue
|
|
194
|
+
if (av == null) return 1
|
|
195
|
+
if (bv == null) return -1
|
|
196
|
+
if (av < bv) return -dir
|
|
197
|
+
if (av > bv) return dir
|
|
198
|
+
}
|
|
199
|
+
return 0
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let items = all.slice(effectiveSkip, effectiveSkip + effectiveLimit)
|
|
204
|
+
items = await Promise.all(items.map(item => expandAndProject(item, expand)))
|
|
205
|
+
if (fields?.length) items = items.map(e => _.pick(e, fields))
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
items,
|
|
209
|
+
total,
|
|
210
|
+
skip: effectiveSkip,
|
|
211
|
+
limit: effectiveLimit,
|
|
212
|
+
hasNext: effectiveSkip + effectiveLimit < total,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Read a single entity by catalog id. Returns the entity (post-expand,
|
|
217
|
+
// post-project) or null when not found. Throws on missing id.
|
|
218
|
+
// Source-file content loading is a separate concern — compose with
|
|
219
|
+
// `readEntityContent` from utils.js if you want `entity.content`
|
|
220
|
+
// populated (with the text/binary gate) or call `readFile(entity.uri)`
|
|
221
|
+
// directly if you want raw bytes.
|
|
222
|
+
export async function readEntity({ id, expand } = {}) {
|
|
223
|
+
if (!id) throw new Error('id is required')
|
|
224
|
+
const result = await queryEntities({
|
|
225
|
+
filter: { id },
|
|
226
|
+
skip: 0,
|
|
227
|
+
limit: 1,
|
|
228
|
+
expand,
|
|
229
|
+
})
|
|
230
|
+
return result.items[0] ?? null
|
|
73
231
|
}
|
package/src/constants.js
CHANGED
|
@@ -13,8 +13,30 @@ export const ACTION = {
|
|
|
13
13
|
TRIGGER: 'trigger',
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// Render / postprocess dispatch modes. Set per-entity via
|
|
17
|
+
// `meta.task: 'inline' | 'serial' | 'worker'`, or per-task by a plugin.
|
|
18
|
+
//
|
|
19
|
+
// INLINE — runs the render/postprocess in the main event loop via
|
|
20
|
+
// `await`. The outer dispatcher iterates with concurrency =
|
|
21
|
+
// runtime.options.threads (default 4), so multiple inline
|
|
22
|
+
// tasks can be in-flight at once; but they share the main
|
|
23
|
+
// thread, so CPU-bound work doesn't actually run in
|
|
24
|
+
// parallel. Right default for cheap renders (HTML / md /
|
|
25
|
+
// yaml templating) where IPC overhead would dominate.
|
|
26
|
+
//
|
|
27
|
+
// SERIAL — runs in the main event loop too, but via p-queue with
|
|
28
|
+
// concurrency 1. One at a time, no interleaving. Right
|
|
29
|
+
// choice for tasks that touch a shared, non-reentrant
|
|
30
|
+
// resource (a single browser instance, a sequential
|
|
31
|
+
// encoder, etc.).
|
|
32
|
+
//
|
|
33
|
+
// WORKER — runs on a real OS thread via the Piscina pool sized to
|
|
34
|
+
// runtime.options.threads. True parallelism. Right choice
|
|
35
|
+
// for expensive CPU-bound renders (PDF via headless
|
|
36
|
+
// Chromium, MJML compilation, image compose) where the
|
|
37
|
+
// render cost amortizes the IPC overhead.
|
|
16
38
|
export const TASKS = {
|
|
17
|
-
|
|
39
|
+
INLINE: 'inline',
|
|
40
|
+
SERIAL: 'serial',
|
|
18
41
|
WORKER: 'worker',
|
|
19
|
-
POOL: 'pool',
|
|
20
42
|
}
|