@voyant-travel/core 0.109.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/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/config.d.ts +204 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +251 -0
- package/dist/config.js.map +1 -0
- package/dist/container.d.ts +20 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +21 -0
- package/dist/container.js.map +1 -0
- package/dist/env.d.ts +29 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -0
- package/dist/env.js.map +1 -0
- package/dist/events.d.ts +177 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +169 -0
- package/dist/events.js.map +1 -0
- package/dist/hooks.d.ts +19 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +33 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/links.d.ts +201 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +159 -0
- package/dist/links.js.map +1 -0
- package/dist/locking.d.ts +12 -0
- package/dist/locking.d.ts.map +1 -0
- package/dist/locking.js +21 -0
- package/dist/locking.js.map +1 -0
- package/dist/module.d.ts +147 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +2 -0
- package/dist/module.js.map +1 -0
- package/dist/orchestration.d.ts +30 -0
- package/dist/orchestration.d.ts.map +1 -0
- package/dist/orchestration.js +2 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/plugin.d.ts +118 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +65 -0
- package/dist/plugin.js.map +1 -0
- package/dist/query.d.ts +111 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +126 -0
- package/dist/query.js.map +1 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +31 -0
- package/dist/registry.js.map +1 -0
- package/dist/workflows.d.ts +140 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +142 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +103 -0
package/dist/events.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Stable, unique event id for envelope metadata / outbox dedup. */
|
|
2
|
+
export function generateEventId() {
|
|
3
|
+
return `evt_${crypto.randomUUID()}`;
|
|
4
|
+
}
|
|
5
|
+
const DEFAULT_HANDLER_TIMEOUT_MS = 15_000;
|
|
6
|
+
/**
|
|
7
|
+
* Create an in-process event bus.
|
|
8
|
+
*
|
|
9
|
+
* Handlers run **in parallel** — they are independent observers by
|
|
10
|
+
* contract, so one slow subscriber doesn't serialize behind another.
|
|
11
|
+
* Errors thrown by a handler are caught and logged and never affect the
|
|
12
|
+
* emitter or sibling handlers ("subscribers are fire-and-forget").
|
|
13
|
+
* Each handler is bounded by {@link EventBusOptions.handlerTimeoutMs}.
|
|
14
|
+
*
|
|
15
|
+
* When the emitter passes {@link EmitOptions.schedule}, handlers not
|
|
16
|
+
* marked `inline` are handed to the scheduler as one promise and
|
|
17
|
+
* `emit()` resolves without waiting for them — this is how the HTTP
|
|
18
|
+
* runtime moves subscriber work (third-party syncs, notifications)
|
|
19
|
+
* after the response.
|
|
20
|
+
*/
|
|
21
|
+
export function createEventBus(options = {}) {
|
|
22
|
+
const handlers = new Map();
|
|
23
|
+
const timeoutMs = options.handlerTimeoutMs ?? DEFAULT_HANDLER_TIMEOUT_MS;
|
|
24
|
+
/**
|
|
25
|
+
* Runs one handler; never rejects. Returns the error message when the
|
|
26
|
+
* handler threw or timed out, `null` on success. Errors are always
|
|
27
|
+
* logged here so non-durable emits keep today's observability.
|
|
28
|
+
*/
|
|
29
|
+
async function runHandler(event, handler, envelope) {
|
|
30
|
+
try {
|
|
31
|
+
if (timeoutMs === false) {
|
|
32
|
+
await handler(envelope);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
let timer = null;
|
|
36
|
+
const timedOut = new Promise((resolve) => {
|
|
37
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
const result = await Promise.race([
|
|
41
|
+
Promise.resolve(handler(envelope)).then(() => "done"),
|
|
42
|
+
timedOut,
|
|
43
|
+
]);
|
|
44
|
+
if (result === "timeout") {
|
|
45
|
+
// The handler keeps running detached — we just stop waiting.
|
|
46
|
+
// Counted as a failure for durable delivery: the retry relies
|
|
47
|
+
// on subscriber idempotency.
|
|
48
|
+
const message = `subscriber for "${event}" exceeded ${timeoutMs}ms; not awaited`;
|
|
49
|
+
console.error(`[events] ${message}`);
|
|
50
|
+
return message;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (timer !== null)
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Subscribers are fire-and-forget — log and continue.
|
|
61
|
+
console.error(`[events] subscriber error for "${event}":`, err);
|
|
62
|
+
return err instanceof Error ? err.message : String(err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Runs a batch in parallel; resolves with the error messages. */
|
|
66
|
+
function runBatch(event, batch, envelope) {
|
|
67
|
+
return Promise.all(batch.map((h) => runHandler(event, h, envelope))).then((results) => results.filter((r) => r !== null));
|
|
68
|
+
}
|
|
69
|
+
function partition(event) {
|
|
70
|
+
const inline = [];
|
|
71
|
+
const deferrable = [];
|
|
72
|
+
const registered = handlers.get(event);
|
|
73
|
+
if (registered) {
|
|
74
|
+
for (const [handler, meta] of registered) {
|
|
75
|
+
;
|
|
76
|
+
(meta.inline ? inline : deferrable).push(handler);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { inline, deferrable };
|
|
80
|
+
}
|
|
81
|
+
function ensureEventId(metadata) {
|
|
82
|
+
if (metadata && typeof metadata.eventId === "string" && metadata.eventId.length > 0) {
|
|
83
|
+
return metadata;
|
|
84
|
+
}
|
|
85
|
+
return { ...(metadata ?? {}), eventId: generateEventId() };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
async emit(event, data, metadata, emitOptions) {
|
|
89
|
+
const store = emitOptions?.store;
|
|
90
|
+
const registered = handlers.get(event);
|
|
91
|
+
if (!store && (!registered || registered.size === 0))
|
|
92
|
+
return;
|
|
93
|
+
const envelope = {
|
|
94
|
+
name: event,
|
|
95
|
+
data,
|
|
96
|
+
emittedAt: new Date().toISOString(),
|
|
97
|
+
// Durable capture and downstream dedup (workflow forwarder) both
|
|
98
|
+
// key on a stable event id — stamp one when the caller didn't.
|
|
99
|
+
...(() => {
|
|
100
|
+
const withId = ensureEventId(metadata);
|
|
101
|
+
return withId === undefined ? {} : { metadata: withId };
|
|
102
|
+
})(),
|
|
103
|
+
};
|
|
104
|
+
const { inline, deferrable } = partition(event);
|
|
105
|
+
const run = (batch) => runBatch(event, batch, envelope);
|
|
106
|
+
if (!store) {
|
|
107
|
+
if (emitOptions?.schedule && deferrable.length > 0) {
|
|
108
|
+
emitOptions.schedule(run(deferrable));
|
|
109
|
+
if (inline.length > 0)
|
|
110
|
+
await run(inline);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await run([...inline, ...deferrable]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// ---- Durable (outbox) path ----
|
|
117
|
+
// Persist BEFORE any handler runs: a crash mid-delivery leaves a
|
|
118
|
+
// pending row the drain redelivers, instead of a lost event.
|
|
119
|
+
const record = await store.insert(envelope);
|
|
120
|
+
if (record === null) {
|
|
121
|
+
// Duplicate eventId — already captured; the original owns delivery.
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const settle = async (errors) => {
|
|
125
|
+
try {
|
|
126
|
+
if (errors.length === 0)
|
|
127
|
+
await store.complete(record.id);
|
|
128
|
+
else
|
|
129
|
+
await store.fail(record.id, errors.join("; "));
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// Best-effort bookkeeping: a failed `complete` leaves the row
|
|
133
|
+
// pending and the drain redelivers (at-least-once, idempotent
|
|
134
|
+
// subscribers). Never surface to the emitter.
|
|
135
|
+
console.error(`[events] outbox bookkeeping failed for "${event}":`, err);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
if (emitOptions.schedule && deferrable.length > 0) {
|
|
139
|
+
const inlineErrors = inline.length > 0 ? await run(inline) : [];
|
|
140
|
+
emitOptions.schedule(run(deferrable).then((deferredErrors) => settle([...inlineErrors, ...deferredErrors])));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const errors = await run([...inline, ...deferrable]);
|
|
144
|
+
await settle(errors);
|
|
145
|
+
},
|
|
146
|
+
async deliver(envelope) {
|
|
147
|
+
const { inline, deferrable } = partition(envelope.name);
|
|
148
|
+
const all = [...inline, ...deferrable];
|
|
149
|
+
if (all.length === 0)
|
|
150
|
+
return { attempted: 0, failed: 0, errors: [] };
|
|
151
|
+
const errors = await runBatch(envelope.name, all, envelope);
|
|
152
|
+
return { attempted: all.length, failed: errors.length, errors };
|
|
153
|
+
},
|
|
154
|
+
subscribe(event, handler, subscribeOptions) {
|
|
155
|
+
let registered = handlers.get(event);
|
|
156
|
+
if (!registered) {
|
|
157
|
+
registered = new Map();
|
|
158
|
+
handlers.set(event, registered);
|
|
159
|
+
}
|
|
160
|
+
registered.set(handler, { inline: subscribeOptions?.inline ?? false });
|
|
161
|
+
return {
|
|
162
|
+
unsubscribe() {
|
|
163
|
+
registered?.delete(handler);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type HookHandler = (...args: unknown[]) => Promise<void> | void;
|
|
2
|
+
/**
|
|
3
|
+
* Simple typed event emitter for module lifecycle hooks.
|
|
4
|
+
*
|
|
5
|
+
* Events are namespaced by convention: "module.event"
|
|
6
|
+
* (e.g., "contacts.beforeCreate", "bookings.afterConfirm")
|
|
7
|
+
*/
|
|
8
|
+
declare class HookSystem {
|
|
9
|
+
private handlers;
|
|
10
|
+
/** Register a handler for an event */
|
|
11
|
+
on(event: string, handler: HookHandler): void;
|
|
12
|
+
/** Remove a handler for an event */
|
|
13
|
+
off(event: string, handler: HookHandler): void;
|
|
14
|
+
/** Emit an event, calling all registered handlers sequentially */
|
|
15
|
+
emit(event: string, ...args: unknown[]): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare const hooks: HookSystem;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AAE/D;;;;;GAKG;AACH,cAAM,UAAU;IACd,OAAO,CAAC,QAAQ,CAAsC;IAEtD,sCAAsC;IACtC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAS7C,oCAAoC;IACpC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAI9C,kEAAkE;IAC5D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAO7D;AAED,eAAO,MAAM,KAAK,YAAmB,CAAA"}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple typed event emitter for module lifecycle hooks.
|
|
3
|
+
*
|
|
4
|
+
* Events are namespaced by convention: "module.event"
|
|
5
|
+
* (e.g., "contacts.beforeCreate", "bookings.afterConfirm")
|
|
6
|
+
*/
|
|
7
|
+
class HookSystem {
|
|
8
|
+
handlers = new Map();
|
|
9
|
+
/** Register a handler for an event */
|
|
10
|
+
on(event, handler) {
|
|
11
|
+
let set = this.handlers.get(event);
|
|
12
|
+
if (!set) {
|
|
13
|
+
set = new Set();
|
|
14
|
+
this.handlers.set(event, set);
|
|
15
|
+
}
|
|
16
|
+
set.add(handler);
|
|
17
|
+
}
|
|
18
|
+
/** Remove a handler for an event */
|
|
19
|
+
off(event, handler) {
|
|
20
|
+
this.handlers.get(event)?.delete(handler);
|
|
21
|
+
}
|
|
22
|
+
/** Emit an event, calling all registered handlers sequentially */
|
|
23
|
+
async emit(event, ...args) {
|
|
24
|
+
const set = this.handlers.get(event);
|
|
25
|
+
if (!set)
|
|
26
|
+
return;
|
|
27
|
+
for (const handler of set) {
|
|
28
|
+
await handler(...args);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export const hooks = new HookSystem();
|
|
33
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU;IACN,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAA;IAEtD,sCAAsC;IACtC,EAAE,CAAC,KAAa,EAAE,OAAoB;QACpC,IAAI,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;QAC/B,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAClB,CAAC;IAED,oCAAoC;IACpC,GAAG,CAAC,KAAa,EAAE,OAAoB;QACrC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IAC3C,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,IAAI,CAAC,KAAa,EAAE,GAAG,IAAe;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACpC,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,KAAK,MAAM,OAAO,IAAI,GAAG,EAAE,CAAC;YAC1B,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;QACxB,CAAC;IACH,CAAC;CACF;AAED,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type { AdminConfig, AdminRoutesConfig, ConfigValidationIssue, ConfigValidationResult, DeploymentTarget, ModuleEntry, PluginEntry, ProjectConfig, VoyantConfig, } from "./config.js";
|
|
2
|
+
export { defineVoyantConfig, resolveEntry, validateVoyantConfig } from "./config.js";
|
|
3
|
+
export type { ModuleContainer } from "./container.js";
|
|
4
|
+
export { createContainer } from "./container.js";
|
|
5
|
+
export type { Actor, VoyantAuthContext, VoyantCallerType, VoyantPermission, VoyantVariables, } from "./env.js";
|
|
6
|
+
export type { DeliveryResult, EmitOptions, EventBus, EventBusOptions, EventCategory, EventEnvelope, EventHandler, EventMetadata, EventSource, OutboxEventStore, SubscribeOptions, Subscription, } from "./events.js";
|
|
7
|
+
export { createEventBus, generateEventId } from "./events.js";
|
|
8
|
+
export { hooks } from "./hooks.js";
|
|
9
|
+
export type { LinkableDefinition, LinkCardinality, LinkDefinition, LinkDefinitionOptions, LinkKeyRecord, LinkListFilter, LinkRow, LinkService, LinkSide, LinkSideInput, LinkSpec, LinkTableSql, ResolvedLinkSpec, } from "./links.js";
|
|
10
|
+
export { defineLink, generateLinkTableSql, resolveLinkFromSpec } from "./links.js";
|
|
11
|
+
export type { ExclusiveExecutionResult, ExecutionLockManager, } from "./locking.js";
|
|
12
|
+
export { createInMemoryExecutionLockManager } from "./locking.js";
|
|
13
|
+
export type { BootstrapContext, BootstrapHandler, EventFilterDescriptor, Extension, Module, WorkflowDescriptor, } from "./module.js";
|
|
14
|
+
export type { JobOptions, JobRunner } from "./orchestration.js";
|
|
15
|
+
export type { Plugin, RegisteredPlugins, RegisterPluginsOptions, Subscriber, } from "./plugin.js";
|
|
16
|
+
export { definePlugin, registerPlugins } from "./plugin.js";
|
|
17
|
+
export type { EntityFetcher, EntityFetcherArgs, EntityRecord, QueryContextValue, QueryFilters, QueryGraphConfig, QueryGraphContext, QueryGraphResult, QueryPagination, QueryRunner, } from "./query.js";
|
|
18
|
+
export { createQueryContext, createQueryRunner, queryGraph } from "./query.js";
|
|
19
|
+
export type { RegistryOptions } from "./registry.js";
|
|
20
|
+
export { createRegistry } from "./registry.js";
|
|
21
|
+
export type { StepBuilder, StepCompensateFn, StepDefinition, StepRunFn, WorkflowContext, WorkflowDefinition, WorkflowResult, WorkflowRunOptions, } from "./workflows.js";
|
|
22
|
+
export { createWorkflow, step, WorkflowError } from "./workflows.js";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,aAAa,EACb,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACpF,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,YAAY,EACV,KAAK,EACL,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA;AACjB,YAAY,EACV,cAAc,EACd,WAAW,EACX,QAAQ,EACR,eAAe,EACf,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,OAAO,EACP,WAAW,EACX,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAClF,YAAY,EACV,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,kCAAkC,EAAE,MAAM,cAAc,CAAA;AACjE,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,MAAM,EACN,kBAAkB,GACnB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC/D,YAAY,EACV,MAAM,EACN,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC3D,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,WAAW,GACZ,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC9E,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { defineVoyantConfig, resolveEntry, validateVoyantConfig } from "./config.js";
|
|
2
|
+
export { createContainer } from "./container.js";
|
|
3
|
+
export { createEventBus, generateEventId } from "./events.js";
|
|
4
|
+
export { hooks } from "./hooks.js";
|
|
5
|
+
export { defineLink, generateLinkTableSql, resolveLinkFromSpec } from "./links.js";
|
|
6
|
+
export { createInMemoryExecutionLockManager } from "./locking.js";
|
|
7
|
+
export { definePlugin, registerPlugins } from "./plugin.js";
|
|
8
|
+
export { createQueryContext, createQueryRunner, queryGraph } from "./query.js";
|
|
9
|
+
export { createRegistry } from "./registry.js";
|
|
10
|
+
export { createWorkflow, step, WorkflowError } from "./workflows.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAEpF,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAsBhD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAgBlC,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAKlF,OAAO,EAAE,kCAAkC,EAAE,MAAM,cAAc,CAAA;AAgBjE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAa3D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE9E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAW9C,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Links — cross-module associations that live in "neutral territory"
|
|
3
|
+
* outside any specific module's schema.
|
|
4
|
+
*
|
|
5
|
+
* Each link defines a pivot table that contains only ID pairs. There are no
|
|
6
|
+
* DB-level foreign key constraints, so modules remain independently
|
|
7
|
+
* swappable, and templates can introduce new cross-module references
|
|
8
|
+
* without modifying any module package.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Metadata describing a linkable entity — the entity kind that can participate
|
|
12
|
+
* in a link from another module.
|
|
13
|
+
*
|
|
14
|
+
* Each module exposes one {@link LinkableDefinition} per entity it wants to
|
|
15
|
+
* expose to other modules (e.g. a CRM module exposes `person` and
|
|
16
|
+
* `organization`).
|
|
17
|
+
*/
|
|
18
|
+
export interface LinkableDefinition {
|
|
19
|
+
/** Owning module name (e.g. "crm"). */
|
|
20
|
+
module: string;
|
|
21
|
+
/** Entity name within the module (e.g. "person"). Used to build the join-column name. */
|
|
22
|
+
entity: string;
|
|
23
|
+
/** Underlying DB table name (informational; used by codegen / diagnostics). */
|
|
24
|
+
table: string;
|
|
25
|
+
/** Primary-key column name on the entity's table. Defaults to "id". */
|
|
26
|
+
primaryKey?: string;
|
|
27
|
+
/** TypeID prefix for IDs of this entity (informational). */
|
|
28
|
+
idPrefix?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One side of a link. `isList: true` means that, from the opposite side,
|
|
32
|
+
* this entity appears as a list.
|
|
33
|
+
*/
|
|
34
|
+
export interface LinkSide {
|
|
35
|
+
linkable: LinkableDefinition;
|
|
36
|
+
isList?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export type LinkSideInput = LinkableDefinition | LinkSide;
|
|
39
|
+
export interface LinkDefinitionOptions {
|
|
40
|
+
/**
|
|
41
|
+
* When true, deleting the link row should cascade-delete the target side
|
|
42
|
+
* (enforced by the link service, not the database).
|
|
43
|
+
*/
|
|
44
|
+
deleteCascade?: boolean;
|
|
45
|
+
database?: {
|
|
46
|
+
/** Override the auto-generated pivot table name. */
|
|
47
|
+
tableName?: string;
|
|
48
|
+
/** Override the auto-generated left-side ID column name. */
|
|
49
|
+
leftColumn?: string;
|
|
50
|
+
/** Override the auto-generated right-side ID column name. */
|
|
51
|
+
rightColumn?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Mark the link as externally-owned and non-materialized.
|
|
55
|
+
*
|
|
56
|
+
* Read-only links participate in cross-module query traversal, but they do
|
|
57
|
+
* not create a neutral-territory pivot table and cannot be mutated through
|
|
58
|
+
* the runtime link service.
|
|
59
|
+
*/
|
|
60
|
+
readOnly?: {
|
|
61
|
+
list(filter?: {
|
|
62
|
+
leftId?: string;
|
|
63
|
+
rightId?: string;
|
|
64
|
+
}): Promise<LinkRow[]>;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export type LinkCardinality = "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
|
68
|
+
/**
|
|
69
|
+
* A fully-resolved link definition. Produced by {@link defineLink} and
|
|
70
|
+
* consumed by codegen (`generateLinkTableSql`) and the runtime link service.
|
|
71
|
+
*/
|
|
72
|
+
export interface LinkDefinition {
|
|
73
|
+
left: LinkSide;
|
|
74
|
+
right: LinkSide;
|
|
75
|
+
/** Generated pivot table name (e.g. "crm_person_products_product"). */
|
|
76
|
+
tableName: string;
|
|
77
|
+
/** Left-side ID column name in the pivot table (e.g. "crm_person_id"). */
|
|
78
|
+
leftColumn: string;
|
|
79
|
+
/** Right-side ID column name in the pivot table (e.g. "products_product_id"). */
|
|
80
|
+
rightColumn: string;
|
|
81
|
+
/** Inferred cardinality from the `isList` flags on each side. */
|
|
82
|
+
cardinality: LinkCardinality;
|
|
83
|
+
deleteCascade: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Read-only links are resolved from an externally-owned relation instead of
|
|
86
|
+
* a generated pivot table.
|
|
87
|
+
*/
|
|
88
|
+
readOnly?: {
|
|
89
|
+
list(filter?: {
|
|
90
|
+
leftId?: string;
|
|
91
|
+
rightId?: string;
|
|
92
|
+
}): Promise<LinkRow[]>;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Declare a link between two linkable entities.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // one-to-one
|
|
101
|
+
* defineLink(crm.linkable.person, products.linkable.product)
|
|
102
|
+
*
|
|
103
|
+
* // one-to-many (each person has many products)
|
|
104
|
+
* defineLink(
|
|
105
|
+
* crm.linkable.person,
|
|
106
|
+
* { linkable: products.linkable.product, isList: true },
|
|
107
|
+
* )
|
|
108
|
+
*
|
|
109
|
+
* // many-to-many
|
|
110
|
+
* defineLink(
|
|
111
|
+
* { linkable: crm.linkable.person, isList: true },
|
|
112
|
+
* { linkable: products.linkable.product, isList: true },
|
|
113
|
+
* )
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function defineLink(left: LinkSideInput, right: LinkSideInput, options?: LinkDefinitionOptions): LinkDefinition;
|
|
117
|
+
/**
|
|
118
|
+
* SQL DDL for materializing a link's pivot table.
|
|
119
|
+
*/
|
|
120
|
+
export interface LinkTableSql {
|
|
121
|
+
/** `CREATE TABLE IF NOT EXISTS …` statement. */
|
|
122
|
+
createTable: string;
|
|
123
|
+
/** `CREATE [UNIQUE] INDEX IF NOT EXISTS …` statements. */
|
|
124
|
+
indexes: string[];
|
|
125
|
+
}
|
|
126
|
+
export declare function generateLinkTableSql(def: LinkDefinition): LinkTableSql;
|
|
127
|
+
/**
|
|
128
|
+
* Medusa-style link spec keyed by module name, then by join-column name.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* {
|
|
133
|
+
* crm: { person_id: "pers_abc" },
|
|
134
|
+
* products: { product_id: "prod_xyz" },
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export type LinkKeyRecord = Record<string, string>;
|
|
139
|
+
export type LinkSpec = Record<string, LinkKeyRecord>;
|
|
140
|
+
export interface ResolvedLinkSpec {
|
|
141
|
+
definition: LinkDefinition;
|
|
142
|
+
leftId: string;
|
|
143
|
+
rightId: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Match a {@link LinkSpec} to a {@link LinkDefinition} from the provided list,
|
|
147
|
+
* returning the matched definition along with the resolved left/right IDs
|
|
148
|
+
* (in the order the definition declares).
|
|
149
|
+
*
|
|
150
|
+
* Throws when the spec doesn't match exactly one definition.
|
|
151
|
+
*/
|
|
152
|
+
export declare function resolveLinkFromSpec(spec: LinkSpec, defs: LinkDefinition[]): ResolvedLinkSpec;
|
|
153
|
+
/**
|
|
154
|
+
* A row in a link's pivot table.
|
|
155
|
+
*/
|
|
156
|
+
export interface LinkRow {
|
|
157
|
+
id: string;
|
|
158
|
+
leftId: string;
|
|
159
|
+
rightId: string;
|
|
160
|
+
createdAt: Date;
|
|
161
|
+
updatedAt: Date;
|
|
162
|
+
deletedAt: Date | null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Filter for {@link LinkService.list}. All provided fields are ANDed.
|
|
166
|
+
*
|
|
167
|
+
* The plural fields (`leftIds`/`rightIds`) match any of the given IDs in a
|
|
168
|
+
* single batched lookup — implementations MUST resolve them with one
|
|
169
|
+
* query/roundtrip, not one per ID. An empty array can never match and
|
|
170
|
+
* short-circuits to `[]`. Singular and plural fields for the same side
|
|
171
|
+
* combine by intersection.
|
|
172
|
+
*/
|
|
173
|
+
export interface LinkListFilter {
|
|
174
|
+
/** Match a single left-side ID. */
|
|
175
|
+
leftId?: string;
|
|
176
|
+
/** Match a single right-side ID. */
|
|
177
|
+
rightId?: string;
|
|
178
|
+
/** Match any of the given left-side IDs (one batched query). */
|
|
179
|
+
leftIds?: string[];
|
|
180
|
+
/** Match any of the given right-side IDs (one batched query). */
|
|
181
|
+
rightIds?: string[];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Runtime service for manipulating link rows. Templates register an
|
|
185
|
+
* implementation in the module container under `"link"`.
|
|
186
|
+
*/
|
|
187
|
+
export interface LinkService {
|
|
188
|
+
/** Create a link between two entity IDs. Idempotent on the unique pair. */
|
|
189
|
+
create(linkKey: string, leftId: string, rightId: string): Promise<LinkRow>;
|
|
190
|
+
/** Create using a Medusa-style module-keyed spec. */
|
|
191
|
+
create(spec: LinkSpec): Promise<LinkRow>;
|
|
192
|
+
/** Soft-delete a link (sets `deleted_at`). */
|
|
193
|
+
dismiss(linkKey: string, leftId: string, rightId: string): Promise<void>;
|
|
194
|
+
dismiss(spec: LinkSpec): Promise<void>;
|
|
195
|
+
/** Hard-delete a link row. */
|
|
196
|
+
delete(linkKey: string, leftId: string, rightId: string): Promise<void>;
|
|
197
|
+
delete(spec: LinkSpec): Promise<void>;
|
|
198
|
+
/** List link rows matching the given filter (non-soft-deleted only). */
|
|
199
|
+
list(linkKey: string, filter?: LinkListFilter): Promise<LinkRow[]>;
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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"}
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Links — cross-module associations that live in "neutral territory"
|
|
3
|
+
* outside any specific module's schema.
|
|
4
|
+
*
|
|
5
|
+
* Each link defines a pivot table that contains only ID pairs. There are no
|
|
6
|
+
* DB-level foreign key constraints, so modules remain independently
|
|
7
|
+
* swappable, and templates can introduce new cross-module references
|
|
8
|
+
* without modifying any module package.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Declare a link between two linkable entities.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // one-to-one
|
|
16
|
+
* defineLink(crm.linkable.person, products.linkable.product)
|
|
17
|
+
*
|
|
18
|
+
* // one-to-many (each person has many products)
|
|
19
|
+
* defineLink(
|
|
20
|
+
* crm.linkable.person,
|
|
21
|
+
* { linkable: products.linkable.product, isList: true },
|
|
22
|
+
* )
|
|
23
|
+
*
|
|
24
|
+
* // many-to-many
|
|
25
|
+
* defineLink(
|
|
26
|
+
* { linkable: crm.linkable.person, isList: true },
|
|
27
|
+
* { linkable: products.linkable.product, isList: true },
|
|
28
|
+
* )
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function defineLink(left, right, options) {
|
|
32
|
+
const leftSide = normalizeSide(left);
|
|
33
|
+
const rightSide = normalizeSide(right);
|
|
34
|
+
const tableName = options?.database?.tableName ?? defaultLinkTableName(leftSide, rightSide);
|
|
35
|
+
const leftColumn = options?.database?.leftColumn ?? defaultColumnName(leftSide);
|
|
36
|
+
const rightColumn = options?.database?.rightColumn ?? defaultColumnName(rightSide);
|
|
37
|
+
if (leftColumn === rightColumn) {
|
|
38
|
+
throw new Error(`defineLink: left and right column names would collide ("${leftColumn}"). ` +
|
|
39
|
+
`Provide distinct database.leftColumn / database.rightColumn overrides.`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
left: leftSide,
|
|
43
|
+
right: rightSide,
|
|
44
|
+
tableName,
|
|
45
|
+
leftColumn,
|
|
46
|
+
rightColumn,
|
|
47
|
+
cardinality: cardinalityFor(leftSide, rightSide),
|
|
48
|
+
deleteCascade: options?.deleteCascade ?? false,
|
|
49
|
+
readOnly: options?.readOnly,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function normalizeSide(input) {
|
|
53
|
+
if ("linkable" in input) {
|
|
54
|
+
return { linkable: input.linkable, isList: input.isList ?? false };
|
|
55
|
+
}
|
|
56
|
+
return { linkable: input, isList: false };
|
|
57
|
+
}
|
|
58
|
+
function defaultLinkTableName(left, right) {
|
|
59
|
+
return `${left.linkable.module}_${left.linkable.entity}_${right.linkable.module}_${right.linkable.entity}`;
|
|
60
|
+
}
|
|
61
|
+
function defaultColumnName(side) {
|
|
62
|
+
return `${side.linkable.module}_${side.linkable.entity}_id`;
|
|
63
|
+
}
|
|
64
|
+
function cardinalityFor(left, right) {
|
|
65
|
+
if (!left.isList && !right.isList)
|
|
66
|
+
return "one-to-one";
|
|
67
|
+
if (!left.isList && right.isList)
|
|
68
|
+
return "one-to-many";
|
|
69
|
+
if (left.isList && !right.isList)
|
|
70
|
+
return "many-to-one";
|
|
71
|
+
return "many-to-many";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate the SQL needed to materialize a link's pivot table.
|
|
75
|
+
*
|
|
76
|
+
* Quote a SQL identifier, escaping embedded double quotes by doubling them.
|
|
77
|
+
*
|
|
78
|
+
* Defense in depth: link identifiers are developer-authored (never HTTP
|
|
79
|
+
* input), but routing them through a quoting helper guarantees the emitted
|
|
80
|
+
* DDL stays well-formed even for hostile or accidental quote-containing
|
|
81
|
+
* names (see security assessment L6).
|
|
82
|
+
*/
|
|
83
|
+
function quoteIdent(identifier) {
|
|
84
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
85
|
+
}
|
|
86
|
+
export function generateLinkTableSql(def) {
|
|
87
|
+
if (def.readOnly) {
|
|
88
|
+
throw new Error(`generateLinkTableSql: read-only link "${def.tableName}" is externally owned and does not materialize a pivot table`);
|
|
89
|
+
}
|
|
90
|
+
const { tableName, leftColumn, rightColumn } = def;
|
|
91
|
+
const table = quoteIdent(tableName);
|
|
92
|
+
const left = quoteIdent(leftColumn);
|
|
93
|
+
const right = quoteIdent(rightColumn);
|
|
94
|
+
const createTable = [
|
|
95
|
+
`CREATE TABLE IF NOT EXISTS ${table} (`,
|
|
96
|
+
` "id" text PRIMARY KEY NOT NULL,`,
|
|
97
|
+
` ${left} text NOT NULL,`,
|
|
98
|
+
` ${right} text NOT NULL,`,
|
|
99
|
+
` "created_at" timestamp with time zone DEFAULT now() NOT NULL,`,
|
|
100
|
+
` "updated_at" timestamp with time zone DEFAULT now() NOT NULL,`,
|
|
101
|
+
` "deleted_at" timestamp with time zone`,
|
|
102
|
+
`)`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
// Postgres truncates identifiers at 63 chars; keep index suffixes short.
|
|
105
|
+
const indexes = [];
|
|
106
|
+
// Dedupe the pair (ignoring soft-deleted rows).
|
|
107
|
+
indexes.push(`CREATE UNIQUE INDEX IF NOT EXISTS ${quoteIdent(`${tableName}_pair_idx`)} ON ${table} (${left}, ${right}) WHERE "deleted_at" IS NULL`);
|
|
108
|
+
// !right.isList → each left has ≤1 right → left_id must be unique.
|
|
109
|
+
if (!def.right.isList) {
|
|
110
|
+
indexes.push(`CREATE UNIQUE INDEX IF NOT EXISTS ${quoteIdent(`${tableName}_l_uniq`)} ON ${table} (${left}) WHERE "deleted_at" IS NULL`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
indexes.push(`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${tableName}_l_idx`)} ON ${table} (${left}) WHERE "deleted_at" IS NULL`);
|
|
114
|
+
}
|
|
115
|
+
// !left.isList → each right has ≤1 left → right_id must be unique.
|
|
116
|
+
if (!def.left.isList) {
|
|
117
|
+
indexes.push(`CREATE UNIQUE INDEX IF NOT EXISTS ${quoteIdent(`${tableName}_r_uniq`)} ON ${table} (${right}) WHERE "deleted_at" IS NULL`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
indexes.push(`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${tableName}_r_idx`)} ON ${table} (${right}) WHERE "deleted_at" IS NULL`);
|
|
121
|
+
}
|
|
122
|
+
return { createTable, indexes };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Match a {@link LinkSpec} to a {@link LinkDefinition} from the provided list,
|
|
126
|
+
* returning the matched definition along with the resolved left/right IDs
|
|
127
|
+
* (in the order the definition declares).
|
|
128
|
+
*
|
|
129
|
+
* Throws when the spec doesn't match exactly one definition.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveLinkFromSpec(spec, defs) {
|
|
132
|
+
const entries = Object.entries(spec);
|
|
133
|
+
if (entries.length !== 2) {
|
|
134
|
+
throw new Error(`resolveLinkFromSpec: expected exactly 2 module keys, got ${entries.length}`);
|
|
135
|
+
}
|
|
136
|
+
const [first, second] = entries;
|
|
137
|
+
for (const def of defs) {
|
|
138
|
+
const leftModule = def.left.linkable.module;
|
|
139
|
+
const rightModule = def.right.linkable.module;
|
|
140
|
+
const leftKey = `${def.left.linkable.entity}_id`;
|
|
141
|
+
const rightKey = `${def.right.linkable.entity}_id`;
|
|
142
|
+
// Spec order matches definition order.
|
|
143
|
+
if (first[0] === leftModule && second[0] === rightModule) {
|
|
144
|
+
const leftId = first[1][leftKey];
|
|
145
|
+
const rightId = second[1][rightKey];
|
|
146
|
+
if (leftId && rightId)
|
|
147
|
+
return { definition: def, leftId, rightId };
|
|
148
|
+
}
|
|
149
|
+
// Spec order is reversed.
|
|
150
|
+
if (second[0] === leftModule && first[0] === rightModule) {
|
|
151
|
+
const leftId = second[1][leftKey];
|
|
152
|
+
const rightId = first[1][rightKey];
|
|
153
|
+
if (leftId && rightId)
|
|
154
|
+
return { definition: def, leftId, rightId };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`resolveLinkFromSpec: no LinkDefinition matches spec keys [${first[0]}, ${second[0]}]`);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=links.js.map
|