@omgbase/core 0.28.0 → 0.29.2
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 +7 -5
- package/dist/src/core/read/document.d.ts +11 -10
- package/dist/src/core/read/document.js +12 -11
- package/dist/src/core/read/nodes.d.ts +10 -10
- package/dist/src/core/read/nodes.js +11 -11
- package/dist/src/core/read/reader.d.ts +37 -24
- package/dist/src/core/read/reader.js +44 -31
- package/dist/src/graph/history.d.ts +25 -22
- package/dist/src/graph/history.js +17 -17
- package/dist/src/graph/inbound-links.d.ts +7 -7
- package/dist/src/graph/inbound-links.js +7 -7
- package/dist/src/graph/link-health.d.ts +9 -9
- package/dist/src/graph/link-health.js +9 -9
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/mcp/arguments.d.ts +10 -3
- package/dist/src/mcp/arguments.js +32 -9
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +22 -22
- package/dist/src/mcp/graph.js +54 -47
- package/dist/src/mcp/reference.d.ts +29 -1
- package/dist/src/mcp/reference.js +398 -255
- package/dist/src/mcp/server.d.ts +9 -0
- package/dist/src/mcp/server.js +170 -108
- package/dist/src/mutate/apply.d.ts +6 -6
- package/dist/src/mutate/apply.js +1 -1
- package/dist/src/mutate/canonical.d.ts +1 -1
- package/dist/src/mutate/canonical.js +2 -2
- package/dist/src/mutate/commit.d.ts +3 -3
- package/dist/src/mutate/commit.js +4 -4
- package/dist/src/mutate/docs.d.ts +1 -1
- package/dist/src/mutate/docs.js +8 -8
- package/dist/src/mutate/effects.d.ts +17 -17
- package/dist/src/mutate/effects.js +113 -113
- package/dist/src/mutate/macros.d.ts +5 -5
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/materialize.js +2 -2
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +7 -7
- package/dist/src/mutate/plan-update.js +2 -2
- package/dist/src/mutate/plan.d.ts +8 -8
- package/dist/src/mutate/plan.js +25 -25
- package/dist/src/mutate/select.d.ts +4 -4
- package/dist/src/mutate/select.js +57 -56
- package/dist/src/mutate/sugar.d.ts +10 -10
- package/dist/src/mutate/sugar.js +21 -21
- package/dist/src/oqx-js/context.d.ts +8 -4
- package/dist/src/oqx-js/context.js +206 -189
- package/dist/src/oqx-js/names.d.ts +28 -17
- package/dist/src/oqx-js/names.js +115 -78
- package/dist/src/oqx-js/planner.d.ts +4 -8
- package/dist/src/oqx-js/planner.js +6 -10
- package/dist/src/oqx-js/root-row.d.ts +6 -5
- package/dist/src/oqx-js/root-row.js +15 -16
- package/dist/src/oqx-js/run.d.ts +18 -17
- package/dist/src/oqx-js/run.js +57 -54
- package/dist/src/oqx-js/sql/translate.d.ts +5 -5
- package/dist/src/oqx-js/sql/translate.js +162 -161
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -30
- package/dist/src/search/external.d.ts +10 -0
- package/dist/src/search/external.js +20 -2
- package/dist/src/search/resolve.d.ts +2 -2
- package/dist/src/search/resolve.js +1 -1
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +5 -5
- package/dist/src/search/text.js +2 -2
- package/dist/src/spec-versions.d.ts +7 -7
- package/dist/src/spec-versions.js +6 -6
- package/dist/src/surface-paths.d.ts +18 -17
- package/dist/src/surface-paths.js +23 -21
- package/dist/src/sync/admin.d.ts +2 -2
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
|
@@ -10,16 +10,16 @@ export type OpsetKind = "doc_update";
|
|
|
10
10
|
export type PlanDisposition = "same" | "edited" | "moved" | "edited_moved" | "inserted" | "deleted" | "split_from" | "merged_into" | "copied_from" | "resurrected" | "bulk_rewrite" | "retiled";
|
|
11
11
|
/** State the opset was planned against; applying against different state
|
|
12
12
|
* fails. The document is named by its row attributes (spec/surface §1.2,
|
|
13
|
-
*
|
|
13
|
+
* 5.0 "underscore = engine"): `_id`, `_path`, and `_hash` — its current hash at plan time;
|
|
14
14
|
* `baseRevision` names a role and keeps its name. */
|
|
15
15
|
export interface OpsetPrecondition {
|
|
16
16
|
/** doc id the plan targets */
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
_id: string;
|
|
18
|
+
_path: string;
|
|
19
19
|
/** the document's current revision id at plan time (null for a doc with no committed revision yet) */
|
|
20
20
|
baseRevision: string | null;
|
|
21
|
-
/** sha256 hex of the document's current rendered bytes (docs.file_hash) at plan time — its `
|
|
22
|
-
|
|
21
|
+
/** sha256 hex of the document's current rendered bytes (docs.file_hash) at plan time — its `_hash` */
|
|
22
|
+
_hash: string;
|
|
23
23
|
}
|
|
24
24
|
/** One kernel op plus the identity consequence the planner attributes to it. */
|
|
25
25
|
export interface PlanOp {
|
|
@@ -54,8 +54,8 @@ export interface Opset {
|
|
|
54
54
|
version: 1;
|
|
55
55
|
kind: OpsetKind;
|
|
56
56
|
target: {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
_id: string;
|
|
58
|
+
_path: string;
|
|
59
59
|
};
|
|
60
60
|
precondition: OpsetPrecondition;
|
|
61
61
|
/** matcher version that produced the identity decisions (reproducibility) */
|
|
@@ -86,8 +86,8 @@ export function planUpdate(store, repoId, rootPath, docRef, content, opts = {})
|
|
|
86
86
|
return {
|
|
87
87
|
version: 1,
|
|
88
88
|
kind: "doc_update",
|
|
89
|
-
target: {
|
|
90
|
-
precondition: {
|
|
89
|
+
target: { _id: docId, _path: path },
|
|
90
|
+
precondition: { _id: docId, _path: path, baseRevision: info.currentRev, _hash: baseContentHash },
|
|
91
91
|
matcherV: config.matcherV,
|
|
92
92
|
ops,
|
|
93
93
|
...(fmChanged ? { frontmatter: { raw: targetFm } } : {}),
|
|
@@ -24,18 +24,18 @@ export type WireOp = Exclude<Op, {
|
|
|
24
24
|
};
|
|
25
25
|
/** The lowered form `execute` replays (§11.4): every key present, `[]` when empty. */
|
|
26
26
|
export interface PlanOpset {
|
|
27
|
-
/** `create` → the composed file, its document symbol and the index of the first kernel op planned after it (3.0; a later `set
|
|
27
|
+
/** `create` → the composed file, its document symbol and the index of the first kernel op planned after it (3.0; a later `set _frontmatter.<k>` on the symbol is a head below) */
|
|
28
28
|
creates: CreateEntry[];
|
|
29
29
|
/** `move document` → the row/file rename (its retarget updates are in `ops`) */
|
|
30
30
|
moves: {
|
|
31
|
-
|
|
31
|
+
_id: string;
|
|
32
32
|
from: string;
|
|
33
33
|
to: string;
|
|
34
34
|
}[];
|
|
35
35
|
/** `remove document` */
|
|
36
36
|
tombstones: {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
_id: string;
|
|
38
|
+
_path: string;
|
|
39
39
|
}[];
|
|
40
40
|
/** per document, the file head — everything before the first block (frontmatter incl. fences and separator) */
|
|
41
41
|
set_frontmatter: {
|
|
@@ -48,14 +48,14 @@ export interface PlanOpset {
|
|
|
48
48
|
export interface PlanPreconditions {
|
|
49
49
|
/** the row triples (`spec/mutate` §4 step 5) of every document the plan touches, loaded ones first in load order */
|
|
50
50
|
docs: {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
_id: string;
|
|
52
|
+
_path: string;
|
|
53
53
|
base_revision: string | null;
|
|
54
|
-
|
|
54
|
+
_hash: string | null;
|
|
55
55
|
}[];
|
|
56
56
|
/** every block content hash the plan pins (caller-given or pinned from the live row), first-pin order */
|
|
57
57
|
blocks: {
|
|
58
|
-
|
|
58
|
+
_id: string;
|
|
59
59
|
content_hash: string;
|
|
60
60
|
}[];
|
|
61
61
|
/** the caller's destination-parent CAS pins */
|
package/dist/src/mutate/plan.js
CHANGED
|
@@ -116,19 +116,19 @@ export function planEffects(store, req) {
|
|
|
116
116
|
const changeset = planner.finish();
|
|
117
117
|
const opset = {
|
|
118
118
|
creates: ctx.creates,
|
|
119
|
-
moves: ctx.moves.map((m) => ({
|
|
120
|
-
tombstones: ctx.tombstones.map((t) => ({
|
|
119
|
+
moves: ctx.moves.map((m) => ({ _id: m.id, from: m.from, to: m.to })),
|
|
120
|
+
tombstones: ctx.tombstones.map((t) => ({ _id: t.id, _path: t.path })),
|
|
121
121
|
set_frontmatter: [...ctx.heads].map(([doc, raw]) => ({ doc, raw })),
|
|
122
122
|
ops: ctx.ops.map(wireOp),
|
|
123
123
|
};
|
|
124
|
-
const docs = changeset.preconditions.map((p) => ({
|
|
124
|
+
const docs = changeset.preconditions.map((p) => ({ _id: p.id, _path: p.path, base_revision: p.currentRev, _hash: p.fileHash }));
|
|
125
125
|
for (const [id, base] of ctx.docBases) {
|
|
126
|
-
if (!docs.some((d) => d.
|
|
127
|
-
docs.push({ id,
|
|
126
|
+
if (!docs.some((d) => d._id === id))
|
|
127
|
+
docs.push({ _id: id, _path: base.path, base_revision: base.currentRev, _hash: base.fileHash });
|
|
128
128
|
}
|
|
129
129
|
const preconditions = {
|
|
130
130
|
docs,
|
|
131
|
-
blocks: [...ctx.pins].map(([id, content_hash]) => ({ id, content_hash })),
|
|
131
|
+
blocks: [...ctx.pins].map(([id, content_hash]) => ({ _id: id, content_hash })),
|
|
132
132
|
parents: ctx.parents,
|
|
133
133
|
absent_paths: ctx.absentPaths,
|
|
134
134
|
};
|
|
@@ -151,9 +151,9 @@ export function planEffects(store, req) {
|
|
|
151
151
|
for (const t of ctx.tombstones)
|
|
152
152
|
diffs[t.path] = { before: docStore.read(t.path) ?? "", after: "" };
|
|
153
153
|
for (const r of revisions) {
|
|
154
|
-
const moved = ctx.moves.find((m) => m.id === r.
|
|
154
|
+
const moved = ctx.moves.find((m) => m.id === r._id);
|
|
155
155
|
if (moved)
|
|
156
|
-
r.
|
|
156
|
+
r._path = moved.to;
|
|
157
157
|
}
|
|
158
158
|
const preview = { results: changeset.results, revisions, diffs, summary: ctx.summary };
|
|
159
159
|
if (ctx.identity.size > 0)
|
|
@@ -171,7 +171,7 @@ function requestOf(req) {
|
|
|
171
171
|
/**
|
|
172
172
|
* `conflicting_effects`: keep the later effect — the request minus the earlier
|
|
173
173
|
* one; under a selector, a cross-hit collision narrows the selector to the
|
|
174
|
-
* later hit (`from <root> where
|
|
174
|
+
* later hit (`from <root> where _id == …`, `expect.max: 1`) and a same-hit
|
|
175
175
|
* collision drops the earlier template. `stale_plan` at plan time (an
|
|
176
176
|
* `expect.revision` that no longer holds): the same request with the current
|
|
177
177
|
* revision in that guard. `target_missing { symbol }` for a `$new.n` no earlier
|
|
@@ -191,8 +191,8 @@ function planningFix(err, i, req, origins, symbols) {
|
|
|
191
191
|
const ob = origins[b];
|
|
192
192
|
if (oa === undefined || ob === undefined || oa.template_index === undefined)
|
|
193
193
|
return undefined;
|
|
194
|
-
if (oa.row.
|
|
195
|
-
return fixRequest({ select: narrowedTo(rootOfId(ob.row.
|
|
194
|
+
if (oa.row._id !== ob.row._id)
|
|
195
|
+
return fixRequest({ select: narrowedTo(rootOfId(ob.row._id), ob.row._id), effects, expect: { max: 1 } });
|
|
196
196
|
return fixRequest({ ...requestOf(req), effects: effects.filter((_, k) => k !== oa.template_index) });
|
|
197
197
|
}
|
|
198
198
|
if (err.code === "stale_plan" && isRecord(err.data.current) && typeof err.data.current.revision === "string") {
|
|
@@ -386,14 +386,14 @@ function underLock(req, fn) {
|
|
|
386
386
|
*/
|
|
387
387
|
function executeAdmitted(store, req, plan, docStore) {
|
|
388
388
|
for (const d of plan.preconditions.docs) {
|
|
389
|
-
const row = readDocBase(store.db, d.
|
|
389
|
+
const row = readDocBase(store.db, d._id);
|
|
390
390
|
if (!row)
|
|
391
|
-
throw new MutationError("doc_missing", `doc ${d.
|
|
392
|
-
if (row.path !== d.
|
|
393
|
-
throw new MutationError("stale_plan", `document ${d.
|
|
394
|
-
doc: d.
|
|
391
|
+
throw new MutationError("doc_missing", `doc ${d._id} no longer exists`, { doc: d._id });
|
|
392
|
+
if (row.path !== d._path || row.currentRev !== d.base_revision || row.fileHash !== d._hash) {
|
|
393
|
+
throw new MutationError("stale_plan", `document ${d._id} changed since the plan was made; re-plan`, {
|
|
394
|
+
doc: d._id,
|
|
395
395
|
expected_revision: d.base_revision,
|
|
396
|
-
current: { revision: row.currentRev,
|
|
396
|
+
current: { revision: row.currentRev, _hash: row.fileHash },
|
|
397
397
|
retriable: true,
|
|
398
398
|
fix: replanFix(plan),
|
|
399
399
|
});
|
|
@@ -418,7 +418,7 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
418
418
|
}),
|
|
419
419
|
};
|
|
420
420
|
const stat = req.omgbaseDir !== undefined;
|
|
421
|
-
const moved = new Map(plan.opset.moves.map((m) => [m.
|
|
421
|
+
const moved = new Map(plan.opset.moves.map((m) => [m._id, m]));
|
|
422
422
|
const blockSymbols = plan.new_ids.filter((s) => s.kind === "block").map((s) => s.symbol);
|
|
423
423
|
const planner = new ChangesetPlanner(store, req.repoId, req.omgbaseDir);
|
|
424
424
|
const { effects, outcome } = observeMints((prefix, id) => {
|
|
@@ -459,8 +459,8 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
459
459
|
const effects = [];
|
|
460
460
|
for (const m of plan.opset.moves) {
|
|
461
461
|
const bytes = docStore.read(m.from) ?? "";
|
|
462
|
-
const rev = readDocBase(store.db, m.
|
|
463
|
-
effects.push({ kind: "move_path", docId: m.
|
|
462
|
+
const rev = readDocBase(store.db, m._id)?.currentRev ?? null;
|
|
463
|
+
effects.push({ kind: "move_path", docId: m._id, from: m.from, to: m.to, currentRev: rev, inbound: inboundLinksTo(store, req.repoId, m._id, m.from), bytes });
|
|
464
464
|
if (stat)
|
|
465
465
|
effects.push({ kind: "clear_stat", path: m.from }, { kind: "record_stat", path: m.to, bytes });
|
|
466
466
|
}
|
|
@@ -477,10 +477,10 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
477
477
|
effects.push(e);
|
|
478
478
|
}
|
|
479
479
|
for (const t of plan.opset.tombstones) {
|
|
480
|
-
const rev = readDocBase(store.db, t.
|
|
481
|
-
effects.push({ kind: "tombstone", docId: t.
|
|
480
|
+
const rev = readDocBase(store.db, t._id)?.currentRev ?? null;
|
|
481
|
+
effects.push({ kind: "tombstone", docId: t._id, path: t._path, currentRev: rev, pool: false });
|
|
482
482
|
if (stat)
|
|
483
|
-
effects.push({ kind: "clear_stat", path: t.
|
|
483
|
+
effects.push({ kind: "clear_stat", path: t._path });
|
|
484
484
|
}
|
|
485
485
|
const outcome = commitChangeset(store, req.repoId, head, effects, ts, { docStore, ...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}) });
|
|
486
486
|
return { effects, outcome };
|
|
@@ -491,9 +491,9 @@ function executeAdmitted(store, req, plan, docStore) {
|
|
|
491
491
|
let j = 0;
|
|
492
492
|
for (const e of effects) {
|
|
493
493
|
if (e.kind === "ingest_reconciled")
|
|
494
|
-
revisions.push({
|
|
494
|
+
revisions.push({ _id: outcome.ingests[j++].docId, _path: e.path, _hash: sha256(e.source).toString("hex") });
|
|
495
495
|
else if (e.kind === "ingest_known")
|
|
496
|
-
revisions.push({
|
|
496
|
+
revisions.push({ _id: outcome.ingests[j++].docId, _path: e.path, _hash: sha256(e.rendered).toString("hex") });
|
|
497
497
|
}
|
|
498
498
|
// `$new.n` → minted id: the i-th block symbol is the i-th `b` the replay
|
|
499
499
|
// minted (all before the transaction, in the plan's order — the aliases
|
|
@@ -22,8 +22,8 @@ export interface Selection {
|
|
|
22
22
|
export interface EffectOrigin {
|
|
23
23
|
template_index?: number;
|
|
24
24
|
row: {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
_id: string;
|
|
26
|
+
_path: string;
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
export interface CompiledSelection {
|
|
@@ -56,8 +56,8 @@ export declare function desugarLeader(v: string, here: string): unknown;
|
|
|
56
56
|
* Desugar one effect or template as written (§11.8 "Sugar"), before the shape
|
|
57
57
|
* check and the admission: in every slot that admits an expression — the
|
|
58
58
|
* strings `markdown`, `to`, `path`, `separator`, `at.before` / `at.after`,
|
|
59
|
-
* the JSON `value`, each top-level `create.frontmatter` value, a target's `
|
|
60
|
-
* / `
|
|
59
|
+
* the JSON `value`, each top-level `create.frontmatter` value, a target's `_id`
|
|
60
|
+
* / `_path`, and a whole target (`into`, `target`, `targets[k]`) — a string
|
|
61
61
|
* beginning `-> ` becomes `{ oqx }` (or folds to its literal); a `-> ` string
|
|
62
62
|
* anywhere else (`expect`, `field`, `op`, a section's `heading_id`, …) is
|
|
63
63
|
* refused. Every other string is literal data, with no escaping and no `${`
|
|
@@ -42,9 +42,9 @@ function isRecord(v) {
|
|
|
42
42
|
function isOqx(v) {
|
|
43
43
|
return isRecord(v) && Object.keys(v).length === 1 && typeof v.oqx === "string";
|
|
44
44
|
}
|
|
45
|
-
/** A row rendered as a value by the runner (`{
|
|
45
|
+
/** A row rendered as a value by the runner (`{ _id, _path }` and nothing else, spec/surface §1.4). */
|
|
46
46
|
function isRowRef(v) {
|
|
47
|
-
return isRecord(v) && Object.keys(v).length === 2 && typeof v.
|
|
47
|
+
return isRecord(v) && Object.keys(v).length === 2 && typeof v._id === "string" && typeof v._path === "string";
|
|
48
48
|
}
|
|
49
49
|
function describeValue(v) {
|
|
50
50
|
if (v === null || v === undefined)
|
|
@@ -63,8 +63,8 @@ function describeValue(v) {
|
|
|
63
63
|
// OQX has no expression entry point: each `{ oqx: e }` is parsed through this
|
|
64
64
|
// wrapper and the one select item taken. A top-level item is scope depth 1 —
|
|
65
65
|
// exactly the hit's scope — so `^` and `N^` validate as they will evaluate.
|
|
66
|
-
const WRAPPER_PREFIX = "select __omg_x: ";
|
|
67
|
-
const WRAPPER_SUFFIX = "
|
|
66
|
+
const WRAPPER_PREFIX = "docs { select __omg_x: ";
|
|
67
|
+
const WRAPPER_SUFFIX = " }";
|
|
68
68
|
const OFFSET_RE = /\(at offset (\d+)\)/;
|
|
69
69
|
/** Parse one expression; a parse error's offset is shifted back by the wrapper and forward by `shift` (the `-> ` leader's 3). */
|
|
70
70
|
function parseExpression(src, here, shift = 0) {
|
|
@@ -80,7 +80,8 @@ function parseExpression(src, here, shift = 0) {
|
|
|
80
80
|
throw selectionInvalid(`${here}: ${message}`);
|
|
81
81
|
}
|
|
82
82
|
// The whole source must have been one expression: a query smuggled into the
|
|
83
|
-
// slot (`x
|
|
83
|
+
// slot (`x where … }` closing the wrapper early does not; `x }` + `{ … }`
|
|
84
|
+
// cannot) parses, but not as one item over `docs`.
|
|
84
85
|
const one = q.select.length === 1 && q.source.kind === "ident" && q.source.name === "docs" && q.from.length === 0 && q.where === null
|
|
85
86
|
&& q.orderBy === null && q.follow === null && q.frontier === null && q.limit === null && q.offset === null && !q.distinct && !q.values && q.consumer === "collect";
|
|
86
87
|
if (!one)
|
|
@@ -144,8 +145,8 @@ function refuseLeader(v, here, path, fix) {
|
|
|
144
145
|
* Desugar one effect or template as written (§11.8 "Sugar"), before the shape
|
|
145
146
|
* check and the admission: in every slot that admits an expression — the
|
|
146
147
|
* strings `markdown`, `to`, `path`, `separator`, `at.before` / `at.after`,
|
|
147
|
-
* the JSON `value`, each top-level `create.frontmatter` value, a target's `
|
|
148
|
-
* / `
|
|
148
|
+
* the JSON `value`, each top-level `create.frontmatter` value, a target's `_id`
|
|
149
|
+
* / `_path`, and a whole target (`into`, `target`, `targets[k]`) — a string
|
|
149
150
|
* beginning `-> ` becomes `{ oqx }` (or folds to its literal); a `-> ` string
|
|
150
151
|
* anywhere else (`expect`, `field`, `op`, a section's `heading_id`, …) is
|
|
151
152
|
* refused. Every other string is literal data, with no escaping and no `${`
|
|
@@ -174,11 +175,11 @@ export function desugarEffect(raw, index, request) {
|
|
|
174
175
|
if (isLeader(t))
|
|
175
176
|
out[key] = desugarLeader(t, `${here}.${key}`);
|
|
176
177
|
else if (isRecord(t)) {
|
|
177
|
-
for (const member of ["
|
|
178
|
+
for (const member of ["_id", "_path"])
|
|
178
179
|
if (isLeader(t[member]))
|
|
179
180
|
t[member] = desugarLeader(t[member], `${here}.${key}.${member}`);
|
|
180
181
|
for (const k of Object.keys(t))
|
|
181
|
-
if (k !== "
|
|
182
|
+
if (k !== "_id" && k !== "_path")
|
|
182
183
|
refuse(t[k], `${here}.${key}.${k}`, [key, k]);
|
|
183
184
|
}
|
|
184
185
|
};
|
|
@@ -191,10 +192,10 @@ export function desugarEffect(raw, index, request) {
|
|
|
191
192
|
if (isLeader(t))
|
|
192
193
|
list[k] = desugarLeader(t, `${here}.targets[${k}]`);
|
|
193
194
|
else if (isRecord(t)) {
|
|
194
|
-
if (isLeader(t.
|
|
195
|
-
t.
|
|
195
|
+
if (isLeader(t._id))
|
|
196
|
+
t._id = desugarLeader(t._id, `${here}.targets[${k}]._id`);
|
|
196
197
|
for (const m of Object.keys(t))
|
|
197
|
-
if (m !== "
|
|
198
|
+
if (m !== "_id")
|
|
198
199
|
refuse(t[m], `${here}.targets[${k}].${m}`, ["targets", String(k), m]);
|
|
199
200
|
}
|
|
200
201
|
});
|
|
@@ -328,7 +329,7 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
328
329
|
probe[key] = null;
|
|
329
330
|
slots.push({ name: key, kind: "json", here: `${here}.${key}`, path: [key], expression: src, item: expr(src, key), put: (e, v) => { e[key] = v; } });
|
|
330
331
|
};
|
|
331
|
-
// A target member: `{ kind,
|
|
332
|
+
// A target member: `{ kind, _id: { oqx } }` or `{ kind: "document", _path: { oqx } }`, or (3.2) the whole target
|
|
332
333
|
// `{ oqx }` — the identity the value names; absent → the hit when `defaultable` (never on a plain effect).
|
|
333
334
|
// `probeKind` is a kind the slot admits, standing in for the whole-target expression's unknown kind.
|
|
334
335
|
const targetSlot = (key, defaultable, probeKind, primary) => {
|
|
@@ -337,7 +338,7 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
337
338
|
if (t === undefined) {
|
|
338
339
|
if (!defaultable || plain)
|
|
339
340
|
return null;
|
|
340
|
-
probe[key] = { kind: HIT_KIND[root],
|
|
341
|
+
probe[key] = { kind: HIT_KIND[root], _id: "$sel" };
|
|
341
342
|
return { key, list: false };
|
|
342
343
|
}
|
|
343
344
|
if (isOqx(t)) {
|
|
@@ -345,19 +346,19 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
345
346
|
throw needsSelector(`${here}.${key}`);
|
|
346
347
|
const src = t.oqx;
|
|
347
348
|
delete base[key];
|
|
348
|
-
probe[key] = { kind: probeKind,
|
|
349
|
+
probe[key] = { kind: probeKind, _id: "$sel" };
|
|
349
350
|
slots.push({ name: key, kind: "target", here: `${here}.${key}`, path: [key], expression: src, item: expr(src, key), put: (e, v) => { e[key] = v; } });
|
|
350
351
|
return null;
|
|
351
352
|
}
|
|
352
353
|
if (isRecord(t)) {
|
|
353
|
-
const idX = isOqx(t.
|
|
354
|
-
const pathX = isOqx(t.
|
|
354
|
+
const idX = isOqx(t._id);
|
|
355
|
+
const pathX = isOqx(t._path);
|
|
355
356
|
if (idX && pathX)
|
|
356
|
-
throw selectionInvalid(`${here}.${key}: give \`
|
|
357
|
+
throw selectionInvalid(`${here}.${key}: give \`_id\` or \`_path\` as an expression, not both`);
|
|
357
358
|
if (idX || pathX) {
|
|
358
359
|
if (plain && primary)
|
|
359
360
|
throw needsSelector(`${here}.${key}`);
|
|
360
|
-
const member = idX ? "
|
|
361
|
+
const member = idX ? "_id" : "_path";
|
|
361
362
|
const src = t[member].oqx;
|
|
362
363
|
const literal = { ...t };
|
|
363
364
|
delete literal[member];
|
|
@@ -369,12 +370,12 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
369
370
|
});
|
|
370
371
|
}
|
|
371
372
|
for (const k of Object.keys(t))
|
|
372
|
-
if (k !== "
|
|
373
|
+
if (k !== "_id" && k !== "_path" && containsOqx(t[k]))
|
|
373
374
|
throw selectionInvalid(`${here}.${key}.${k}: \`{ oqx }\` is not allowed here`);
|
|
374
375
|
}
|
|
375
376
|
return null;
|
|
376
377
|
};
|
|
377
|
-
// A `targets` list: each element `{ kind: "block",
|
|
378
|
+
// A `targets` list: each element `{ kind: "block", _id: { oqx } }` (or, 3.2, a whole `{ oqx }`) yields one block;
|
|
378
379
|
// absent → `[the hit]` when `defaultable`. The list is always the primary target of its op.
|
|
379
380
|
const targetsSlot = (defaultable) => {
|
|
380
381
|
slotKeys.add("targets");
|
|
@@ -382,7 +383,7 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
382
383
|
if (list === undefined) {
|
|
383
384
|
if (!defaultable || plain)
|
|
384
385
|
return null;
|
|
385
|
-
probe.targets = [{ kind: HIT_KIND[root],
|
|
386
|
+
probe.targets = [{ kind: HIT_KIND[root], _id: "$sel" }];
|
|
386
387
|
return { key: "targets", list: true };
|
|
387
388
|
}
|
|
388
389
|
if (Array.isArray(list)) {
|
|
@@ -392,7 +393,7 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
392
393
|
throw needsSelector(`${here}.targets[${k}]`);
|
|
393
394
|
const src = t.oqx;
|
|
394
395
|
base.targets[k] = {};
|
|
395
|
-
probe.targets[k] = { kind: "block",
|
|
396
|
+
probe.targets[k] = { kind: "block", _id: "$sel" };
|
|
396
397
|
slots.push({
|
|
397
398
|
name: `targets_${k}`, kind: "target", here: `${here}.targets[${k}]`, path: ["targets", String(k)], expression: src, item: expr(src, `targets[${k}]`),
|
|
398
399
|
put: (e, v) => { e.targets[k] = v; },
|
|
@@ -401,23 +402,23 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
401
402
|
}
|
|
402
403
|
if (!isRecord(t))
|
|
403
404
|
return;
|
|
404
|
-
if (isOqx(t.
|
|
405
|
-
throw selectionInvalid(`${here}.targets[${k}]: a block target has \`
|
|
406
|
-
if (isOqx(t.
|
|
405
|
+
if (isOqx(t._path))
|
|
406
|
+
throw selectionInvalid(`${here}.targets[${k}]: a block target has \`_id\`, not \`_path\``);
|
|
407
|
+
if (isOqx(t._id)) {
|
|
407
408
|
if (plain)
|
|
408
|
-
throw needsSelector(`${here}.targets[${k}].
|
|
409
|
-
const src = t.
|
|
409
|
+
throw needsSelector(`${here}.targets[${k}]._id`);
|
|
410
|
+
const src = t._id.oqx;
|
|
410
411
|
const literal = { ...t };
|
|
411
|
-
delete literal.
|
|
412
|
+
delete literal._id;
|
|
412
413
|
base.targets[k] = literal;
|
|
413
|
-
probe.targets[k].
|
|
414
|
+
probe.targets[k]._id = "$sel";
|
|
414
415
|
slots.push({
|
|
415
|
-
name: `targets_${k}`, kind: "identity", here: `${here}.targets[${k}].
|
|
416
|
-
put: (e, v) => { const arr = e.targets; arr[k] = { ...arr[k], ...identityMember("
|
|
416
|
+
name: `targets_${k}`, kind: "identity", here: `${here}.targets[${k}]._id`, path: ["targets", String(k), "_id"], expression: src, item: expr(src, `targets[${k}]._id`),
|
|
417
|
+
put: (e, v) => { const arr = e.targets; arr[k] = { ...arr[k], ...identityMember("_id", v) }; },
|
|
417
418
|
});
|
|
418
419
|
}
|
|
419
420
|
for (const key of Object.keys(t))
|
|
420
|
-
if (key !== "
|
|
421
|
+
if (key !== "_id" && containsOqx(t[key]))
|
|
421
422
|
throw selectionInvalid(`${here}.targets[${k}].${key}: \`{ oqx }\` is not allowed here`);
|
|
422
423
|
});
|
|
423
424
|
}
|
|
@@ -505,10 +506,10 @@ function admitTemplate(raw, index, root, plain, fix) {
|
|
|
505
506
|
validateEffect(probe, index, fix);
|
|
506
507
|
return { index, base, slots, defaultTarget };
|
|
507
508
|
}
|
|
508
|
-
/** The `
|
|
509
|
+
/** The `_id`/`_path` member a bound identity value supplies: a string as given, a row ref by its id. */
|
|
509
510
|
function identityMember(member, v) {
|
|
510
511
|
if (isRowRef(v))
|
|
511
|
-
return {
|
|
512
|
+
return { _id: v._id };
|
|
512
513
|
return { [member]: v };
|
|
513
514
|
}
|
|
514
515
|
// ---- the request ---------------------------------------------------------------------
|
|
@@ -570,7 +571,7 @@ function admitSelector(select, request) {
|
|
|
570
571
|
throw selectionInvalid("select: `revisions` rows have no effect target — select docs, blocks or nodes", rootFix(select, raw.source, request));
|
|
571
572
|
}
|
|
572
573
|
if (root === null)
|
|
573
|
-
throw selectionInvalid("select: a selector scans a root collection — `
|
|
574
|
+
throw selectionInvalid("select: a selector scans a root collection — `docs { … }`, `blocks { … }` or `nodes { … }`", rootFix(select, raw.source, request));
|
|
574
575
|
if (root === "links")
|
|
575
576
|
throw selectionInvalid("select: `links` rows have no effect target — select docs, blocks or nodes", rootFix(select, raw.source, request));
|
|
576
577
|
if (raw.follow !== null)
|
|
@@ -624,7 +625,7 @@ export function compileSelection(store, repoId, select, expectRaw, templatesRaw)
|
|
|
624
625
|
}
|
|
625
626
|
// Row → effects: hits in result order × templates in order (k = h·T + t). A
|
|
626
627
|
// selector with `flatten` (oqx 0.25; §11.8) fans out per FLATTENED row: each
|
|
627
|
-
// hit is the parent row (its `
|
|
628
|
+
// hit is the parent row (its `_id`/`_path`), the slot items read the bound name
|
|
628
629
|
// (`-> tag`) in the flattened scope, and `expect.max` — the page bound above —
|
|
629
630
|
// counts the flattened hits.
|
|
630
631
|
const effects = [];
|
|
@@ -632,7 +633,7 @@ export function compileSelection(store, repoId, select, expectRaw, templatesRaw)
|
|
|
632
633
|
hits.forEach((hit) => {
|
|
633
634
|
for (const t of templates) {
|
|
634
635
|
const k = effects.length;
|
|
635
|
-
const row = {
|
|
636
|
+
const row = { _id: hit._id, _path: hit._path };
|
|
636
637
|
const origin = { template_index: t.index, row };
|
|
637
638
|
effects.push(bind(hit, t, k, origin, root, request));
|
|
638
639
|
origins.push(origin);
|
|
@@ -643,7 +644,7 @@ export function compileSelection(store, repoId, select, expectRaw, templatesRaw)
|
|
|
643
644
|
...(expect !== undefined ? { expect } : {}),
|
|
644
645
|
templates: templatesRaw,
|
|
645
646
|
cardinality: hits.length,
|
|
646
|
-
identities: hits.map((h) => h.
|
|
647
|
+
identities: hits.map((h) => h._id),
|
|
647
648
|
};
|
|
648
649
|
return { effects, selection, origins };
|
|
649
650
|
}
|
|
@@ -682,20 +683,20 @@ function slotName(t, slot) {
|
|
|
682
683
|
*/
|
|
683
684
|
function ambiguousFix(request, t, s, hit, root, list) {
|
|
684
685
|
const head = list[0];
|
|
685
|
-
const first = isRowRef(head) ? head.
|
|
686
|
+
const first = isRowRef(head) ? head._id : head;
|
|
686
687
|
if (typeof first !== "string")
|
|
687
688
|
return {};
|
|
688
689
|
const effects = withMember(request.effects, [String(t.index), ...s.path], first);
|
|
689
690
|
if (request.select === undefined)
|
|
690
691
|
return { fix: fixRequest({ effects }) };
|
|
691
|
-
return { fix: fixRequest({ select: narrowedTo(root, hit.
|
|
692
|
+
return { fix: fixRequest({ select: narrowedTo(root, hit._id), effects, expect: { max: 1 } }) };
|
|
692
693
|
}
|
|
693
694
|
/** One hit × one template → the concrete effect (the binding table of §11.8). */
|
|
694
695
|
function bind(hit, t, k, origin, root, request) {
|
|
695
696
|
const e = clone(t.base);
|
|
696
697
|
const data = { effect_index: k, ...(origin.template_index !== undefined ? { template_index: origin.template_index } : {}), row: origin.row };
|
|
697
698
|
if (t.defaultTarget !== null) {
|
|
698
|
-
const target = { kind: HIT_KIND[root],
|
|
699
|
+
const target = { kind: HIT_KIND[root], _id: hit._id };
|
|
699
700
|
e[t.defaultTarget.key] = t.defaultTarget.list ? [target] : target;
|
|
700
701
|
}
|
|
701
702
|
for (const s of t.slots) {
|
|
@@ -705,10 +706,10 @@ function bind(hit, t, k, origin, root, request) {
|
|
|
705
706
|
case "identity":
|
|
706
707
|
case "target": {
|
|
707
708
|
if (absent || (Array.isArray(v) && v.length === 0)) {
|
|
708
|
-
throw new MutationError("target_missing", `${s.here}: the expression yielded no identity for row ${hit.
|
|
709
|
+
throw new MutationError("target_missing", `${s.here}: the expression yielded no identity for row ${hit._id}`, { ...data, expression: s.expression });
|
|
709
710
|
}
|
|
710
711
|
if (Array.isArray(v)) {
|
|
711
|
-
throw new MutationError("ambiguous_target", `${s.here}: the expression yielded ${v.length} identities for row ${hit.
|
|
712
|
+
throw new MutationError("ambiguous_target", `${s.here}: the expression yielded ${v.length} identities for row ${hit._id}; a target resolves to exactly one`, { ...data, expression: s.expression, count: v.length, ...ambiguousFix(request, t, s, hit, root, v) });
|
|
712
713
|
}
|
|
713
714
|
if (typeof v !== "string" && !isRowRef(v))
|
|
714
715
|
throw selectionInvalid(`${s.here}: the expression yielded ${describeValue(v)}, not an identity (a string or a row)`, { ...data, expression: s.expression });
|
|
@@ -717,7 +718,7 @@ function bind(hit, t, k, origin, root, request) {
|
|
|
717
718
|
break;
|
|
718
719
|
}
|
|
719
720
|
// (3.2) a whole target: a row by its id's prefix, a string as a bare target (a path → document, an id → its kind).
|
|
720
|
-
const ref = isRowRef(v) ? v.
|
|
721
|
+
const ref = isRowRef(v) ? v._id : v;
|
|
721
722
|
if (!isRowRef(v) && !looksLikePath(ref) && !isTargetId(ref) && !isNewSymbol(ref)) {
|
|
722
723
|
throw selectionInvalid(`${s.here}: the expression yielded ${describeValue(v)}, not an identity (a document path, an id or a row)`, { ...data, expression: s.expression });
|
|
723
724
|
}
|
|
@@ -726,14 +727,14 @@ function bind(hit, t, k, origin, root, request) {
|
|
|
726
727
|
}
|
|
727
728
|
case "string":
|
|
728
729
|
if (absent)
|
|
729
|
-
throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit.
|
|
730
|
+
throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit._id}`, { ...data, expression: s.expression });
|
|
730
731
|
if (typeof v !== "string")
|
|
731
732
|
throw selectionInvalid(`${s.here}: the expression yielded ${describeValue(v)}, expected a string`, { ...data, expression: s.expression });
|
|
732
733
|
s.put(e, v);
|
|
733
734
|
break;
|
|
734
735
|
case "json":
|
|
735
736
|
if (absent)
|
|
736
|
-
throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit.
|
|
737
|
+
throw selectionInvalid(`${s.here}: the expression yielded absent for row ${hit._id}${s.name === "value" ? " — use `unset` to remove a key" : ""}`, { ...data, expression: s.expression });
|
|
737
738
|
s.put(e, v);
|
|
738
739
|
break;
|
|
739
740
|
}
|
|
@@ -813,25 +814,25 @@ function scopeOf(store, repoId, e, i) {
|
|
|
813
814
|
validateEffect(e, i); // an unknown op (or `retarget`): the shape check refuses it
|
|
814
815
|
throw effectInvalid(`${here}: \`op\` ${JSON.stringify(e.op)} is not row-shaped`);
|
|
815
816
|
}
|
|
816
|
-
if (isOqx(v) || (isRecord(v) && (isOqx(v.
|
|
817
|
+
if (isOqx(v) || (isRecord(v) && (isOqx(v._id) || isOqx(v._path))))
|
|
817
818
|
throw needsSelector(`${here}.${key}`);
|
|
818
819
|
const t = validateTarget(v, `${here}.${key}`, kinds);
|
|
819
820
|
switch (t.kind) {
|
|
820
821
|
case "symbol":
|
|
821
|
-
throw selectionInvalid(`${here}.${key}: ${t.
|
|
822
|
+
throw selectionInvalid(`${here}.${key}: ${t._id} has no row yet — it is created by this plan; use a literal target or a selector`);
|
|
822
823
|
case "document": {
|
|
823
824
|
const d = findDocumentTarget(store, repoId, t);
|
|
824
|
-
return { root: "docs", id: d.
|
|
825
|
+
return { root: "docs", id: d._id, missing: () => new MutationError("doc_missing", `doc ${d._id} not found`, { doc: d._id }) };
|
|
825
826
|
}
|
|
826
827
|
case "block":
|
|
827
828
|
return {
|
|
828
|
-
root: "blocks", id: t.
|
|
829
|
-
missing: () => (placement ? new MutationError("parent_missing", `parent ${t.
|
|
829
|
+
root: "blocks", id: t._id,
|
|
830
|
+
missing: () => (placement ? new MutationError("parent_missing", `parent ${t._id} not found`) : new MutationError("block_missing", `block ${t._id} not found`, { block: t._id })),
|
|
830
831
|
};
|
|
831
832
|
case "section":
|
|
832
833
|
return { root: "blocks", id: t.heading_id, missing: () => new MutationError("parent_missing", `parent ${t.heading_id} not found`) };
|
|
833
834
|
case "node":
|
|
834
|
-
return { root: "nodes", id: t.
|
|
835
|
+
return { root: "nodes", id: t._id, missing: () => new MutationError("block_missing", `node ${t._id} not found`, {}) };
|
|
835
836
|
}
|
|
836
837
|
}
|
|
837
838
|
/**
|
|
@@ -839,7 +840,7 @@ function scopeOf(store, repoId, e, i) {
|
|
|
839
840
|
* then — when nothing is an expression — the shape check as before (3.0
|
|
840
841
|
* byte-identical). Otherwise each effect with expressions is admitted as a
|
|
841
842
|
* template over its own primary target's row: the effects are grouped by that
|
|
842
|
-
* row's identity, one synthetic query per identity — `from <root> where
|
|
843
|
+
* row's identity, one synthetic query per identity — `from <root> where _id ==
|
|
843
844
|
* "<id>"` with the slot items appended, named `__omg_sel_<effectIndex>_<slot>`
|
|
844
845
|
* — runs, and the row binds the slots exactly as a selector's hit would. The
|
|
845
846
|
* concrete effects feed the lowering unchanged; `templates` is the list as
|
|
@@ -893,7 +894,7 @@ export function compilePlain(store, repoId, raw) {
|
|
|
893
894
|
g.members.push(t);
|
|
894
895
|
});
|
|
895
896
|
for (const g of scopes.values()) {
|
|
896
|
-
const q = parse(`from ${g.root} where
|
|
897
|
+
const q = parse(`from ${g.root} where _id == ${JSON.stringify(g.id)}`);
|
|
897
898
|
const items = [];
|
|
898
899
|
for (const t of g.members)
|
|
899
900
|
for (const s of t.slots)
|
|
@@ -903,7 +904,7 @@ export function compilePlain(store, repoId, raw) {
|
|
|
903
904
|
if (hit === undefined)
|
|
904
905
|
throw g.missing();
|
|
905
906
|
for (const t of g.members) {
|
|
906
|
-
const origin = { row: {
|
|
907
|
+
const origin = { row: { _id: hit._id, _path: hit._path } };
|
|
907
908
|
effects[t.index] = bind(hit, t, t.index, origin, g.root, request);
|
|
908
909
|
origins[t.index] = origin;
|
|
909
910
|
}
|