@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.
@@ -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
- export const CORPUS_MODEL = {
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
- export const CORPUS_REFERENTIAL_ACTIONS = {
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
- export const CORPUS_RESOURCES = {
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
- export const CORPUS_APPLICATION_CONFIG = {
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
- export const CORPUS_CLIENT_CONFIG = {
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:502530d5b918266c61f45e70035acd7d9637bbf7305c5572106185d5e63ec307";
175
- export const CORPUS_CLIENT_CONTRACT_DOCUMENT = '{"enums":{},"limits":{"maxBooleanNodes":50,"maxJsonDepth":64,"maxListLimit":250,"maxNestingDepth":12,"maxRequestBytes":1048576,"maxTransactionOperations":20},"protocol":{"hash":"sha256:502530d5b918266c61f45e70035acd7d9637bbf7305c5572106185d5e63ec307","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"}';
176
- export const CORPUS_NONE_CLIENT_CONTRACT_HASH = "sha256:5e99809294a2aae922b635e9a44b464b1b6e03359a54d30420292c62676a7ce5";
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
- export const CORPUS_SAMPLE = {
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 async function createCorpusFramework(contractHash) {
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
- api: CORPUS_ENTRYPOINT,
256
- adapter,
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: () => adapter.operations.length };
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
- * every other request to `protocol.handleOperation`, and the answer is written
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`, `/_contract` on `encodeContract`,
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; the main corpus contract for any other hash), and its answer is
12
- * held to the row's `server` block:
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
- return typeof value === "string" ? value : undefined;
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 main one for anything else.
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. */