@jarenjs/linq 0.56.0 → 0.66.1
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/ARCHITECTURE.md +10 -0
- package/README.md +93 -2
- package/docs/APP-PEN.md +3 -3
- package/docs/CONTRACT-PEN.md +10 -6
- package/docs/DB-CLIENT.md +84 -16
- package/docs/FLOW-PEN.md +12 -5
- package/docs/FORMS-PEN.md +2 -2
- package/docs/JSLT-PEN.md +4 -4
- package/docs/LINQ-FORMAT.md +41 -34
- package/docs/MIGRATION-PEN.md +2 -2
- package/docs/MODEL-PEN.md +15 -6
- package/docs/QUERY-PEN.md +107 -19
- package/docs/SCHEMA-PEN.md +2 -2
- package/package.json +6 -6
- package/src/app/action.js +4 -8
- package/src/app/define.js +8 -13
- package/src/async.js +58 -10
- package/src/concurrency.js +40 -8
- package/src/contract/define.js +23 -10
- package/src/contract/index.js +5 -5
- package/src/contract/operation.js +10 -14
- package/src/db/handle.js +3 -0
- package/src/db/include.js +40 -5
- package/src/db/index.js +5 -0
- package/src/db/ledger.js +195 -0
- package/src/db/open.js +59 -11
- package/src/errors.js +10 -1
- package/src/expression.js +30 -4
- package/src/federate.js +531 -0
- package/src/flow/dag.js +28 -14
- package/src/flow/fsm.js +6 -11
- package/src/index.js +1 -0
- package/src/jslt/rules.js +7 -12
- package/src/migration/define.js +9 -14
- package/src/migration/steps.js +5 -9
- package/src/model/collection.js +102 -0
- package/src/model/index.js +1 -1
- package/types/contract.d.ts +115 -18
- package/types/db.d.ts +182 -11
- package/types/index.d.ts +65 -0
- package/types/model.d.ts +34 -1
package/src/db/ledger.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `createDbLedger(client, options)`: the contract ledger
|
|
4
|
+
* (`@jarenjs/contract` docs/CONTRACT-FORMAT.md §8) over a declared
|
|
5
|
+
* collection of the client's store — every claim state, the persisted
|
|
6
|
+
* generation fence, expiry, `sweep` — through the client's collection
|
|
7
|
+
* and transaction surface alone. Nothing here imports the contract
|
|
8
|
+
* package or a driver: the ledger SHAPE is structural (the
|
|
9
|
+
* `claim`/`commit`/`fail`/`lookup` the http binding calls), the record
|
|
10
|
+
* is exactly `idempotencyLedgerModel`'s, and the id is the same
|
|
11
|
+
* versioned JSON tuple the memory ledger spells. A root client claims
|
|
12
|
+
* inside `transaction(…, { mode: 'immediate' })` — one writer, so two
|
|
13
|
+
* processes never both hold `new`; a transaction client claims and
|
|
14
|
+
* settles inside the transaction it was handed, which is the ledger a
|
|
15
|
+
* lifecycle settlement lease carries: a domain write and the settlement
|
|
16
|
+
* then commit together or not at all.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { resolveRuntime } from '@jarenjs/core/runtime';
|
|
20
|
+
|
|
21
|
+
import { LinqRuntimeError } from '../errors.js';
|
|
22
|
+
|
|
23
|
+
/** One day, the default retention of a key — the memory ledger's. */
|
|
24
|
+
const DEFAULT_TTL_MS = 86_400_000;
|
|
25
|
+
|
|
26
|
+
/** The collection `idempotencyLedgerModel` declares. */
|
|
27
|
+
const DEFAULT_COLLECTION = 'ledger';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The id of one `(op, scope, key)` tuple — the version `1`, a colon, the
|
|
31
|
+
* JSON array of the three: the spelling `@jarenjs/contract/ledger`'s
|
|
32
|
+
* `ledgerId` writes, so a store's records and a memory ledger's agree.
|
|
33
|
+
* A record written under the legacy `"<op>|<scope>|<key>"` spelling is
|
|
34
|
+
* matched by no claim again; it expires by its own `expiresAt` (`sweep`)
|
|
35
|
+
* or a host rewrites it once (DB-CLIENT.md §2.6).
|
|
36
|
+
* @param {string} op
|
|
37
|
+
* @param {string} scope
|
|
38
|
+
* @param {string} key
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function ledgerId(op, scope, key) {
|
|
42
|
+
return `1:${JSON.stringify([op, scope, key])}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} DbLedgerOptions
|
|
47
|
+
* @property {string} [collection] - the declared collection, `'ledger'` by default
|
|
48
|
+
* @property {number} [ttlMs] - the retention of a key, 86,400,000 ms by default
|
|
49
|
+
* @property {Partial<import('@jarenjs/core/runtime').Runtime>} [runtime] -
|
|
50
|
+
* the host's runtime record: its `now` is the clock, its `uuid` mints
|
|
51
|
+
* every generation
|
|
52
|
+
* @property {() => number} [now] - the clock; wins over the runtime's
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {unknown} ref
|
|
57
|
+
* @param {string} reason
|
|
58
|
+
* @returns {LinqRuntimeError}
|
|
59
|
+
*/
|
|
60
|
+
function stale(ref, reason) {
|
|
61
|
+
const r = /** @type {any} */ (ref);
|
|
62
|
+
const named = r !== null && typeof r === 'object' && typeof r.id === 'string' ? r.id : 'a ref this ledger did not issue';
|
|
63
|
+
return new LinqRuntimeError('JL2007', `${named} settles no started record: ${reason}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The durable ledger over a client's declared collection.
|
|
68
|
+
* @param {any} client - a `@jarenjs/linq/db` client: the root one, or
|
|
69
|
+
* the one a transaction callback received
|
|
70
|
+
* @param {DbLedgerOptions} [options]
|
|
71
|
+
* @returns {{ claim: (claim: { op: string, scope: string, key: string, hash: string, now?: number }) => Promise<any>,
|
|
72
|
+
* commit: (ref: unknown, response: unknown, now?: number) => Promise<void>,
|
|
73
|
+
* fail: (ref: unknown, retryable: boolean, response?: unknown, now?: number) => Promise<void>,
|
|
74
|
+
* lookup: (key: { op: string, scope: string, key: string, now?: number }) => Promise<any>,
|
|
75
|
+
* sweep: (now?: number) => Promise<number> }}
|
|
76
|
+
*/
|
|
77
|
+
export function createDbLedger(client, options = {}) {
|
|
78
|
+
if (client === null || typeof client !== 'object' || client.collections === null
|
|
79
|
+
|| typeof client.collections !== 'object' || typeof client.transaction !== 'function') {
|
|
80
|
+
throw new TypeError('createDbLedger: client must be a @jarenjs/linq/db client — the root client, or the one a transaction callback received');
|
|
81
|
+
}
|
|
82
|
+
const name = options.collection === undefined ? DEFAULT_COLLECTION : options.collection;
|
|
83
|
+
if (typeof name !== 'string' || name === '') throw new TypeError('createDbLedger: collection must be a non-empty collection name');
|
|
84
|
+
if (!Object.hasOwn(client.collections, name)) {
|
|
85
|
+
throw new TypeError(`createDbLedger: the client declares no collection '${name}' — open the store with idempotencyLedgerModel, or a model that declares it`);
|
|
86
|
+
}
|
|
87
|
+
const ttlMs = options.ttlMs === undefined ? DEFAULT_TTL_MS : options.ttlMs;
|
|
88
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) throw new TypeError('createDbLedger: ttlMs must be a positive number');
|
|
89
|
+
let runtime;
|
|
90
|
+
try {
|
|
91
|
+
runtime = resolveRuntime(options.runtime);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new TypeError(`createDbLedger: runtime: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
const clock = options.now === undefined ? runtime.now : options.now;
|
|
97
|
+
if (typeof clock !== 'function') throw new TypeError('createDbLedger: now must be a function');
|
|
98
|
+
// the memory ledger's clock discipline: one clock judges a record from
|
|
99
|
+
// claim to expiry — its own when given, else the binding's instants
|
|
100
|
+
const ownClock = options.now !== undefined || options.runtime !== undefined;
|
|
101
|
+
/** @type {number | null} */
|
|
102
|
+
let latest = null;
|
|
103
|
+
/** @param {number | undefined} given */
|
|
104
|
+
const instant = (given) => {
|
|
105
|
+
if (typeof given === 'number') {
|
|
106
|
+
if (!ownClock) latest = latest === null ? given : Math.max(latest, given);
|
|
107
|
+
return given;
|
|
108
|
+
}
|
|
109
|
+
return ownClock || latest === null ? clock() : latest;
|
|
110
|
+
};
|
|
111
|
+
// a root client owns a connection and takes the write lock up front; a
|
|
112
|
+
// transaction client is inside one already and nests a savepoint, so
|
|
113
|
+
// the settlement commits with the host's own writes
|
|
114
|
+
const root = typeof client.close === 'function';
|
|
115
|
+
/** @param {(tx: any) => Promise<any>} fn */
|
|
116
|
+
const inside = (fn) => (root ? client.transaction(fn, { mode: 'immediate' }) : client.transaction(fn));
|
|
117
|
+
/** @param {any} tx */
|
|
118
|
+
const rows = (tx) => tx.collections[name];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Settle the started record a ref names, or refuse (`JL2007`).
|
|
122
|
+
* @param {unknown} ref
|
|
123
|
+
* @param {{ status: 'committed' | 'failed', response: unknown, retryable: boolean | null }} changes
|
|
124
|
+
* @param {number | undefined} now
|
|
125
|
+
*/
|
|
126
|
+
function settle(ref, changes, now) {
|
|
127
|
+
const r = /** @type {any} */ (ref);
|
|
128
|
+
if (r === null || typeof r !== 'object' || typeof r.id !== 'string' || typeof r.generation !== 'string') {
|
|
129
|
+
return Promise.reject(stale(ref, 'a ref is { id, generation } as this ledger issued it'));
|
|
130
|
+
}
|
|
131
|
+
const at = instant(now);
|
|
132
|
+
return inside(async (tx) => {
|
|
133
|
+
const c = rows(tx);
|
|
134
|
+
const record = await c.get(r.id);
|
|
135
|
+
if (record === undefined || record.generation !== r.generation || record.status !== 'started') {
|
|
136
|
+
throw stale(ref, 'the key expired, was reclaimed under a newer generation, or was settled already');
|
|
137
|
+
}
|
|
138
|
+
await c.put({ ...record, ...changes, updatedAt: at }, r.id);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
claim({ op, scope, key, hash, now }) {
|
|
144
|
+
const at = instant(now);
|
|
145
|
+
const id = ledgerId(op, scope, key);
|
|
146
|
+
return inside(async (tx) => {
|
|
147
|
+
const c = rows(tx);
|
|
148
|
+
const existing = await c.get(id);
|
|
149
|
+
if (existing !== undefined) {
|
|
150
|
+
if (existing.expiresAt <= at) await c.delete(id);
|
|
151
|
+
else if (existing.hash !== hash) return { state: 'mismatch' };
|
|
152
|
+
else if (existing.status === 'started') return { state: 'in-progress' };
|
|
153
|
+
else if (existing.status === 'committed') return { state: 'replay', response: existing.response };
|
|
154
|
+
else if (existing.retryable !== true && existing.response !== null) return { state: 'replay', response: existing.response };
|
|
155
|
+
else await c.delete(id); // a retryable failure: the key runs again
|
|
156
|
+
}
|
|
157
|
+
const generation = runtime.uuid();
|
|
158
|
+
await c.insert({
|
|
159
|
+
id, generation, op, scope, key, hash, status: 'started', response: null, retryable: null,
|
|
160
|
+
createdAt: at, updatedAt: at, expiresAt: at + ttlMs,
|
|
161
|
+
});
|
|
162
|
+
return { state: 'new', ref: Object.freeze({ id, generation }) };
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
commit(ref, response, now = undefined) {
|
|
166
|
+
return settle(ref, { status: 'committed', response, retryable: null }, now);
|
|
167
|
+
},
|
|
168
|
+
fail(ref, retryable, response = undefined, now = undefined) {
|
|
169
|
+
return settle(ref, { status: 'failed', response: response === undefined ? null : response, retryable: retryable === true }, now);
|
|
170
|
+
},
|
|
171
|
+
lookup({ op, scope, key, now = undefined }) {
|
|
172
|
+
const at = instant(now);
|
|
173
|
+
const id = ledgerId(op, scope, key);
|
|
174
|
+
return inside(async (tx) => {
|
|
175
|
+
const c = rows(tx);
|
|
176
|
+
const record = await c.get(id);
|
|
177
|
+
if (record === undefined) return null;
|
|
178
|
+
if (record.expiresAt <= at) {
|
|
179
|
+
await c.delete(id);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return record; // the store's fresh document: nothing of the ledger's own
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
sweep(now = undefined) {
|
|
186
|
+
const at = instant(now);
|
|
187
|
+
return inside(async (tx) => {
|
|
188
|
+
const c = rows(tx);
|
|
189
|
+
const expired = await c.where((/** @type {any} */ r) => r.expiresAt.le(at)).select((/** @type {any} */ r) => r.id).toArray();
|
|
190
|
+
for (const id of expired) await c.delete(id);
|
|
191
|
+
return expired.length;
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
package/src/db/open.js
CHANGED
|
@@ -55,21 +55,69 @@ export async function open(model, options) {
|
|
|
55
55
|
storeOptions.compileSchema = (schema) => jaren.compile(schema);
|
|
56
56
|
}
|
|
57
57
|
const store = await openStore(model, storeOptions);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The typed handles over ONE store view — the root store, or the one
|
|
61
|
+
* a transaction callback received. Building them from the same two
|
|
62
|
+
* constructors is what keeps a transaction's `entities.X` the same
|
|
63
|
+
* surface, with the same inference, as the client's own.
|
|
64
|
+
* @param {any} over
|
|
65
|
+
*/
|
|
66
|
+
const handlesOf = (over) => {
|
|
67
|
+
const entities = {};
|
|
68
|
+
for (const name of over.roots ?? []) {
|
|
69
|
+
setObjectMember(entities, name, createEntityHandle(over, name));
|
|
70
|
+
}
|
|
71
|
+
const collections = {};
|
|
72
|
+
for (const name of Object.keys(model.collections ?? {})) {
|
|
73
|
+
setObjectMember(collections, name, createCollectionHandle(over, name));
|
|
74
|
+
}
|
|
75
|
+
return { entities: Object.freeze(entities), collections: Object.freeze(collections) };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The client a transaction callback receives: the same shape as the
|
|
80
|
+
* root client, over the store that is INSIDE the transaction. Its
|
|
81
|
+
* handles run as the transaction's owner rather than waiting for a
|
|
82
|
+
* commit they are part of, and its `transaction` nests.
|
|
83
|
+
*
|
|
84
|
+
* Built per transaction, because the handles bind to the store view —
|
|
85
|
+
* and the whole point of a transaction's own unit of work is that two
|
|
86
|
+
* handlers do not share one.
|
|
87
|
+
* @param {any} tx - the store the callback received
|
|
88
|
+
*/
|
|
89
|
+
const transactionClient = (tx) => {
|
|
90
|
+
/** @type {Record<string, any>} */
|
|
91
|
+
const inner = {
|
|
92
|
+
store: tx,
|
|
93
|
+
capabilities: tx.capabilities,
|
|
94
|
+
...handlesOf(tx),
|
|
95
|
+
transaction: (fn) => tx.transaction((nested) => fn(transactionClient(nested))),
|
|
96
|
+
// the named-savepoint group (MODEL-FORMAT §5.2), forwarded as it
|
|
97
|
+
// is: partial rollback belongs to the transaction that owns the
|
|
98
|
+
// connection, so the root client deliberately has no twin
|
|
99
|
+
savepoints: tx.savepoints,
|
|
100
|
+
};
|
|
101
|
+
if (tx.saveChanges !== undefined) {
|
|
102
|
+
inner.saveChanges = () => tx.saveChanges();
|
|
103
|
+
inner.live = (source, liveOptions) => registerLive(tx.live, source, liveOptions);
|
|
104
|
+
}
|
|
105
|
+
return Object.freeze(inner);
|
|
106
|
+
};
|
|
107
|
+
|
|
66
108
|
/** @type {Record<string, any>} */
|
|
67
109
|
const client = {
|
|
68
110
|
store,
|
|
69
111
|
capabilities: store.capabilities,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
...handlesOf(store),
|
|
113
|
+
// A transaction gets its own unit of work by default: two handlers
|
|
114
|
+
// on one client then hold two records for the same entity key and
|
|
115
|
+
// neither can see the other's pending state. `unitOfWork: 'shared'`
|
|
116
|
+
// opts back into the store's, for a caller who staged changes
|
|
117
|
+
// outside the transaction and means to save them inside it.
|
|
118
|
+
transaction: (fn, transactionOptions) => store.transaction(
|
|
119
|
+
(tx) => fn(transactionClient(tx)),
|
|
120
|
+
{ unitOfWork: 'own', ...transactionOptions }),
|
|
73
121
|
close: (closeOptions) => store.close(closeOptions),
|
|
74
122
|
};
|
|
75
123
|
// the unit of work and entity live queries exist exactly when the
|
package/src/errors.js
CHANGED
|
@@ -27,7 +27,7 @@ export const LINQ_CODES = Object.freeze({
|
|
|
27
27
|
JL0102: 'a pen was asked for a construct the format cannot carry',
|
|
28
28
|
JL0103: 'a $defs name collision, a dangling ref, or an unnamed recursion',
|
|
29
29
|
JL0104: 'a pen-owned keyword through meta(), or an external a captured rule did not declare',
|
|
30
|
-
JL0105: 'a relation hop cannot lower: a
|
|
30
|
+
JL0105: 'a relation hop cannot lower: a composite or undeclared key, an incomplete many-to-many entry, or a malformed relation record',
|
|
31
31
|
JL0106: 'a migration step names a table the target model does not declare, or a draft it cannot match',
|
|
32
32
|
JL0107: 'a client operation named a member that is not the relation kind it needs',
|
|
33
33
|
JL2001: 'first/single found no element',
|
|
@@ -36,6 +36,8 @@ export const LINQ_CODES = Object.freeze({
|
|
|
36
36
|
JL2004: 'an asynchronous provider cannot back the synchronous surface',
|
|
37
37
|
JL2005: 'a push queue was fed after it ended',
|
|
38
38
|
JL2006: 'a provider answered an element terminal with something other than one array',
|
|
39
|
+
JL2007: 'a ledger settlement named a ref that settles no started record',
|
|
40
|
+
JL2008: 'a federated fetch reached its row or byte budget',
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -136,10 +138,17 @@ export class LinqBuildError extends CodedError {
|
|
|
136
138
|
* the provider directly instead.
|
|
137
139
|
* - `JL2005` — `feed()` was called on a push queue after `end()`
|
|
138
140
|
* closed it (a condition of the running stream, not of the build)
|
|
141
|
+
* - `JL2007` — `createDbLedger`'s `commit`/`fail` named a ref that
|
|
142
|
+
* settles no started record: the key expired, was reclaimed under a
|
|
143
|
+
* newer generation, or was settled already (DB-CLIENT.md §2.6)
|
|
139
144
|
* - `JL2006` — a provider answered an element terminal (`toArray`,
|
|
140
145
|
* `first`, …) with something other than exactly one array; the
|
|
141
146
|
* emitted document is an array constructor, so a conforming
|
|
142
147
|
* `execute()` never answers `undefined` there
|
|
148
|
+
* - `JL2008` — a `federate()` fetch reached one side's row or byte
|
|
149
|
+
* budget. A budget is a REFUSAL, not a spill: the fetch stops at the
|
|
150
|
+
* row that would have broken it, every cursor it opened is closed,
|
|
151
|
+
* and the reason names the side and the bound (QUERY-PEN.md §13)
|
|
143
152
|
*/
|
|
144
153
|
export class LinqRuntimeError extends CodedError {
|
|
145
154
|
/**
|
package/src/expression.js
CHANGED
|
@@ -512,10 +512,18 @@ function checkRelation(relation, member) {
|
|
|
512
512
|
+ '({ to, kind, via, fkEntity, fkTargets, targetKey } — MODEL-FORMAT §10.1)');
|
|
513
513
|
}
|
|
514
514
|
if (relation.kind === 'manyToMany') {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
515
|
+
// the join table is a read-only query root (MODEL-FORMAT §10.7), so
|
|
516
|
+
// the hop lowers through it — provided the relation record names
|
|
517
|
+
// the row's two columns and the key each references
|
|
518
|
+
if (typeof relation.joinTable !== 'string' || typeof relation.ownColumn !== 'string'
|
|
519
|
+
|| typeof relation.ownKey !== 'string' || typeof relation.targetColumn !== 'string'
|
|
520
|
+
|| typeof relation.targetKey !== 'string') {
|
|
521
|
+
throw new LinqBuildError('JL0105',
|
|
522
|
+
`'${member}' is a many-to-many relation whose entry does not name its join row's `
|
|
523
|
+
+ 'columns ({ joinTable, ownColumn, ownKey, targetColumn, targetKey } — '
|
|
524
|
+
+ 'MODEL-FORMAT §10.1), so the hop has no phrase to lower to');
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
519
527
|
}
|
|
520
528
|
if (relation.kind !== 'oneToOne' && relation.kind !== 'oneToMany') {
|
|
521
529
|
throw new LinqBuildError('JL0105',
|
|
@@ -599,6 +607,24 @@ function startHop(target, member) {
|
|
|
599
607
|
many = true;
|
|
600
608
|
fan = true;
|
|
601
609
|
}
|
|
610
|
+
if (relation.kind === 'manyToMany') {
|
|
611
|
+
// TWO links, numbered in chain order: the join row that names the
|
|
612
|
+
// membership, then the target row it names. The join table is a
|
|
613
|
+
// query root of its own, carrying exactly the two key columns (§10.7)
|
|
614
|
+
const joinBinding = `r${sink.next++}`;
|
|
615
|
+
const binding = `r${sink.next++}`;
|
|
616
|
+
chain.push({ binding: joinBinding, source: entityRootOf(relation.joinTable),
|
|
617
|
+
where: { $eq: [`$${joinBinding}${memberSegment(relation.ownColumn)}`,
|
|
618
|
+
`${subject}${memberSegment(relation.ownKey)}`] } });
|
|
619
|
+
chain.push({ binding, source: entityRootOf(relation.to),
|
|
620
|
+
where: { $eq: [`$${binding}${memberSegment(relation.targetKey)}`,
|
|
621
|
+
`$${joinBinding}${memberSegment(relation.targetColumn)}`] } });
|
|
622
|
+
sink.hops.push({ member, kind: relation.kind, binding });
|
|
623
|
+
const manyTarget = resolve(relation.to);
|
|
624
|
+
return makeHop({ chain, ret: '$' + binding, many: true, fan },
|
|
625
|
+
target.epoch,
|
|
626
|
+
manyTarget === undefined ? undefined : { table: manyTarget, resolve, sink });
|
|
627
|
+
}
|
|
602
628
|
const binding = `r${sink.next++}`;
|
|
603
629
|
const where = relation.kind === 'oneToMany'
|
|
604
630
|
? { $eq: [`$${binding}${memberSegment(relation.via)}`, `${subject}${memberSegment(relation.targetKey)}`] }
|