@aventara/testing 1.0.0-rc.1 → 1.0.0-rc.11
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/CHANGELOG.md +138 -1
- package/README.md +10 -5
- package/dist/protocol/corpus-contract.fixture.d.ts +139 -366
- package/dist/protocol/corpus-contract.fixture.js +71 -17
- package/dist/protocol/in-process.driver.d.ts +5 -2
- package/dist/protocol/in-process.driver.js +14 -0
- package/dist/protocol/per-route.driver.d.ts +2 -1
- package/dist/protocol/per-route.driver.js +5 -0
- package/dist/protocol/protocol-conformance.d.ts +3 -2
- package/dist/protocol/protocol-conformance.js +5 -1
- package/dist/protocol/protocol-corpus.d.ts +4 -1
- package/dist/protocol/protocol-fixtures.js +480 -1
- package/dist/protocol/protocol-fixtures.json-codec.d.ts +2 -1
- package/dist/protocol/protocol-fixtures.json-codec.js +9 -1
- package/fixtures/protocol-v1.json +141 -100
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AvDecimal, AvError, createFramework } from "@aventara/core";
|
|
1
|
+
import { AvDecimal, AvError, cache, createFramework } from "@aventara/core";
|
|
2
2
|
import { createFakeAdapter } from "../adapters/fake-adapter.js";
|
|
3
3
|
const identityString = {
|
|
4
4
|
kind: "scalar",
|
|
@@ -26,7 +26,7 @@ const wired = (scalar) => ({
|
|
|
26
26
|
capabilities: { select: true, create: [] },
|
|
27
27
|
});
|
|
28
28
|
const findMany = { find: { many: true } };
|
|
29
|
-
|
|
29
|
+
const CORPUS_MODEL = {
|
|
30
30
|
transactions: "interactive",
|
|
31
31
|
scalars: {
|
|
32
32
|
string: { builtin: true },
|
|
@@ -80,7 +80,7 @@ export const CORPUS_MODEL = {
|
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
|
-
|
|
83
|
+
const CORPUS_REFERENTIAL_ACTIONS = {
|
|
84
84
|
books: [
|
|
85
85
|
{
|
|
86
86
|
field: "author",
|
|
@@ -133,7 +133,7 @@ function corpusPipe(args) {
|
|
|
133
133
|
const record = args;
|
|
134
134
|
return { ...record, where: { ...record.where, nope: "x" } };
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
const CORPUS_RESOURCES = {
|
|
137
137
|
books: {
|
|
138
138
|
fields: {
|
|
139
139
|
label: {
|
|
@@ -155,11 +155,32 @@ export const CORPUS_RESOURCES = {
|
|
|
155
155
|
},
|
|
156
156
|
},
|
|
157
157
|
};
|
|
158
|
-
|
|
158
|
+
const CORPUS_CACHE_RESOURCES = {
|
|
159
|
+
...CORPUS_RESOURCES,
|
|
160
|
+
authors: {
|
|
161
|
+
pipelines: { cache: cache({ for: "user", maxAge: 60 }) },
|
|
162
|
+
operations: {
|
|
163
|
+
find: {
|
|
164
|
+
first: {
|
|
165
|
+
pipelines: { cache: cache({ for: "anonymous", maxAge: 30 }) },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
books: {
|
|
171
|
+
...CORPUS_RESOURCES.books,
|
|
172
|
+
operations: {
|
|
173
|
+
find: {
|
|
174
|
+
many: { pipelines: { cache: cache({ for: "everyone", maxAge: 300 }) } },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const CORPUS_APPLICATION_CONFIG = {
|
|
159
180
|
exposed: true,
|
|
160
181
|
exemptions: { resources: { books: { fields: { label: true } } } },
|
|
161
182
|
};
|
|
162
|
-
|
|
183
|
+
const CORPUS_CLIENT_CONFIG = {
|
|
163
184
|
pipelines: { guards: [corpusGuard], pipes: [corpusPipe] },
|
|
164
185
|
exposed: true,
|
|
165
186
|
exemptions: {
|
|
@@ -171,12 +192,14 @@ export const CORPUS_CLIENT_CONFIG = {
|
|
|
171
192
|
},
|
|
172
193
|
};
|
|
173
194
|
export const CORPUS_ENTRYPOINT = "/api";
|
|
174
|
-
export const CORPUS_CLIENT_CONTRACT_HASH = "sha256:
|
|
175
|
-
export const CORPUS_CLIENT_CONTRACT_DOCUMENT = '{"enums":{},"limits":{"maxBooleanNodes":50,"maxJsonDepth":64,"maxListLimit":250,"maxNestingDepth":12,"maxRequestBytes":1048576,"maxTransactionOperations":20},"protocol":{"hash":"sha256:
|
|
176
|
-
export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:
|
|
195
|
+
export const CORPUS_CLIENT_CONTRACT_HASH = "sha256:e0f996802a6a8f1ac9954796ae3f48e0892c9d1a2e691b5cd831098f4b78d7f5";
|
|
196
|
+
export const CORPUS_CLIENT_CONTRACT_DOCUMENT = '{"enums":{},"limits":{"maxAll":10,"maxAllConcurrency":4,"maxBooleanNodes":50,"maxJsonDepth":64,"maxListLimit":250,"maxNestingDepth":12,"maxRequestBytes":1048576,"maxTransactionOperations":20},"protocol":{"hash":"sha256:e0f996802a6a8f1ac9954796ae3f48e0892c9d1a2e691b5cd831098f4b78d7f5","version":1},"resources":{"authors":{"fields":{"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}},"name":{"capabilities":{"create":[],"filter":["equals"],"select":true,"update":[]},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{"create":{"count":true,"many":true,"one":true},"delete":{"count":true,"first":true,"many":true,"unique":true},"find":{"count":true,"first":true,"many":true,"unique":true},"update":{"count":true,"first":true,"many":true,"unique":true},"upsert":{"unique":true}}},"books":{"fields":{"authorId":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}},"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}},"label":{"capabilities":{"select":true},"kind":"scalar","lifecycle":["COMPUTED_ON_READ","VIRTUAL"],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{"find":{"many":true}}},"samples":{"fields":{"amount":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"decimal"}},"at":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"datetime"}},"blob":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"bytes"}},"count":{"capabilities":{"create":[],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"bigint"}},"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{"create":{"one":true},"find":{"unique":true}}},"secrets":{"fields":{"id":{"capabilities":{"filter":["equals"],"select":true},"kind":"scalar","lifecycle":[],"list":false,"nullable":false,"type":{"scalar":"string"}}},"identifiers":[["id"]],"operations":{}}},"scalars":{"bigint":{"builtin":true},"bytes":{"builtin":true},"datetime":{"builtin":true},"decimal":{"builtin":true},"string":{"builtin":true}},"transactions":"interactive"}';
|
|
197
|
+
export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:34a73fd521d186350252a9bf905a9a627465f8267e0051ba4efe0441510e164a";
|
|
198
|
+
export const CORPUS_ACCESS_CLIENT_CONTRACT_HASH = "sha256:ee098756b61a2081f92c7c065816c9f77782561aef572c117d2ee7e18389db0b";
|
|
199
|
+
export const CORPUS_CACHE_CLIENT_CONTRACT_HASH = "sha256:d82386747eef41809c28721bc513e70db59884b5bd85ca1535038ce32127fd99";
|
|
177
200
|
export const CORPUS_AUTHOR = { id: "a1", name: "Ada" };
|
|
178
201
|
export const CORPUS_NOBODY = "nobody";
|
|
179
|
-
|
|
202
|
+
const CORPUS_SAMPLE = {
|
|
180
203
|
id: "s1",
|
|
181
204
|
count: 9007199254740993n,
|
|
182
205
|
at: new Date("2026-10-05T12:34:56.789Z"),
|
|
@@ -221,6 +244,22 @@ function corpusAnswer(operation) {
|
|
|
221
244
|
}
|
|
222
245
|
return CORPUS_AUTHOR;
|
|
223
246
|
}
|
|
247
|
+
const CORPUS_ACCESS = {
|
|
248
|
+
state: () => ({ me: null }),
|
|
249
|
+
policy: {
|
|
250
|
+
can: ({ resource }) => {
|
|
251
|
+
if (resource === "samples") {
|
|
252
|
+
throw new Error("the corpus policy failed");
|
|
253
|
+
}
|
|
254
|
+
if (resource === "books") {
|
|
255
|
+
throw new AvError("A4002", "The corpus policy refuses books.");
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
},
|
|
259
|
+
scopeOf: ({ resource, action }) => !(resource === "authors" && action === "delete"),
|
|
260
|
+
fieldsOf: () => ({ always: "all", perRow: {} }),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
224
263
|
function failingCommit(transaction) {
|
|
225
264
|
return async (work) => {
|
|
226
265
|
let doomed = false;
|
|
@@ -238,7 +277,7 @@ function failingCommit(transaction) {
|
|
|
238
277
|
return result;
|
|
239
278
|
};
|
|
240
279
|
}
|
|
241
|
-
export
|
|
280
|
+
export function corpusConfiguration(contractHash) {
|
|
242
281
|
const fake = createFakeAdapter({
|
|
243
282
|
model: contractHash === CORPUS_NONE_CLIENT_CONTRACT_HASH
|
|
244
283
|
? { ...CORPUS_MODEL, transactions: "none" }
|
|
@@ -251,12 +290,27 @@ export async function createCorpusFramework(contractHash) {
|
|
|
251
290
|
? {}
|
|
252
291
|
: { transaction: failingCommit(fake.transaction) }),
|
|
253
292
|
});
|
|
293
|
+
return {
|
|
294
|
+
config: {
|
|
295
|
+
api: CORPUS_ENTRYPOINT,
|
|
296
|
+
adapter,
|
|
297
|
+
resources: contractHash === CORPUS_CACHE_CLIENT_CONTRACT_HASH
|
|
298
|
+
? CORPUS_CACHE_RESOURCES
|
|
299
|
+
: CORPUS_RESOURCES,
|
|
300
|
+
application: CORPUS_APPLICATION_CONFIG,
|
|
301
|
+
client: CORPUS_CLIENT_CONFIG,
|
|
302
|
+
...(contractHash === CORPUS_ACCESS_CLIENT_CONTRACT_HASH
|
|
303
|
+
? { access: CORPUS_ACCESS }
|
|
304
|
+
: {}),
|
|
305
|
+
},
|
|
306
|
+
adapterCalls: () => adapter.operations.length,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
export async function createCorpusFramework(contractHash, prebuilt) {
|
|
310
|
+
const { config, adapterCalls } = corpusConfiguration(contractHash);
|
|
254
311
|
const framework = await createFramework({
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
resources: CORPUS_RESOURCES,
|
|
258
|
-
application: CORPUS_APPLICATION_CONFIG,
|
|
259
|
-
client: CORPUS_CLIENT_CONFIG,
|
|
312
|
+
...config,
|
|
313
|
+
...(prebuilt === undefined ? {} : { prebuilt }),
|
|
260
314
|
});
|
|
261
|
-
return { framework, adapterCalls
|
|
315
|
+
return { framework, adapterCalls };
|
|
262
316
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { AvProtocol, type AvProtocolResponse } from "@aventara/core/protocol";
|
|
1
2
|
import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
2
3
|
/**
|
|
3
4
|
* The protocol hosted in process, with no HTTP stack: `/_contract` goes to
|
|
4
5
|
* `protocol.encodeContract`, `/_transactions` to `protocol.handleTransaction`,
|
|
5
|
-
*
|
|
6
|
-
* back as it is.
|
|
6
|
+
* `/_access/can` to `protocol.handleCan`, every other request to
|
|
7
|
+
* `protocol.handleOperation`, and the answer is written back as it is.
|
|
7
8
|
*/
|
|
8
9
|
export declare const inProcessDriver: AvProtocolDriver;
|
|
10
|
+
/** An Operation Set's answer as a host writes it: a streamed one's lines, in order, as its body text. */
|
|
11
|
+
export declare function written(answer: Awaited<ReturnType<ReturnType<typeof AvProtocol.bind>["handleAll"]>>): Promise<AvProtocolResponse>;
|
|
@@ -8,6 +8,10 @@ export const inProcessDriver = async (framework) => {
|
|
|
8
8
|
return protocol.encodeContract(request);
|
|
9
9
|
case "/_transactions":
|
|
10
10
|
return protocol.handleTransaction(request);
|
|
11
|
+
case "/_access/can":
|
|
12
|
+
return protocol.handleCan(request);
|
|
13
|
+
case "/_all":
|
|
14
|
+
return written(await protocol.handleAll(request));
|
|
11
15
|
default:
|
|
12
16
|
return protocol.handleOperation(request);
|
|
13
17
|
}
|
|
@@ -15,3 +19,13 @@ export const inProcessDriver = async (framework) => {
|
|
|
15
19
|
close: async () => undefined,
|
|
16
20
|
};
|
|
17
21
|
};
|
|
22
|
+
export async function written(answer) {
|
|
23
|
+
if (typeof answer.body === "string") {
|
|
24
|
+
return answer;
|
|
25
|
+
}
|
|
26
|
+
let body = "";
|
|
27
|
+
for await (const line of answer.body) {
|
|
28
|
+
body += line;
|
|
29
|
+
}
|
|
30
|
+
return { status: answer.status, headers: answer.headers, body };
|
|
31
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* The protocol hosted in process as a host that mounts one route per
|
|
4
4
|
* `protocol.surface()` entry (each operation on `handleOperation`,
|
|
5
|
-
* `/_transactions` on `handleTransaction`, `/
|
|
5
|
+
* `/_transactions` on `handleTransaction`, `/_access/can` on `handleCan`,
|
|
6
|
+
* `/_all` on `handleAll`, `/_contract` on `encodeContract`,
|
|
6
7
|
* matched by method and path) and uses `protocol.answerAbsent` as the fallback
|
|
7
8
|
* for everything it did not mount. `undefined` from the fallback is a path
|
|
8
9
|
* outside the protocol, which this host answers with its own bodiless 404.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
2
|
+
import { written } from "./in-process.driver.js";
|
|
2
3
|
const HOST_NOT_FOUND = Object.freeze({
|
|
3
4
|
status: 404,
|
|
4
5
|
headers: Object.freeze({}),
|
|
@@ -14,6 +15,10 @@ export const perRouteDriver = async (framework) => {
|
|
|
14
15
|
return protocol.handleOperation(request);
|
|
15
16
|
case "transactions":
|
|
16
17
|
return protocol.handleTransaction(request);
|
|
18
|
+
case "can":
|
|
19
|
+
return protocol.handleCan(request);
|
|
20
|
+
case "all":
|
|
21
|
+
return written(await protocol.handleAll(request));
|
|
17
22
|
case "contract":
|
|
18
23
|
return protocol.encodeContract(request);
|
|
19
24
|
default:
|
|
@@ -8,8 +8,9 @@ import type { AvProtocolDriver } from "./protocol-driver.js";
|
|
|
8
8
|
* answer, so they are skipped. Each driven row runs against a fresh corpus
|
|
9
9
|
* Framework, through a fresh driver session, with one request (the corpus
|
|
10
10
|
* Framework whose ClientContract hash the row's `Aventara-Contract-Hash`
|
|
11
|
-
* names
|
|
12
|
-
* held to the row's `server`
|
|
11
|
+
* names, or a cached read's `v` when it sends no such header; the main corpus
|
|
12
|
+
* contract for any other hash), and its answer is held to the row's `server`
|
|
13
|
+
* block:
|
|
13
14
|
*
|
|
14
15
|
* - `status`, exactly;
|
|
15
16
|
* - `code`: the body is a framework envelope carrying that code or, for
|
|
@@ -70,7 +70,11 @@ function mismatchesOf(server, response, adapterCalls) {
|
|
|
70
70
|
}
|
|
71
71
|
function contractHashOf(request) {
|
|
72
72
|
const value = Object.entries(request.headers).find(([name]) => name.toLowerCase() === "aventara-contract-hash")?.[1];
|
|
73
|
-
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
const v = new URLSearchParams(request.query ?? "").get("v");
|
|
77
|
+
return v === null ? undefined : v;
|
|
74
78
|
}
|
|
75
79
|
function parsedBody(text) {
|
|
76
80
|
try {
|
|
@@ -17,7 +17,10 @@ export declare const AvProtocolCorpus: {
|
|
|
17
17
|
* pure function of the operation it is handed. `contractHash` picks which
|
|
18
18
|
* corpus ClientContract it serves, as a row's `Aventara-Contract-Hash` does:
|
|
19
19
|
* the corpus's `transactions: "none"` contract for that contract's hash (the
|
|
20
|
-
* `/_transactions` absence rows send it), the
|
|
20
|
+
* `/_transactions` absence rows send it), the one with the framework's
|
|
21
|
+
* `access` option for its hash (the `/_access/can` rows send it), the one with
|
|
22
|
+
* cached reads for its hash (the cached-read rows send it as their query's
|
|
23
|
+
* `v`), the main one for anything else.
|
|
21
24
|
*/
|
|
22
25
|
readonly framework: (contractHash?: string) => Promise<AvFramework>;
|
|
23
26
|
/** Where the corpus Framework is mounted; every row's `path` is relative to it. */
|