@fougere/core 0.1.0-alpha.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 +21 -0
- package/README.md +21 -0
- package/dist/binding.d.ts +63 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +107 -0
- package/dist/binding.js.map +1 -0
- package/dist/boot.d.ts +38 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +53 -0
- package/dist/boot.js.map +1 -0
- package/dist/bootstrap.d.ts +4 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +825 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/builtins/config.d.ts +4 -0
- package/dist/builtins/config.d.ts.map +1 -0
- package/dist/builtins/config.js +6 -0
- package/dist/builtins/config.js.map +1 -0
- package/dist/builtins/logger.d.ts +28 -0
- package/dist/builtins/logger.d.ts.map +1 -0
- package/dist/builtins/logger.js +76 -0
- package/dist/builtins/logger.js.map +1 -0
- package/dist/call.d.ts +172 -0
- package/dist/call.d.ts.map +1 -0
- package/dist/call.js +209 -0
- package/dist/call.js.map +1 -0
- package/dist/collector.d.ts +28 -0
- package/dist/collector.d.ts.map +1 -0
- package/dist/collector.js +32 -0
- package/dist/collector.js.map +1 -0
- package/dist/config-loader.d.ts +59 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +97 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/contract.d.ts +43 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +29 -0
- package/dist/contract.js.map +1 -0
- package/dist/crud.d.ts +61 -0
- package/dist/crud.d.ts.map +1 -0
- package/dist/crud.js +85 -0
- package/dist/crud.js.map +1 -0
- package/dist/define.d.ts +8 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +9 -0
- package/dist/define.js.map +1 -0
- package/dist/egress.d.ts +78 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +185 -0
- package/dist/egress.js.map +1 -0
- package/dist/emit.d.ts +51 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +57 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/frond-config.d.ts +80 -0
- package/dist/frond-config.d.ts.map +1 -0
- package/dist/frond-config.js +32 -0
- package/dist/frond-config.js.map +1 -0
- package/dist/graph.d.ts +33 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +142 -0
- package/dist/graph.js.map +1 -0
- package/dist/handler-parser.d.ts +82 -0
- package/dist/handler-parser.d.ts.map +1 -0
- package/dist/handler-parser.js +505 -0
- package/dist/handler-parser.js.map +1 -0
- package/dist/imports.d.ts +22 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +80 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/invocation.d.ts +20 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +15 -0
- package/dist/invocation.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +15 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +29 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/logger.d.ts +14 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +25 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware.d.ts +75 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +89 -0
- package/dist/middleware.js.map +1 -0
- package/dist/operation.d.ts +87 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +95 -0
- package/dist/operation.js.map +1 -0
- package/dist/orm.d.ts +96 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +33 -0
- package/dist/orm.js.map +1 -0
- package/dist/presenter.d.ts +63 -0
- package/dist/presenter.d.ts.map +1 -0
- package/dist/presenter.js +72 -0
- package/dist/presenter.js.map +1 -0
- package/dist/remote.d.ts +48 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +135 -0
- package/dist/remote.js.map +1 -0
- package/dist/repository.d.ts +55 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +26 -0
- package/dist/repository.js.map +1 -0
- package/dist/scan-cache.d.ts +10 -0
- package/dist/scan-cache.d.ts.map +1 -0
- package/dist/scan-cache.js +92 -0
- package/dist/scan-cache.js.map +1 -0
- package/dist/scanner.d.ts +34 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +595 -0
- package/dist/scanner.js.map +1 -0
- package/dist/seed.d.ts +28 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +110 -0
- package/dist/seed.js.map +1 -0
- package/dist/types.d.ts +387 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +57 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +147 -0
- package/dist/verify.js.map +1 -0
- package/package.json +47 -0
package/dist/egress.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value leaving the domain.
|
|
3
|
+
*
|
|
4
|
+
* Two things happen every time, in this order:
|
|
5
|
+
*
|
|
6
|
+
* 1. JUDGE — is this a legal value? Read from `shape` alone, so the answer does not
|
|
7
|
+
* depend on who is receiving. It has to come first: judging after the projection
|
|
8
|
+
* would fail on a client view that legitimately dropped its `writeOnly` fields.
|
|
9
|
+
* 2. PROJECT — what may THIS receiver see? A client does not get a password hash;
|
|
10
|
+
* storage sees everything.
|
|
11
|
+
*
|
|
12
|
+
* Storage is a way out like the others. It used to be the exception — the ORM wrote
|
|
13
|
+
* whatever a handler handed it, so `status: 'n-importe-quoi'` on a
|
|
14
|
+
* `oneOf('draft','published')` was stored and read back unchanged (measured
|
|
15
|
+
* 2026-07-25). The database is a weak judge: it catches nullability and, outside
|
|
16
|
+
* SQLite, the column type — never a closed set, a format or a range, since the DDL
|
|
17
|
+
* emits no CHECK. The choice was never "fail late or judge early", it was "corrupt
|
|
18
|
+
* silently or judge early".
|
|
19
|
+
*
|
|
20
|
+
* What the judge must NOT read: `boundary` and `lifecycle`. Both answer "may a CLIENT
|
|
21
|
+
* send this?", a question with no meaning when the domain itself is writing — reading
|
|
22
|
+
* them here is exactly what makes `Post.validate(a_db_row)` come back invalid.
|
|
23
|
+
*
|
|
24
|
+
* Shallow, like `encodeFields` it stands on: a relation's nested rows are not reached.
|
|
25
|
+
* A handler that hand-rolls its own envelope owns its own egress.
|
|
26
|
+
*/
|
|
27
|
+
import { checkValue, encodeFields } from '@fougere/schema';
|
|
28
|
+
import { ErrorCode, FougereError } from './middleware.js';
|
|
29
|
+
import { assertListOptions } from './orm.js';
|
|
30
|
+
// ─── 1 · Judge ──────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Refuse a value the shape does not accept, before it goes anywhere.
|
|
33
|
+
*
|
|
34
|
+
* Only the keys actually present are judged, so a patch stays a patch: an update
|
|
35
|
+
* naming one field says nothing about the others. A key with no field is not this
|
|
36
|
+
* judge's business — the receiver fails on its own (an unknown column), and a client
|
|
37
|
+
* input already met `validateFields`, which refuses strangers.
|
|
38
|
+
*/
|
|
39
|
+
export function judgeEgress(fields, value, entity, operation) {
|
|
40
|
+
if (typeof value !== 'object' || value === null)
|
|
41
|
+
return;
|
|
42
|
+
const errors = [];
|
|
43
|
+
for (const [key, item] of Object.entries(value)) {
|
|
44
|
+
const field = fields[key];
|
|
45
|
+
if (!field || item === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
const checked = checkValue(field, item);
|
|
48
|
+
if ('error' in checked)
|
|
49
|
+
errors.push(`${key}: ${checked.error}`);
|
|
50
|
+
}
|
|
51
|
+
if (errors.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
// The domain produced this, not a caller — so it is our bug, not a bad request,
|
|
54
|
+
// and no retry with different input can fix it.
|
|
55
|
+
throw new FougereError({
|
|
56
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
57
|
+
message: `Refused on the way out — ${errors.join(', ')}`,
|
|
58
|
+
entity,
|
|
59
|
+
operation,
|
|
60
|
+
details: errors,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// ─── 2 · Project ────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* An array's own non-index properties. `ListResult` IS an array — it carries
|
|
66
|
+
* `hasMore`/`endCursor`/`total` on itself rather than wrapping the rows — so
|
|
67
|
+
* mapping over it must not drop them.
|
|
68
|
+
*/
|
|
69
|
+
function arrayExtras(source) {
|
|
70
|
+
const extras = {};
|
|
71
|
+
for (const key of Object.keys(source)) {
|
|
72
|
+
if (!/^\d+$/.test(key))
|
|
73
|
+
extras[key] = source[key];
|
|
74
|
+
}
|
|
75
|
+
return extras;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Drop what this receiver may not see — a `writeOnly` field goes here — then hand the
|
|
79
|
+
* rest to `encodeFields`, which converts the values to their wire form.
|
|
80
|
+
*
|
|
81
|
+
* `closed` says the field set is the WHOLE of what this op emits, so anything else is
|
|
82
|
+
* dropped. That is what naming a view for an op means (`Crud(Post, { list: PostCard })`):
|
|
83
|
+
* the author states the audience, and a field they left out must not ride along. Open is
|
|
84
|
+
* the default and stays the rule for the entity itself — a presenter's computed field is
|
|
85
|
+
* an addition to the entity's output, not an intruder.
|
|
86
|
+
*/
|
|
87
|
+
export function projectEgress(fields, result, closed = false) {
|
|
88
|
+
if (result === null || result === undefined)
|
|
89
|
+
return result;
|
|
90
|
+
if (Array.isArray(result)) {
|
|
91
|
+
const rows = result.map((item) => projectEgress(fields, item, closed));
|
|
92
|
+
return Object.assign(rows, arrayExtras(result));
|
|
93
|
+
}
|
|
94
|
+
// A scalar (the boolean from `delete`, an id) crosses untouched.
|
|
95
|
+
if (typeof result !== 'object')
|
|
96
|
+
return result;
|
|
97
|
+
const record = result;
|
|
98
|
+
const scoped = closed
|
|
99
|
+
? Object.fromEntries(Object.keys(fields).filter((k) => k in record).map((k) => [k, record[k]]))
|
|
100
|
+
: record;
|
|
101
|
+
return encodeFields(fields, scoped);
|
|
102
|
+
}
|
|
103
|
+
export async function presentEgress(result, presenter, fieldNames, entity = 'unknown', operation = 'unknown', args = {}) {
|
|
104
|
+
if (!presenter || !fieldNames?.length || result === null || typeof result !== 'object')
|
|
105
|
+
return result;
|
|
106
|
+
const rows = Array.isArray(result) ? result : [result];
|
|
107
|
+
const values = new Map();
|
|
108
|
+
// One pass per FIELD over every row, not one pass per row over every field. A
|
|
109
|
+
// computed field that reads issues its query once for the page — which is the
|
|
110
|
+
// whole reason it is handed the page. And the reader arrives here as an argument,
|
|
111
|
+
// bound from the signature like a handler's, so a field may depend on who asks.
|
|
112
|
+
for (const name of fieldNames) {
|
|
113
|
+
const fn = presenter[name];
|
|
114
|
+
if (typeof fn !== 'function')
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const answered = await fn.call(presenter, rows, ...(args[name] ?? []));
|
|
118
|
+
if (!Array.isArray(answered) || answered.length !== rows.length) {
|
|
119
|
+
throw new Error(`expected ${rows.length} value(s) for ${rows.length} row(s), got ${Array.isArray(answered) ? answered.length : typeof answered}`);
|
|
120
|
+
}
|
|
121
|
+
values.set(name, answered);
|
|
122
|
+
}
|
|
123
|
+
catch (cause) {
|
|
124
|
+
// A throwing computed field would take the whole operation down with a raw
|
|
125
|
+
// stack. Name the field instead: the domain produced this, so it is our bug —
|
|
126
|
+
// same verdict `judgeEgress` renders.
|
|
127
|
+
throw new FougereError({
|
|
128
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
129
|
+
message: `Computed field '${name}' failed: ${cause?.message ?? cause}`,
|
|
130
|
+
entity,
|
|
131
|
+
operation,
|
|
132
|
+
cause,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const enriched = rows.map((item, index) => {
|
|
137
|
+
if (item === null || typeof item !== 'object')
|
|
138
|
+
return item;
|
|
139
|
+
const out = { ...item };
|
|
140
|
+
for (const [name, answered] of values)
|
|
141
|
+
out[name] = answered[index];
|
|
142
|
+
return out;
|
|
143
|
+
});
|
|
144
|
+
// `ListResult` IS an array — carry its own properties across, like projectEgress.
|
|
145
|
+
return Array.isArray(result) ? Object.assign(enriched, arrayExtras(result)) : enriched[0];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The storage way out: judge, then hand over. There is no projection step — storage is
|
|
149
|
+
* the receiver that sees everything, which is precisely why a `writeOnly` field can be
|
|
150
|
+
* persisted while never reaching a browser.
|
|
151
|
+
*
|
|
152
|
+
* `Object.create` keeps the original on the prototype chain, so reads, `output()` and
|
|
153
|
+
* whatever else an adapter carries still resolve, and a copy scoped later inherits the
|
|
154
|
+
* judgement instead of escaping it.
|
|
155
|
+
*/
|
|
156
|
+
export function guardStorage(orm, fields, entityName) {
|
|
157
|
+
const base = orm;
|
|
158
|
+
if (typeof base.create !== 'function' || typeof base.update !== 'function')
|
|
159
|
+
return orm;
|
|
160
|
+
const guarded = Object.create(orm);
|
|
161
|
+
// `async` on purpose: a refusal must REJECT the promise, not throw synchronously —
|
|
162
|
+
// a sync throw from an awaited call skips every `.catch` the caller wrote.
|
|
163
|
+
// Rest arguments, forwarded verbatim: an ORM must see the arity its caller used.
|
|
164
|
+
guarded.create = async function (...args) {
|
|
165
|
+
judgeEgress(fields, args[0], entityName, 'create');
|
|
166
|
+
return base.create.apply(this, args);
|
|
167
|
+
};
|
|
168
|
+
guarded.update = async function (...args) {
|
|
169
|
+
judgeEgress(fields, args[1], entityName, 'update');
|
|
170
|
+
return base.update.apply(this, args);
|
|
171
|
+
};
|
|
172
|
+
// A read is judged too, on its ARGUMENTS. The façade refuses an unknown key in a client's
|
|
173
|
+
// input; the same rule applied to the port's own options closes the mirror image of that
|
|
174
|
+
// hole: `list({ orderId })` was accepted, the criterion dropped, and a one-to-many relation
|
|
175
|
+
// quietly answered with the whole table. An option nobody reads must say so.
|
|
176
|
+
const reader = orm;
|
|
177
|
+
if (typeof reader.list === 'function') {
|
|
178
|
+
guarded.list = async function (...args) {
|
|
179
|
+
assertListOptions(args[0], entityName);
|
|
180
|
+
return reader.list.apply(this, args);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return guarded;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=egress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.js","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"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"}
|
package/dist/emit.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an emitter injects. Resolved by type, like `EntityOrm<Post>` and `Facade<T>`.
|
|
3
|
+
*
|
|
4
|
+
* The returned promise settles when the fact has been **dispatched**, never when it has
|
|
5
|
+
* been handled: a subscriber's failure is its own. The `EventBus` this replaces did
|
|
6
|
+
* `await Promise.all(handlers)` and handed their rejections back, which made a publication
|
|
7
|
+
* hostage to its own indexer.
|
|
8
|
+
*/
|
|
9
|
+
export type Emit<T> = (fact: T) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* What a subscriber accepts — and what it PROMISES about itself.
|
|
12
|
+
*
|
|
13
|
+
* Not merely "I subscribe". Push is the strict mode and pull is a special case of it: an
|
|
14
|
+
* op written for push is correct when called directly, the reverse is false. So this
|
|
15
|
+
* wrapper commits the operation to three things its author must honour, because no type
|
|
16
|
+
* can check them:
|
|
17
|
+
*
|
|
18
|
+
* 1. **replayable** — delivery is at-least-once the day a broker sits underneath;
|
|
19
|
+
* 2. **no reader for its return** — nobody receives what it hands back;
|
|
20
|
+
* 3. **owner of its failures** — they reach a log, never the emitter.
|
|
21
|
+
*
|
|
22
|
+
* It is the identity at runtime. The scan reads the written name, which is the whole
|
|
23
|
+
* mechanism: `binding.ts` sees `Fact<X>` and binds the parameter as a fact instead of
|
|
24
|
+
* letting it fall through to "everything else — body".
|
|
25
|
+
*/
|
|
26
|
+
export type Fact<T> = T;
|
|
27
|
+
/**
|
|
28
|
+
* The container key of an emission — THE one place that spells the format.
|
|
29
|
+
*
|
|
30
|
+
* Its dual is right below, because the boot reads the fact back out of a handler's `deps`
|
|
31
|
+
* to know which emissions to register. A key and the way to undo it belong together: the
|
|
32
|
+
* pair that is split is the pair that drifts.
|
|
33
|
+
*/
|
|
34
|
+
export declare function emitKeyOf(fact: string): string;
|
|
35
|
+
/** The fact behind an emission key, or `undefined` when the key is not one. */
|
|
36
|
+
export declare function factOfEmitKey(key: string): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* What a set of handlers ANNOUNCES — `Emit<T>` read back out of their dependencies.
|
|
39
|
+
*
|
|
40
|
+
* Two readers, which is exactly why it is a function and not a line: the boot registers one
|
|
41
|
+
* emission value per name, and the identity card publishes the same set. Spelled twice they
|
|
42
|
+
* would drift the day an emission stops being a constructor dependency.
|
|
43
|
+
*
|
|
44
|
+
* The dual — what a handler ACCEPTS — is not here, because it is not read the same way: a
|
|
45
|
+
* `Fact<T>` parameter is a fact about one operation, and `computeBindingPlan` has already
|
|
46
|
+
* written it into the plan (`app.listensTo()`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function factsAnnouncedBy(handlers: ReadonlyArray<{
|
|
49
|
+
deps: readonly string[];
|
|
50
|
+
}>): string[];
|
|
51
|
+
//# sourceMappingURL=emit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../src/emit.ts"],"names":[],"mappings":"AAuBA;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAExB;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAID,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAO/F"}
|
package/dist/emit.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Announcing a fact — the half of the topology `remotes:` never covered.
|
|
3
|
+
*
|
|
4
|
+
* Every other call in Fougere names **one** recipient: `remotes` names one address per
|
|
5
|
+
* frond, `Facade<T>` resolves one door. `Emit<T>` names a **subject** instead, and the
|
|
6
|
+
* number of readers is not the emitter's business.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* constructor(private published: Emit<PostPublished>) {}
|
|
10
|
+
* await this.published({ id, title }); // I say it happened. I do not know who cares.
|
|
11
|
+
*
|
|
12
|
+
* async reindex(fact: Fact<PostPublished>) {} // any handler, any frond. Nothing to register.
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* **It is a resolver, not a channel.** A bus moves messages — it has a queue, an envelope
|
|
16
|
+
* format, a delivery semantic. This has none of that: it answers *who*, then hands over to
|
|
17
|
+
* the call path that already exists. It is `resolve` returning N things instead of one.
|
|
18
|
+
* That is also why temporal decoupling stays out of reach: a resolver holds nothing. The
|
|
19
|
+
* day at-least-once is wanted, a real channel goes UNDER this, in the transport — the
|
|
20
|
+
* dispatch line becomes a publish and a consumer calls the same door on the far side.
|
|
21
|
+
*/
|
|
22
|
+
import { toRegistrationName } from './contract.js';
|
|
23
|
+
/**
|
|
24
|
+
* The container key of an emission — THE one place that spells the format.
|
|
25
|
+
*
|
|
26
|
+
* Its dual is right below, because the boot reads the fact back out of a handler's `deps`
|
|
27
|
+
* to know which emissions to register. A key and the way to undo it belong together: the
|
|
28
|
+
* pair that is split is the pair that drifts.
|
|
29
|
+
*/
|
|
30
|
+
export function emitKeyOf(fact) {
|
|
31
|
+
return `${toRegistrationName(fact)}Emit`;
|
|
32
|
+
}
|
|
33
|
+
const SUFFIX = 'Emit';
|
|
34
|
+
/** The fact behind an emission key, or `undefined` when the key is not one. */
|
|
35
|
+
export function factOfEmitKey(key) {
|
|
36
|
+
return key.length > SUFFIX.length && key.endsWith(SUFFIX)
|
|
37
|
+
? key.slice(0, -SUFFIX.length)
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What a set of handlers ANNOUNCES — `Emit<T>` read back out of their dependencies.
|
|
42
|
+
*
|
|
43
|
+
* Two readers, which is exactly why it is a function and not a line: the boot registers one
|
|
44
|
+
* emission value per name, and the identity card publishes the same set. Spelled twice they
|
|
45
|
+
* would drift the day an emission stops being a constructor dependency.
|
|
46
|
+
*
|
|
47
|
+
* The dual — what a handler ACCEPTS — is not here, because it is not read the same way: a
|
|
48
|
+
* `Fact<T>` parameter is a fact about one operation, and `computeBindingPlan` has already
|
|
49
|
+
* written it into the plan (`app.listensTo()`).
|
|
50
|
+
*/
|
|
51
|
+
export function factsAnnouncedBy(handlers) {
|
|
52
|
+
return [...new Set(handlers
|
|
53
|
+
.flatMap((handler) => handler.deps)
|
|
54
|
+
.map(factOfEmitKey)
|
|
55
|
+
.filter((fact) => fact !== undefined))];
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=emit.js.map
|
package/dist/emit.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit.js","sourceRoot":"","sources":["../src/emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AA8BnD;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC;AAC3C,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QACvD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QAC9B,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAoD;IACnF,OAAO,CAAC,GAAG,IAAI,GAAG,CAChB,QAAQ;aACL,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,GAAG,CAAC,aAAa,CAAC;aAClB,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CACxD,CAAC,CAAC;AACL,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP error mapping — used by HTTP bridges (REST, Next, Inertia).
|
|
3
|
+
*
|
|
4
|
+
* Not imported by GraphQL, CLI, or event bus bridges.
|
|
5
|
+
*/
|
|
6
|
+
import { FougereError, ErrorCode } from './middleware.js';
|
|
7
|
+
/** Map a FougereError code to its HTTP status. */
|
|
8
|
+
export declare function httpStatusFor(code: ErrorCode): number;
|
|
9
|
+
/**
|
|
10
|
+
* Serialize an application error for an untrusted caller.
|
|
11
|
+
*
|
|
12
|
+
* Every code but INTERNAL_ERROR was written for the caller and travels whole. An
|
|
13
|
+
* INTERNAL_ERROR is replaced by a constant — and logged here, with its cause, so the
|
|
14
|
+
* sentence exists exactly once: on the server.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toPublicError(err: FougereError): ReturnType<FougereError['toJSON']>;
|
|
17
|
+
/** Map any thrown error to `{ status, body }` for HTTP bridges. */
|
|
18
|
+
export declare function toHttpError(err: unknown): {
|
|
19
|
+
status: number;
|
|
20
|
+
body: ReturnType<FougereError['toJSON']>;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAkC1D,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAErD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,GAAG,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAUnF;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;CAAE,CAYtG"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP error mapping — used by HTTP bridges (REST, Next, Inertia).
|
|
3
|
+
*
|
|
4
|
+
* Not imported by GraphQL, CLI, or event bus bridges.
|
|
5
|
+
*/
|
|
6
|
+
import { FougereError, ErrorCode } from './middleware.js';
|
|
7
|
+
import { Logger } from './builtins/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* An INTERNAL_ERROR is the one class of error whose message never leaves: it was not
|
|
10
|
+
* written for a caller and may quote a path, a query or a row. Masking it is right, and
|
|
11
|
+
* masking it *silently* is how a bug becomes unobservable — the operator loses the same
|
|
12
|
+
* sentence the attacker does. So the mask and the record live in one function: whoever
|
|
13
|
+
* calls `toPublicError` cannot forget the half that keeps the error findable.
|
|
14
|
+
*/
|
|
15
|
+
const log = new Logger('error');
|
|
16
|
+
const HTTP_STATUS = {
|
|
17
|
+
[ErrorCode.VALIDATION_FAILED]: 400,
|
|
18
|
+
[ErrorCode.BAD_REQUEST]: 400,
|
|
19
|
+
[ErrorCode.UNAUTHORIZED]: 401,
|
|
20
|
+
[ErrorCode.FORBIDDEN]: 403,
|
|
21
|
+
[ErrorCode.NOT_FOUND]: 404,
|
|
22
|
+
[ErrorCode.GONE]: 410,
|
|
23
|
+
[ErrorCode.CONFLICT]: 409,
|
|
24
|
+
[ErrorCode.LOCKED]: 423,
|
|
25
|
+
[ErrorCode.METHOD_NOT_ALLOWED]: 405,
|
|
26
|
+
[ErrorCode.PRECONDITION_FAILED]: 412,
|
|
27
|
+
[ErrorCode.PAYLOAD_TOO_LARGE]: 413,
|
|
28
|
+
[ErrorCode.UNPROCESSABLE_ENTITY]: 422,
|
|
29
|
+
[ErrorCode.TOO_MANY_REQUESTS]: 429,
|
|
30
|
+
[ErrorCode.REQUEST_TIMEOUT]: 408,
|
|
31
|
+
[ErrorCode.INTERNAL_ERROR]: 500,
|
|
32
|
+
[ErrorCode.NOT_IMPLEMENTED]: 501,
|
|
33
|
+
[ErrorCode.BAD_GATEWAY]: 502,
|
|
34
|
+
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
|
|
35
|
+
[ErrorCode.GATEWAY_TIMEOUT]: 504,
|
|
36
|
+
};
|
|
37
|
+
/** Map a FougereError code to its HTTP status. */
|
|
38
|
+
export function httpStatusFor(code) {
|
|
39
|
+
return HTTP_STATUS[code] ?? 500;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Serialize an application error for an untrusted caller.
|
|
43
|
+
*
|
|
44
|
+
* Every code but INTERNAL_ERROR was written for the caller and travels whole. An
|
|
45
|
+
* INTERNAL_ERROR is replaced by a constant — and logged here, with its cause, so the
|
|
46
|
+
* sentence exists exactly once: on the server.
|
|
47
|
+
*/
|
|
48
|
+
export function toPublicError(err) {
|
|
49
|
+
if (err.code !== ErrorCode.INTERNAL_ERROR)
|
|
50
|
+
return err.toJSON();
|
|
51
|
+
const where = [err.entity, err.operation].filter(Boolean).join('.');
|
|
52
|
+
log.error(`${where || 'internal'}: ${err.message}`, err.cause ?? err);
|
|
53
|
+
return {
|
|
54
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
55
|
+
message: 'Internal error',
|
|
56
|
+
...(err.entity && { entity: err.entity }),
|
|
57
|
+
...(err.operation && { operation: err.operation }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Map any thrown error to `{ status, body }` for HTTP bridges. */
|
|
61
|
+
export function toHttpError(err) {
|
|
62
|
+
if (err instanceof FougereError) {
|
|
63
|
+
return { status: httpStatusFor(err.code), body: toPublicError(err) };
|
|
64
|
+
}
|
|
65
|
+
// A throw that never became a FougereError is masked by the same rule, so it goes
|
|
66
|
+
// through the same door rather than growing a second, quieter one here.
|
|
67
|
+
const framed = new FougereError({
|
|
68
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
69
|
+
message: err?.message ?? 'Internal error',
|
|
70
|
+
cause: err,
|
|
71
|
+
});
|
|
72
|
+
return { status: 500, body: toPublicError(framed) };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SchemaLike } from '@fougere/schema';
|
|
2
|
+
import type { BindingPlan } from './binding.js';
|
|
3
|
+
/**
|
|
4
|
+
* Per-operation override. Config takes precedence over conventions (isReadOp, facade lookup).
|
|
5
|
+
*
|
|
6
|
+
* Two families of keys, and they act at different depths:
|
|
7
|
+
*
|
|
8
|
+
* - **Surface** (`kind`, `handler`, `method`, `policy`) — how the op is EXPOSED. Read by
|
|
9
|
+
* the transport adapters: `kind` decides query vs mutation and GET vs POST.
|
|
10
|
+
* - **Contract** (`input`, `output`, `binding`) — what the op IS. Read by the façade,
|
|
11
|
+
* and this is the third producer of an {@link OperationContract}: a prefab DECLARES its
|
|
12
|
+
* ops (`Crud.__ops`), the scan DERIVES them from source, config STATES them outright.
|
|
13
|
+
* The façade cannot tell the three apart.
|
|
14
|
+
*
|
|
15
|
+
* Stating a contract here makes the scan optional rather than load-bearing. Two cases it
|
|
16
|
+
* answers that nothing else does:
|
|
17
|
+
* - a method inherited from an **installed** base class — the scan resolves nothing there
|
|
18
|
+
* (workspace-only heritage) and says nothing, so the op silently misses the façade;
|
|
19
|
+
* - a signature the AST parser cannot read (a type alias, `type CurrentUser = User | null`).
|
|
20
|
+
*
|
|
21
|
+
* Config wins over both other producers — it is the most explicit statement, made by
|
|
22
|
+
* whoever assembles the app. Precedence: CLI > frond config > fougere config > scan >
|
|
23
|
+
* conventions.
|
|
24
|
+
*/
|
|
25
|
+
export interface OperationOverride {
|
|
26
|
+
/** Force operation kind (overrides isReadOp-based default). */
|
|
27
|
+
kind?: 'query' | 'command';
|
|
28
|
+
/**
|
|
29
|
+
* Handler class to delegate to (overrides the default `{Entity}Handler` lookup).
|
|
30
|
+
* Class name is used to resolve from DI. E.g. `ArchiveHandler` → `app.resolve('ArchiveHandler')`.
|
|
31
|
+
*/
|
|
32
|
+
handler?: abstract new (...args: any[]) => any;
|
|
33
|
+
/** Method name on `handler` (defaults to the operation name). */
|
|
34
|
+
method?: string;
|
|
35
|
+
/**
|
|
36
|
+
* CASL ability check (e.g. 'archive Post'). **Declared, not implemented** — no
|
|
37
|
+
* reader interprets it today, in any adapter. Kept as the named slot for the
|
|
38
|
+
* question rather than silently dropped, but do not rely on it: a config that
|
|
39
|
+
* states a policy is not enforced anywhere.
|
|
40
|
+
*
|
|
41
|
+
* Wiring it means adopting a rules engine, which is precisely what the design
|
|
42
|
+
* turns down: an identity-dependent right is judged INSIDE the operation
|
|
43
|
+
* (`publish` reads `post.authorId !== user.id`), and a door-dependent one is a
|
|
44
|
+
* surface. Neither needs a second vocabulary evaluated by a second engine.
|
|
45
|
+
*/
|
|
46
|
+
policy?: string;
|
|
47
|
+
/** What judges the input. The view carries its own mode (`partial()` → patch). */
|
|
48
|
+
input?: SchemaLike;
|
|
49
|
+
/**
|
|
50
|
+
* Where each argument is read from — states what the scan would otherwise derive
|
|
51
|
+
* from the method signature. An empty array is meaningful: "this op takes nothing".
|
|
52
|
+
*/
|
|
53
|
+
binding?: BindingPlan;
|
|
54
|
+
}
|
|
55
|
+
export interface FrondConfig {
|
|
56
|
+
/** Class names exposed as the frond's public contract (all surfaces). */
|
|
57
|
+
expose?: string[];
|
|
58
|
+
/** Per-surface entity lists. Overrides default deduction for each named surface. */
|
|
59
|
+
surfaces?: Record<string, string[]>;
|
|
60
|
+
/** Interface → implementation bindings for DI (e.g. { Database: 'SqliteDatabase' }). */
|
|
61
|
+
bindings?: Record<string, string>;
|
|
62
|
+
/** Per-operation overrides. Key = operation name (method name on a handler). */
|
|
63
|
+
operations?: Record<string, OperationOverride>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Define a frond configuration (typed identity helper).
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* // fronds/auth/frond.config.ts
|
|
70
|
+
* import { defineFrond } from '@fougere/core'
|
|
71
|
+
* export default defineFrond({ expose: ['User', 'AuthHandler'] })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function defineFrond(config: FrondConfig): FrondConfig;
|
|
75
|
+
/**
|
|
76
|
+
* Load a frond's `frond.config.{ts,js,mjs}` from the given frond directory.
|
|
77
|
+
* Returns `undefined` if no config file is found.
|
|
78
|
+
*/
|
|
79
|
+
export declare function loadFrondConfig(frondPath: string): Promise<FrondConfig | undefined>;
|
|
80
|
+
//# sourceMappingURL=frond-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frond-config.d.ts","sourceRoot":"","sources":["../src/frond-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;IAC/C,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAIhB,kFAAkF;IAClF,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CAOvB;AAED,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAChD;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE5D;AAMD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CASzF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
// ── Helper ───────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Define a frond configuration (typed identity helper).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // fronds/auth/frond.config.ts
|
|
9
|
+
* import { defineFrond } from '@fougere/core'
|
|
10
|
+
* export default defineFrond({ expose: ['User', 'AuthHandler'] })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function defineFrond(config) {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
// ── Loading ──────────────────────────────────────
|
|
17
|
+
const FROND_CONFIG_FILES = ['frond.config.ts', 'frond.config.js', 'frond.config.mjs'];
|
|
18
|
+
/**
|
|
19
|
+
* Load a frond's `frond.config.{ts,js,mjs}` from the given frond directory.
|
|
20
|
+
* Returns `undefined` if no config file is found.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadFrondConfig(frondPath) {
|
|
23
|
+
for (const file of FROND_CONFIG_FILES) {
|
|
24
|
+
const path = resolve(frondPath, file);
|
|
25
|
+
if (existsSync(path)) {
|
|
26
|
+
const mod = await import(path);
|
|
27
|
+
return mod.default ?? mod;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=frond-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frond-config.js","sourceRoot":"","sources":["../src/frond-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AA+ErC,oDAAoD;AAEpD;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,MAAmB;IAC7C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,oDAAoD;AAEpD,MAAM,kBAAkB,GAAG,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,kBAAkB,CAAC,CAAC;AAEtF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;YAC/B,OAAO,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity dependency graph — analyzes ref() links between entities
|
|
3
|
+
* to suggest domain boundaries for splitting fronds.
|
|
4
|
+
*/
|
|
5
|
+
import type { FrondDescriptor } from './types.js';
|
|
6
|
+
export interface EntityNode {
|
|
7
|
+
name: string;
|
|
8
|
+
frond: string;
|
|
9
|
+
refs: string[];
|
|
10
|
+
referencedBy: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface DomainCluster {
|
|
13
|
+
name: string;
|
|
14
|
+
entities: string[];
|
|
15
|
+
crossRefs: {
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
targetCluster: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/** Build the full entity dependency graph from scanned fronds. */
|
|
22
|
+
export declare function buildGraph(fronds: FrondDescriptor[]): Map<string, EntityNode>;
|
|
23
|
+
/**
|
|
24
|
+
* Cluster entities into domains using connected component analysis.
|
|
25
|
+
* Entities linked by refs (in either direction) belong to the same cluster.
|
|
26
|
+
*/
|
|
27
|
+
export declare function clusterEntities(nodes: Map<string, EntityNode>): DomainCluster[];
|
|
28
|
+
/**
|
|
29
|
+
* If there's only one big cluster, try to split it using Kernighan-Lin-style
|
|
30
|
+
* heuristic: find the entity with the fewest cross-links and use it as a cut point.
|
|
31
|
+
*/
|
|
32
|
+
export declare function suggestSplit(nodes: Map<string, EntityNode>, minClusterSize?: number): DomainCluster[];
|
|
33
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAClE;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CA6B7E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,aAAa,EAAE,CAwC/E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAC9B,cAAc,SAAI,GACjB,aAAa,EAAE,CAmDjB"}
|