@fougere/core 0.11.0-alpha.0 → 0.12.1-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/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +18 -0
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts +10 -6
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js +19 -4
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts +0 -6
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/FrondDeclaration.d.ts +2 -9
- package/dist/FrondDeclaration.d.ts.map +1 -1
- package/dist/FrondDeclaration.js +0 -1
- package/dist/FrondDeclaration.js.map +1 -1
- package/dist/FrondsStated.d.ts +54 -0
- package/dist/FrondsStated.d.ts.map +1 -0
- package/dist/FrondsStated.js +26 -0
- package/dist/FrondsStated.js.map +1 -0
- package/dist/StatedFrond.d.ts +21 -0
- package/dist/StatedFrond.d.ts.map +1 -0
- package/dist/StatedFrond.js +34 -0
- package/dist/StatedFrond.js.map +1 -0
- package/dist/StatedModules.d.ts +19 -0
- package/dist/StatedModules.d.ts.map +1 -0
- package/dist/StatedModules.js +37 -0
- package/dist/StatedModules.js.map +1 -0
- package/dist/boot/CreateAppOptions.d.ts +19 -2
- package/dist/boot/CreateAppOptions.d.ts.map +1 -1
- package/dist/boot/Emissions.d.ts +9 -0
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js +11 -5
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +342 -307
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/declared.d.ts.map +1 -1
- package/dist/boot/declared.js +3 -0
- package/dist/boot/declared.js.map +1 -1
- package/dist/boot/frame.d.ts +0 -4
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js +31 -14
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/hosted.d.ts.map +1 -1
- package/dist/boot/hosted.js +4 -3
- package/dist/boot/hosted.js.map +1 -1
- package/dist/boot/install.d.ts +9 -0
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +275 -185
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/nesting.d.ts +33 -0
- package/dist/boot/nesting.d.ts.map +1 -0
- package/dist/boot/nesting.js +164 -0
- package/dist/boot/nesting.js.map +1 -0
- package/dist/boot/ownership.d.ts +0 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js +66 -51
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/ports.js +30 -25
- package/dist/boot/ports.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +39 -30
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.d.ts.map +1 -1
- package/dist/boot/seed.js +27 -20
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js +71 -56
- package/dist/boot/together.js.map +1 -1
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/descriptor/FrondDescriptor.d.ts +7 -0
- package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
- package/dist/descriptor/MiddlewareEntry.d.ts +6 -5
- package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
- package/dist/descriptor/ProviderEntry.d.ts +7 -0
- package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +23 -20
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/OutputView.d.ts.map +1 -1
- package/dist/dispatch/OutputView.js +7 -0
- package/dist/dispatch/OutputView.js.map +1 -1
- package/dist/dispatch/PresenterExecutor.d.ts +6 -0
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
- package/dist/dispatch/PresenterExecutor.js +37 -22
- package/dist/dispatch/PresenterExecutor.js.map +1 -1
- package/dist/dispatch/Received.d.ts +3 -0
- package/dist/dispatch/Received.d.ts.map +1 -0
- package/dist/dispatch/Received.js +2 -0
- package/dist/dispatch/Received.js.map +1 -0
- package/dist/dispatch/Release.js +53 -47
- package/dist/dispatch/Release.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts +21 -0
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +84 -66
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/dispatch/decoded.d.ts +19 -0
- package/dist/dispatch/decoded.d.ts.map +1 -0
- package/dist/dispatch/decoded.js +36 -0
- package/dist/dispatch/decoded.js.map +1 -0
- package/dist/entry/facade.d.ts +9 -2
- package/dist/entry/facade.d.ts.map +1 -1
- package/dist/entry/facade.js +16 -7
- package/dist/entry/facade.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -1
- package/dist/node.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js +3 -2
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/CrudOps.d.ts +2 -2
- package/dist/prefab/CrudOps.d.ts.map +1 -1
- package/dist/prefab/MirrorConstructor.js +1 -1
- package/dist/prefab/MirrorConstructor.js.map +1 -1
- package/dist/prefab/Refreshed.d.ts +7 -2
- package/dist/prefab/Refreshed.d.ts.map +1 -1
- package/dist/storage/Storage.js +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +35 -31
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +14 -0
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts +10 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/AppNext.d.ts +2 -1
- package/dist/wire/AppNext.d.ts.map +1 -1
- package/dist/wire/Facade.d.ts +14 -2
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.js +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/Page.d.ts +32 -0
- package/dist/wire/Page.d.ts.map +1 -0
- package/dist/wire/Page.js +32 -0
- package/dist/wire/Page.js.map +1 -0
- package/dist/wire/Signature.d.ts +7 -0
- package/dist/wire/Signature.d.ts.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js +29 -57
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js +22 -19
- package/dist/wire/drift.js.map +1 -1
- package/package.json +4 -4
- package/src/EffectiveOperationModel.ts +19 -0
- package/src/FougereConfig.ts +24 -10
- package/src/FrondConfig.ts +0 -6
- package/src/FrondDeclaration.ts +2 -7
- package/src/FrondsStated.ts +72 -0
- package/src/StatedFrond.ts +51 -0
- package/src/StatedModules.ts +58 -0
- package/src/boot/CreateAppOptions.ts +19 -2
- package/src/boot/Emissions.ts +16 -5
- package/src/boot/bootstrap.ts +478 -359
- package/src/boot/declared.ts +3 -0
- package/src/boot/frame.ts +42 -11
- package/src/boot/hosted.ts +4 -3
- package/src/boot/install.ts +392 -212
- package/src/boot/nesting.ts +196 -0
- package/src/boot/ownership.ts +79 -51
- package/src/boot/ports.ts +45 -31
- package/src/boot/remote.ts +57 -32
- package/src/boot/seed.ts +36 -27
- package/src/boot/together.ts +98 -63
- package/src/contract.ts +2 -0
- package/src/descriptor/FrondDescriptor.ts +7 -0
- package/src/descriptor/MiddlewareEntry.ts +6 -5
- package/src/descriptor/ProviderEntry.ts +7 -0
- package/src/dispatch/HandlerFacade.ts +27 -17
- package/src/dispatch/OutputView.ts +7 -0
- package/src/dispatch/PresenterExecutor.ts +40 -22
- package/src/dispatch/Received.ts +2 -0
- package/src/dispatch/Release.ts +68 -44
- package/src/dispatch/StorageGuard.ts +90 -69
- package/src/dispatch/decoded.ts +37 -0
- package/src/entry/facade.ts +21 -9
- package/src/index.ts +4 -0
- package/src/node.ts +2 -1
- package/src/prefab/CrudConstructor.ts +3 -2
- package/src/prefab/CrudOps.ts +2 -2
- package/src/prefab/MirrorConstructor.ts +1 -1
- package/src/prefab/Refreshed.ts +7 -2
- package/src/storage/Storage.ts +2 -2
- package/src/storage/Store.ts +45 -27
- package/src/verify.ts +15 -0
- package/src/wire/AppMiddleware.ts +10 -1
- package/src/wire/AppNext.ts +2 -1
- package/src/wire/Facade.ts +14 -2
- package/src/wire/FougereError.ts +1 -1
- package/src/wire/Invocation.ts +2 -1
- package/src/wire/OperationContract.ts +1 -1
- package/src/wire/Page.ts +50 -0
- package/src/wire/Signature.ts +7 -0
- package/src/wire/binding.ts +26 -58
- package/src/wire/drift.ts +48 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FieldSet, FieldValueValidator, InputRefusal, type Fields } from '@fougere/schema';
|
|
1
|
+
import { FieldSet, FieldValueValidator, InputRefusal, type Fields, type Verdict } from '@fougere/schema';
|
|
2
2
|
import { COMPARISONS, comparisonOf, unknownIn } from '../storage/Comparison.js';
|
|
3
3
|
import { assertListOptions } from '../storage/Storage.js';
|
|
4
4
|
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
@@ -34,22 +34,37 @@ export class StorageGuard {
|
|
|
34
34
|
const writer = storage as unknown as Writer;
|
|
35
35
|
if (typeof writer.create !== 'function' || typeof writer.update !== 'function') return storage;
|
|
36
36
|
|
|
37
|
-
const validation = this;
|
|
38
37
|
const guarded = Object.create(storage) as T & Writer;
|
|
39
38
|
|
|
39
|
+
this.guardDelete(guarded, writer);
|
|
40
|
+
this.guardWrites(guarded, writer);
|
|
41
|
+
this.guardList(guarded, writer);
|
|
42
|
+
|
|
43
|
+
return guarded;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What names this row goes first, and this row last: an interruption then leaves fewer
|
|
48
|
+
* children rather than an orphan. A key holds the rest, at the rows, in one statement.
|
|
49
|
+
*/
|
|
50
|
+
private guardDelete(guarded: Writer, writer: Writer): void {
|
|
40
51
|
const remove = writer.delete;
|
|
41
|
-
if (typeof remove
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (typeof remove !== 'function' || !this.releasing) return;
|
|
53
|
+
|
|
54
|
+
const validation = this;
|
|
55
|
+
guarded.delete = async function (id) {
|
|
56
|
+
let gone = false;
|
|
57
|
+
await release(validation.entity, id, validation.releasing!, [], async () => {
|
|
58
|
+
gone = await remove.call(this, id);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return gone;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Every gesture that puts a row down: judged first, and handed on the value it parsed. */
|
|
66
|
+
private guardWrites(guarded: Writer, writer: Writer): void {
|
|
67
|
+
const validation = this;
|
|
53
68
|
|
|
54
69
|
guarded.create = async function (...args) {
|
|
55
70
|
args[0] = validation.validated(args[0], 'create');
|
|
@@ -74,29 +89,29 @@ export class StorageGuard {
|
|
|
74
89
|
|
|
75
90
|
const upsertAll = writer.upsertAll;
|
|
76
91
|
if (typeof upsertAll === 'function') {
|
|
77
|
-
// Every row before the first write
|
|
78
|
-
//
|
|
92
|
+
// Every row before the first write, and the keys of the whole page in one read per
|
|
93
|
+
// relation: a page refused on its fourth row has already written three.
|
|
79
94
|
guarded.upsertAll = async function (...args) {
|
|
80
95
|
args[0] = args[0].map((row, index) => validation.validated(row, 'upsertAll', index));
|
|
81
|
-
// The keys of the whole page in one read per relation, for the reason above: a page
|
|
82
|
-
// refused on its fourth row has already written three.
|
|
83
96
|
await validation.targetsOf(args[0], 'upsertAll');
|
|
84
97
|
return upsertAll.apply(this, args);
|
|
85
98
|
};
|
|
86
99
|
}
|
|
100
|
+
}
|
|
87
101
|
|
|
102
|
+
/** A criterion is read where the entity declares it, so a page is asked for what it can answer. */
|
|
103
|
+
private guardList(guarded: Writer, writer: Writer): void {
|
|
88
104
|
const list = writer.list;
|
|
89
|
-
if (typeof list
|
|
90
|
-
guarded.list = async function (...args: unknown[]) {
|
|
91
|
-
const options = args[0] as { where?: Record<string, unknown> } | undefined;
|
|
92
|
-
assertListOptions(options, validation.entity, Object.keys(validation.fields));
|
|
93
|
-
if (options?.where) args[0] = { ...options, where: validation.criteria(options.where) };
|
|
105
|
+
if (typeof list !== 'function') return;
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
const validation = this;
|
|
108
|
+
guarded.list = async function (...args: unknown[]) {
|
|
109
|
+
const options = args[0] as { where?: Record<string, unknown> } | undefined;
|
|
110
|
+
assertListOptions(options, validation.entity, Object.keys(validation.fields));
|
|
111
|
+
if (options?.where) args[0] = { ...options, where: validation.criteria(options.where) };
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
return list.apply(this, args);
|
|
114
|
+
};
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
/**
|
|
@@ -123,34 +138,12 @@ export class StorageGuard {
|
|
|
123
138
|
const parsed: Record<string, unknown> = {};
|
|
124
139
|
|
|
125
140
|
for (const [key, asked] of Object.entries(where)) {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
// A comparison names its own vocabulary, and a typo in it would otherwise be a
|
|
132
|
-
// criterion that filters nothing — the silent truncation this facade exists to stop.
|
|
133
|
-
const comparison = comparisonOf(field, asked);
|
|
134
|
-
if (comparison) {
|
|
135
|
-
const unknown = unknownIn(comparison);
|
|
136
|
-
if (unknown.length) {
|
|
137
|
-
errors.push(`${key}: unknown comparison ${unknown.join(', ')} — one of ${COMPARISONS.join(', ')}`);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
parsed[key] = comparison;
|
|
141
|
+
const read = this.criterion(key, asked);
|
|
142
|
+
if ('message' in read) errors.push(`${key}: ${read.message}`);
|
|
143
|
+
else {
|
|
144
|
+
parsed[key] = read.value;
|
|
141
145
|
this.beyondTheView(key);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const values = Array.isArray(asked) ? asked : [asked];
|
|
146
|
-
const each = values.map((value) => this.value(field, value));
|
|
147
|
-
const refused = each.find((one) => typeof one === 'object' && one !== null && 'error' in one);
|
|
148
|
-
if (refused) {
|
|
149
|
-
errors.push(`${key}: ${(refused as { error: string }).error}`);
|
|
150
|
-
continue;
|
|
151
146
|
}
|
|
152
|
-
parsed[key] = Array.isArray(asked) ? each.map(unwrap) : unwrap(each[0]);
|
|
153
|
-
this.beyondTheView(key);
|
|
154
147
|
}
|
|
155
148
|
|
|
156
149
|
if (errors.length > 0) {
|
|
@@ -166,6 +159,33 @@ export class StorageGuard {
|
|
|
166
159
|
return parsed;
|
|
167
160
|
}
|
|
168
161
|
|
|
162
|
+
/**
|
|
163
|
+
* One criterion, told from a value by the FIELD and never by its own shape.
|
|
164
|
+
*
|
|
165
|
+
* A comparison names its own vocabulary, and a typo in it would otherwise be a criterion that
|
|
166
|
+
* filters nothing — the silent truncation this facade exists to stop.
|
|
167
|
+
*/
|
|
168
|
+
private criterion(key: string, asked: unknown): Verdict {
|
|
169
|
+
const field = this.fields[key];
|
|
170
|
+
if (!field) return { message: InputRefusal.unknownField };
|
|
171
|
+
|
|
172
|
+
const comparison = comparisonOf(field, asked);
|
|
173
|
+
if (comparison) {
|
|
174
|
+
const unknown = unknownIn(comparison);
|
|
175
|
+
|
|
176
|
+
return unknown.length
|
|
177
|
+
? { message: `unknown comparison ${unknown.join(', ')} — one of ${COMPARISONS.join(', ')}` }
|
|
178
|
+
: { value: comparison };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const values = Array.isArray(asked) ? asked : [asked];
|
|
182
|
+
const each = values.map((value) => this.value(field, value));
|
|
183
|
+
const refused = each.find((one) => 'message' in one);
|
|
184
|
+
if (refused) return refused;
|
|
185
|
+
|
|
186
|
+
return { value: Array.isArray(asked) ? each.map(unwrap) : unwrap(each[0]) };
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
/**
|
|
170
190
|
* A filter on a field this facade does not hand back.
|
|
171
191
|
*
|
|
@@ -186,7 +206,19 @@ export class StorageGuard {
|
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
/** One value against one field — validated, then decoded the way the wire hands it. */
|
|
189
|
-
|
|
209
|
+
/**
|
|
210
|
+
* One key a handler wrote. A key the entity does not declare has no column to land in and no
|
|
211
|
+
* judge to pass: on the client facade it is a typo, and on this one a mapping that went stale.
|
|
212
|
+
*/
|
|
213
|
+
private written(key: string, item: unknown): Verdict {
|
|
214
|
+
const field = this.fields[key];
|
|
215
|
+
if (!field) return { message: InputRefusal.unknownField };
|
|
216
|
+
if (item === undefined) return { value: item };
|
|
217
|
+
|
|
218
|
+
return FieldValueValidator.of(field).parse(item);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private value(field: Fields[string], asked: unknown): Verdict {
|
|
190
222
|
if (asked === null || asked === undefined) return { value: asked };
|
|
191
223
|
return FieldValueValidator.of(field).parse(asked);
|
|
192
224
|
}
|
|
@@ -199,20 +231,9 @@ export class StorageGuard {
|
|
|
199
231
|
const parsed: Record<string, unknown> = {};
|
|
200
232
|
|
|
201
233
|
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!field) {
|
|
206
|
-
errors.push(`${where}${key}: ${InputRefusal.unknownField}`);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (item === undefined) {
|
|
210
|
-
parsed[key] = item;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const value = FieldValueValidator.of(field).parse(item);
|
|
214
|
-
if ('error' in value) errors.push(`${where}${key}: ${value.error}`);
|
|
215
|
-
else parsed[key] = value.value;
|
|
234
|
+
const read = this.written(key, item);
|
|
235
|
+
if ('message' in read) errors.push(`${where}${key}: ${read.message}`);
|
|
236
|
+
else parsed[key] = read.value;
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
if (errors.length > 0) {
|
|
@@ -266,5 +287,5 @@ export class StorageGuard {
|
|
|
266
287
|
}
|
|
267
288
|
}
|
|
268
289
|
|
|
269
|
-
const unwrap = (one:
|
|
290
|
+
const unwrap = (one: Verdict): unknown =>
|
|
270
291
|
'value' in one ? one.value : undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Visibility, type Fields, type SchemaView } from '@fougere/schema';
|
|
2
|
+
import { preserveArrayProperties } from './ArrayResult.js';
|
|
3
|
+
import { asPage } from '../wire/Page.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a caller receives, read back into the shape its type promises — the dual of
|
|
7
|
+
* `OutputView.project`, applied on the other side of the call.
|
|
8
|
+
*
|
|
9
|
+
* `date-time` means a `Date` on both sides (`Boundary.forShape`), and only the outgoing half was
|
|
10
|
+
* ever applied: `Visibility.encode` turned `new Date(0)` into `"1970-01-01T00:00:00.000Z"` and
|
|
11
|
+
* nobody turned it back, so `Facade<PostHandler>` promised `createdAt: Date` and handed over a
|
|
12
|
+
* string — in this process as well as across a wire.
|
|
13
|
+
*
|
|
14
|
+
* Applied by the facade a caller HOLDS, never by the one that answers: a row leaves as data,
|
|
15
|
+
* which is what a Rust frond or a plain HTTP client reads, and the codecs are what this side
|
|
16
|
+
* knows how to put back. Measured 2026-09-18: 389 ns per row against the 1 277 ns encoding one
|
|
17
|
+
* already costs.
|
|
18
|
+
*
|
|
19
|
+
* Documented: [the gradient](https://fougere.dev/docs/infra/gradient).
|
|
20
|
+
*/
|
|
21
|
+
export function decoded(schema: SchemaView | undefined, answer: unknown): unknown {
|
|
22
|
+
if (!schema || answer === null || answer === undefined) return answer;
|
|
23
|
+
|
|
24
|
+
const fields = schema.getFields() as Fields;
|
|
25
|
+
const visibility = Visibility.of(fields);
|
|
26
|
+
const row = (one: unknown) => (one !== null && typeof one === 'object' && !Array.isArray(one)
|
|
27
|
+
? visibility.decode(one as Record<string, unknown>)
|
|
28
|
+
: one);
|
|
29
|
+
|
|
30
|
+
const page = asPage(answer, fields);
|
|
31
|
+
if (page) return { ...page, items: page.items.map(row) };
|
|
32
|
+
|
|
33
|
+
// An op annotating `ListResult<T>` still answers the array those properties ride on.
|
|
34
|
+
return Array.isArray(answer)
|
|
35
|
+
? preserveArrayProperties(answer, answer.map(row))
|
|
36
|
+
: row(answer);
|
|
37
|
+
}
|
package/src/entry/facade.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Call } from '../wire/Call.js';
|
|
|
2
2
|
|
|
3
3
|
import { RouteAddress } from '../wire/RouteAddress.js';
|
|
4
4
|
import type { DispatchPort } from '../dispatch/DispatchPort.js';
|
|
5
|
+
import type { Received } from '../dispatch/Received.js';
|
|
5
6
|
|
|
6
7
|
type Operation = (...args: any[]) => unknown;
|
|
7
8
|
|
|
@@ -22,21 +23,32 @@ export function dynamicOperations(operation: (name: string) => Operation): Recor
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Turns facade method calls into canonical dispatches.
|
|
28
|
+
*
|
|
29
|
+
* `received` is what this side puts back before handing the answer over: a row leaves as data
|
|
30
|
+
* and `date-time` means a `Date` on both sides. A facade built without one hands over what the
|
|
31
|
+
* wire carried, which is what a caller holding no schema can do.
|
|
32
|
+
*/
|
|
26
33
|
export function facadeOperations(
|
|
27
34
|
dispatcher: DispatchPort,
|
|
28
35
|
entity: string,
|
|
29
36
|
operationNames?: Iterable<string>,
|
|
30
37
|
surface?: string,
|
|
38
|
+
received?: Received,
|
|
31
39
|
): Record<string, Operation> {
|
|
32
|
-
const operation = (name: string): Operation => (invocation) =>
|
|
33
|
-
new
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const operation = (name: string): Operation => async (invocation) => {
|
|
41
|
+
const answer = await dispatcher.dispatch(new Call(
|
|
42
|
+
new RouteAddress({
|
|
43
|
+
entity,
|
|
44
|
+
operation: name,
|
|
45
|
+
...(surface !== undefined ? { surface } : {}),
|
|
46
|
+
}),
|
|
47
|
+
invocation,
|
|
48
|
+
));
|
|
49
|
+
|
|
50
|
+
return received ? received(name, answer) : answer;
|
|
51
|
+
};
|
|
40
52
|
|
|
41
53
|
return operationNames
|
|
42
54
|
? Object.fromEntries([...operationNames].map((name) => [name, operation(name)]))
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,9 @@ export type { Extension } from './boot/Extension.js';
|
|
|
6
6
|
export { defineFougere } from './define.js';
|
|
7
7
|
export type { AdapterConfig } from './AdapterConfig.js';
|
|
8
8
|
export type { FougereConfig } from './FougereConfig.js';
|
|
9
|
+
export { statesModule, type FrondsStated, type FrondStated } from './FrondsStated.js';
|
|
10
|
+
export { statedFronds, type StatedFrond } from './StatedFrond.js';
|
|
11
|
+
export { nested } from './boot/nesting.js';
|
|
9
12
|
export type { AnswerFor } from './AnswerFor.js';
|
|
10
13
|
export type { FougereNames } from './FougereNames.js';
|
|
11
14
|
export type { FougerePorts } from './FougerePorts.js';
|
|
@@ -46,6 +49,7 @@ export { JOURNAL, type Journal } from './dispatch/Journal.js';
|
|
|
46
49
|
// them — or a test of one — has to be able to make one through the facade.
|
|
47
50
|
export { DispatchEvent } from './dispatch/DispatchEvent.js';
|
|
48
51
|
export type { CallPage, CallRecord } from './contract.js';
|
|
52
|
+
export { pageOf, asPage, type Page } from './contract.js';
|
|
49
53
|
export { driftOf, agrees, explain, type CardDrift } from './contract.js';
|
|
50
54
|
export type { OperationContract } from './wire/OperationContract.js';
|
|
51
55
|
export type { OperationsMap } from './wire/OperationsMap.js';
|
package/src/node.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** What Fougere does with a filesystem, minus the scan — that is `@fougere/compiler`. */
|
|
2
2
|
export { setModuleLoader, getModuleLoader } from './loader.js';
|
|
3
|
-
export { loadConfig, loadCascadedConfig } from './FougereConfig.js';
|
|
3
|
+
export { loadConfig, loadCascadedConfig, remotesOf } from './FougereConfig.js';
|
|
4
|
+
export { statedModules } from './StatedModules.js';
|
|
4
5
|
export { defineFrond, loadFrondConfig } from './FrondConfig.js';
|
|
5
6
|
|
|
6
7
|
// Making a key and binding a name to it happen once, at a deployment, on a machine with
|
|
@@ -6,6 +6,7 @@ import type { OperationContract } from '../wire/OperationContract.js';
|
|
|
6
6
|
import { targetOf } from './prefab.js';
|
|
7
7
|
import type { CrudOpName } from './CrudOpName.js';
|
|
8
8
|
import type { CrudViews } from './CrudViews.js';
|
|
9
|
+
import { pageOf, type Page } from '../wire/Page.js';
|
|
9
10
|
import type { CrudOps } from './CrudOps.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -56,7 +57,7 @@ function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<st
|
|
|
56
57
|
output: entity, cardinality: 'page',
|
|
57
58
|
binding: [{ name: 'options', source: { kind: 'query' }, optional: true }],
|
|
58
59
|
signature: {
|
|
59
|
-
name: 'list', returnType: returns(`
|
|
60
|
+
name: 'list', returnType: returns(`Page<${name}>`, 'Page'),
|
|
60
61
|
params: [{ name: 'options', type: { raw: 'ListOptions', name: 'ListOptions' }, optional: true }],
|
|
61
62
|
},
|
|
62
63
|
},
|
|
@@ -139,7 +140,7 @@ export function Crud<E extends EntityConstructor, V extends CrudViews | EntityCo
|
|
|
139
140
|
this.storage = storage as Storage<T>;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
async list(options?: ListOptions): Promise<
|
|
143
|
+
async list(options?: ListOptions): Promise<Page<T>> { return pageOf(await this.storage.list(options)); }
|
|
143
144
|
async findById(id: string): Promise<T | undefined> { return this.storage.findById(id); }
|
|
144
145
|
async create(input: Partial<T>): Promise<T> { return this.storage.create(input); }
|
|
145
146
|
async update(id: string, input: Partial<T>): Promise<T> { return this.storage.update(id, input); }
|
package/src/prefab/CrudOps.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CrudOpName } from './CrudOpName.js';
|
|
2
2
|
import type { EntityConstructor } from '@fougere/schema';
|
|
3
3
|
import type { ListOptions } from '../storage/ListOptions.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { Page } from '../wire/Page.js';
|
|
5
5
|
import type { Storage } from '../storage/Storage.js';
|
|
6
6
|
|
|
7
7
|
/** The view an op emits, fabricated. */
|
|
@@ -16,7 +16,7 @@ type OutOf<V, K extends CrudOpName, T> =
|
|
|
16
16
|
/** The five ops, typed from the entity and its views. */
|
|
17
17
|
export interface CrudOps<T, V = {}> {
|
|
18
18
|
storage: Storage<T>;
|
|
19
|
-
list(options?: ListOptions, ...collected: never[]): Promise<
|
|
19
|
+
list(options?: ListOptions, ...collected: never[]): Promise<Page<OutOf<V, 'list', T>>>;
|
|
20
20
|
findById(id: string, ...collected: never[]): Promise<OutOf<V, 'findById', T> | undefined>;
|
|
21
21
|
create(input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'create', T>>;
|
|
22
22
|
update(id: string, input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'update', T>>;
|
|
@@ -27,7 +27,7 @@ export function Mirror<E extends EntityConstructor>(shape: E): MirrorConstructor
|
|
|
27
27
|
written += await this.storage.upsertAll(page);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return { written, since, ms: Date.now() - started };
|
|
30
|
+
return { written, ...(since && { since: since.toISOString() }), ms: Date.now() - started };
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
package/src/prefab/Refreshed.ts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
export interface Refreshed {
|
|
3
3
|
/** Instances written, counting a replaced one once. */
|
|
4
4
|
written: number;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The age the pull was asked to start from, as an ISO string — absent when it asked for
|
|
7
|
+
* everything. A STRING because a refresh is an operation: what it answers leaves through a
|
|
8
|
+
* facade, where only data crosses, and a `Date` reaches a caller here and an ISO string
|
|
9
|
+
* behind `fronds:` off the same code.
|
|
10
|
+
*/
|
|
11
|
+
since?: string;
|
|
7
12
|
/** How long the whole pass took, pull included. */
|
|
8
13
|
ms: number;
|
|
9
14
|
}
|
package/src/storage/Storage.ts
CHANGED
|
@@ -172,7 +172,7 @@ const KINDS = '|';
|
|
|
172
172
|
/** The members behind a frame key, or `undefined` when the key is not one. */
|
|
173
173
|
export function membersOfTogetherKey(key: string): { entities: string[]; providers: string[] } | undefined {
|
|
174
174
|
if (key.length <= FRAME.length || !key.endsWith(FRAME)) return undefined;
|
|
175
|
-
const [entities, providers = ''] = key.slice(0, -FRAME.length).split(KINDS);
|
|
175
|
+
const [entities = '', providers = ''] = key.slice(0, -FRAME.length).split(KINDS);
|
|
176
176
|
const split = (list: string) => list.split(SEPARATOR).filter(Boolean);
|
|
177
|
-
return { entities: split(entities
|
|
177
|
+
return { entities: split(entities), providers: split(providers) };
|
|
178
178
|
}
|
package/src/storage/Store.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { applyCreate, applyUpdate,
|
|
1
|
+
import { applyCreate, applyUpdate, Role, type SchemaView } from '@fougere/schema';
|
|
2
2
|
import { comparisonOf, comparisonsIn, type Comparison } from './Comparison.js';
|
|
3
3
|
import type { Storage } from './Storage.js';
|
|
4
4
|
import type { StorageFactory } from './StorageFactory.js';
|
|
5
5
|
import type { Values } from './Values.js';
|
|
6
|
+
import type { ListResult } from './ListResult.js';
|
|
6
7
|
|
|
7
8
|
/** Instances addressed by key — what an adapter supplies, and all of it. */
|
|
8
9
|
export interface Store {
|
|
@@ -45,19 +46,16 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
45
46
|
? Object.fromEntries(Object.entries(values).filter(([key]) => selected.has(key)))
|
|
46
47
|
: values);
|
|
47
48
|
|
|
48
|
-
// Same contract as SQL:
|
|
49
|
+
// Same contract as SQL: a row that is already there is UPDATED, so what the write leaves
|
|
50
|
+
// out stays where it was.
|
|
49
51
|
// Named, and not reached through `this`: a caller may have wrapped these gestures,
|
|
50
52
|
// and a derived one that goes back through the front facade is judged twice.
|
|
51
53
|
const upsert = async (input: Partial<Record<string, unknown>>): Promise<Values> => {
|
|
52
|
-
const
|
|
53
|
-
const id =
|
|
54
|
+
const created = applyCreate(fields, applyUpdate(fields, input));
|
|
55
|
+
const id = created[pk] as string | undefined;
|
|
54
56
|
if (id === undefined) throw new Error(`${name}.upsert(): no \`${pk}\` — an upsert needs the key it writes at.`);
|
|
55
57
|
const previous = await store.get(keyOf(id));
|
|
56
|
-
|
|
57
|
-
for (const [key, field] of Object.entries(fields)) {
|
|
58
|
-
if (key === pk || Lifecycle.of(field).stampedOnce) values[key] = previous[key];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
58
|
+
const values = previous ? { ...previous, ...applyUpdate(fields, input) } : created;
|
|
61
59
|
await store.set(keyOf(id), values);
|
|
62
60
|
return pick(values);
|
|
63
61
|
};
|
|
@@ -68,24 +66,8 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
|
|
|
68
66
|
let items = await store.all();
|
|
69
67
|
if (options?.where) items = items.filter((values) => matches(values, options.where));
|
|
70
68
|
if (options?.orderBy) items = sorted(items, options.orderBy, options.order);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// answered a different question — everything the store holds, including the ones
|
|
74
|
-
// the filter exists to keep out of this caller's sight.
|
|
75
|
-
const matching = items.length;
|
|
76
|
-
const limit = options?.limit;
|
|
77
|
-
const offset = options?.page && limit ? (options.page - 1) * limit : options?.offset ?? 0;
|
|
78
|
-
if (offset > 0) items = items.slice(offset);
|
|
79
|
-
const hasMore = limit ? items.length > limit : false;
|
|
80
|
-
if (limit) items = items.slice(0, limit);
|
|
81
|
-
// The cursor is read before the scope cuts: a view that drops the key still
|
|
82
|
-
// pages, the way it does over SQL.
|
|
83
|
-
const endCursor = items.length > 0 ? String((items[items.length - 1] as any)[pk] ?? '') : undefined;
|
|
84
|
-
const result = items.map(pick) as any;
|
|
85
|
-
result.hasMore = hasMore;
|
|
86
|
-
result.endCursor = endCursor;
|
|
87
|
-
if (options?.count) result.total = matching;
|
|
88
|
-
return result;
|
|
69
|
+
|
|
70
|
+
return paged(items, options, pk, pick);
|
|
89
71
|
},
|
|
90
72
|
async findById(id: string) {
|
|
91
73
|
const values = await store.get(keyOf(id));
|
|
@@ -206,3 +188,39 @@ const rank = (left: unknown, right: unknown): number => {
|
|
|
206
188
|
|
|
207
189
|
return held === against ? 0 : held < against ? -1 : 1;
|
|
208
190
|
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The page cut out of what matched.
|
|
194
|
+
*
|
|
195
|
+
* `total` is held BEFORE the cut and after the filter — it answers "how many match", which is
|
|
196
|
+
* what a paginator divides. Reading the store's size at the end answered a different question:
|
|
197
|
+
* everything it holds, including the rows the filter exists to keep out of this caller's sight.
|
|
198
|
+
*/
|
|
199
|
+
function paged(
|
|
200
|
+
matched: Values[],
|
|
201
|
+
options: { limit?: number; page?: number; offset?: number; count?: boolean } | undefined,
|
|
202
|
+
pk: string,
|
|
203
|
+
pick: (values: Values) => Values,
|
|
204
|
+
): ListResult<Values> {
|
|
205
|
+
const matching = matched.length;
|
|
206
|
+
const limit = options?.limit;
|
|
207
|
+
const offset = options?.page && limit ? (options.page - 1) * limit : options?.offset ?? 0;
|
|
208
|
+
|
|
209
|
+
let items = offset > 0 ? matched.slice(offset) : matched;
|
|
210
|
+
const hasMore = limit ? items.length > limit : false;
|
|
211
|
+
if (limit) items = items.slice(0, limit);
|
|
212
|
+
|
|
213
|
+
// The cursor is read before the scope cuts: a view that drops the key still pages, the way it
|
|
214
|
+
// does over SQL.
|
|
215
|
+
const endCursor = items.length > 0
|
|
216
|
+
? String((items[items.length - 1] as Record<string, unknown>)[pk] ?? '')
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
219
|
+
// An array that carries the page's terms on itself — what `ListResult` is.
|
|
220
|
+
const result = items.map(pick) as ListResult<Values>;
|
|
221
|
+
result.hasMore = hasMore;
|
|
222
|
+
result.endCursor = endCursor;
|
|
223
|
+
if (options?.count) result.total = matching;
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
package/src/verify.ts
CHANGED
|
@@ -17,6 +17,16 @@ export type Misplaced = Diagnostic & {
|
|
|
17
17
|
/** A dependency declared in a frond's scope, and what kind of thing it is. */
|
|
18
18
|
type Registration = { frond: string; kind: string };
|
|
19
19
|
|
|
20
|
+
/** Everything above a frond in the tree — what its scope reaches by walking up. */
|
|
21
|
+
function ancestors(frond: FrondDescriptor, byName: Map<string, FrondDescriptor>): Set<string> {
|
|
22
|
+
const above = new Set<string>();
|
|
23
|
+
for (let at = frond.extends; at !== undefined && !above.has(at); at = byName.get(at)?.extends) {
|
|
24
|
+
above.add(at);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return above;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
/**
|
|
21
31
|
* The container keys a frond registers in its own scope, keyed as a handler's `deps` spell them —
|
|
22
32
|
* DI resolves by type name, so both sides are PascalCase.
|
|
@@ -50,6 +60,7 @@ function injectablesOf(frond: FrondDescriptor) {
|
|
|
50
60
|
*/
|
|
51
61
|
export function verify(app: { fronds: readonly FrondDescriptor[] }): Misplaced[] {
|
|
52
62
|
const index = new Map<string, Registration>();
|
|
63
|
+
const byName = new Map(app.fronds.map((frond) => [frond.name, frond]));
|
|
53
64
|
for (const frond of app.fronds) {
|
|
54
65
|
for (const [key, reg] of registrationsOf(frond)) index.set(key, reg);
|
|
55
66
|
}
|
|
@@ -76,6 +87,10 @@ export function verify(app: { fronds: readonly FrondDescriptor[] }): Misplaced[]
|
|
|
76
87
|
// façade key, or an unresolved name. None is a boundary crossing, and
|
|
77
88
|
// an unresolved dependency is the container's complaint, not this rule's.
|
|
78
89
|
if (!declared || declared.frond === frond.name) continue;
|
|
90
|
+
// An ANCESTOR is not across: the frond tree says this one resolves what that one
|
|
91
|
+
// declared, and its scope hangs off it. A sibling, a descendant or a stranger stays
|
|
92
|
+
// refused — inheriting code is not calling a frond.
|
|
93
|
+
if (ancestors(frond, byName).has(declared.frond)) continue;
|
|
79
94
|
violations.push({
|
|
80
95
|
code: 'cross-frond-dependency',
|
|
81
96
|
severity: 'warning',
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { OperationContext } from './OperationContext.js';
|
|
2
2
|
import type { AppNext } from './AppNext.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A middleware answers what the chain answered — the same TYPE, whatever it does with the value.
|
|
6
|
+
*
|
|
7
|
+
* `T` is the whole rule and nothing enforces it at runtime: a middleware is handed a type it
|
|
8
|
+
* cannot name, so the only value of that type it can produce is the one `next()` gave it. It may
|
|
9
|
+
* observe it, log it, replace it with another of the same shape, or refuse by throwing; it may
|
|
10
|
+
* not wrap it in `{ data, meta }` nor invent one, because a caller reads the handler's signature
|
|
11
|
+
* and nothing tells that signature a middleware stood in the way.
|
|
12
|
+
*/
|
|
13
|
+
export type AppMiddleware = <T>(ctx: OperationContext, next: AppNext<T>) => Promise<T>;
|
|
5
14
|
|
|
6
15
|
// ── Runner ──────────────────────────────────────
|
|
7
16
|
|
package/src/wire/AppNext.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** What a middleware holds: the rest of the chain, and the answer it will hand back. */
|
|
2
|
+
export type AppNext<T = unknown> = () => Promise<T>;
|
package/src/wire/Facade.ts
CHANGED
|
@@ -10,10 +10,22 @@ type Served = keyof FougereOperations & string;
|
|
|
10
10
|
*/
|
|
11
11
|
type AddressIn<Key> = Key extends `${infer Address}.${string}` ? Address : never;
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The facade built in front of a handler — the framework's second port, after `Storage`.
|
|
15
|
+
*
|
|
16
|
+
* What the handler knows is WHICH operations exist and what each one answers. The two ends are
|
|
17
|
+
* the port's own: an invocation goes in where the handler takes positional arguments, and a
|
|
18
|
+
* promise comes back where the handler may answer a bare value. A facade is a crossing, and a
|
|
19
|
+
* crossing is awaited before any transport — the dispatch resolves a route, runs the middlewares
|
|
20
|
+
* and awaits the collectors, so `readLocation(): string` was typed as answering now and never did.
|
|
21
|
+
*
|
|
22
|
+
* `Awaited` because a promise does not stack: `Promise.resolve(p)` IS `p`, so writing
|
|
23
|
+
* `Promise<R>` over an async handler would describe a `Promise<Promise<Post>>` that no value can
|
|
24
|
+
* have — `.then` would hand its callback a promise the runtime never delivers.
|
|
25
|
+
*/
|
|
14
26
|
export type Facade<T> = {
|
|
15
27
|
[K in keyof T]: T[K] extends (...args: never[]) => infer R
|
|
16
|
-
? (invocation?: InvocationContext) => R
|
|
28
|
+
? (invocation?: InvocationContext) => Promise<Awaited<R>>
|
|
17
29
|
: never;
|
|
18
30
|
};
|
|
19
31
|
|
package/src/wire/FougereError.ts
CHANGED
|
@@ -20,7 +20,7 @@ export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
|
|
|
20
20
|
|
|
21
21
|
constructor(options: FougereErrorOptions<Code>) {
|
|
22
22
|
super(options.message, { cause: options.cause });
|
|
23
|
-
this.name =
|
|
23
|
+
this.name = 'FougereError';
|
|
24
24
|
this.code = options.code;
|
|
25
25
|
this.entity = options.entity;
|
|
26
26
|
this.operation = options.operation;
|
package/src/wire/Invocation.ts
CHANGED
|
@@ -24,6 +24,8 @@ function canonicalRecord(value: unknown): Record<string, unknown> {
|
|
|
24
24
|
|
|
25
25
|
/** Canonical invocation shared by every entry and transport. */
|
|
26
26
|
export class Invocation implements InvocationContext {
|
|
27
|
+
static readonly empty = Invocation.from();
|
|
28
|
+
|
|
27
29
|
readonly params: Record<string, unknown>;
|
|
28
30
|
readonly query: Record<string, unknown>;
|
|
29
31
|
readonly input: unknown;
|
|
@@ -53,7 +55,6 @@ export class Invocation implements InvocationContext {
|
|
|
53
55
|
return context instanceof Invocation ? context : new Invocation(context ?? {});
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
static readonly empty = Invocation.from();
|
|
57
58
|
|
|
58
59
|
/** Replaces the input — how the façade hands on the value it parsed. */
|
|
59
60
|
withInput(input: unknown): Invocation {
|
|
@@ -36,7 +36,7 @@ export function cardinalityOf(type: TypeRef | undefined): OperationContract['car
|
|
|
36
36
|
if (!type) return undefined;
|
|
37
37
|
const inner = type.name === 'Promise' ? type.generics?.[0] : type;
|
|
38
38
|
if (!inner) return 'none';
|
|
39
|
-
if (inner.name === 'ListResult') return 'page';
|
|
39
|
+
if (inner.name === 'Page' || inner.name === 'ListResult') return 'page';
|
|
40
40
|
if (inner.array) return 'many';
|
|
41
41
|
if (PRIMITIVE_RETURNS.has(inner.name)) return 'none';
|
|
42
42
|
return inner.nullable || inner.undefined ? 'maybe' : 'one';
|