@ultimat3/query 29.4.0 → 30.1.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/CLAUDE.md +14 -5
- package/README.md +38 -3
- package/package.json +18 -7
- package/src/cache.ts +15 -3
- package/src/client.ts +32 -8
- package/src/contract-test.ts +59 -0
- package/src/declared-errors-contract.ts +34 -0
- package/src/declared-errors.ts +26 -0
- package/src/download-http.ts +38 -27
- package/src/download.ts +28 -9
- package/src/errors.ts +16 -5
- package/src/exercise.ts +156 -0
- package/src/facade.ts +13 -5
- package/src/http-etag.ts +44 -0
- package/src/http-pin-error.ts +17 -0
- package/src/http-pin.ts +67 -0
- package/src/http.ts +38 -4
- package/src/index.ts +15 -0
- package/src/live-trace.ts +36 -0
- package/src/live.ts +3 -2
- package/src/openapi-errors.ts +19 -0
- package/src/openapi-etag.ts +43 -0
- package/src/openapi.ts +100 -51
- package/src/query.ts +41 -14
- package/src/read-freshness.ts +63 -0
- package/src/read.ts +69 -18
- package/src/record-answer.ts +18 -6
- package/src/registry.ts +4 -0
package/src/record-answer.ts
CHANGED
|
@@ -25,8 +25,8 @@ export type RecordAnswer = (answer: readonly object[] | Page<object>) => Respons
|
|
|
25
25
|
export function recordAnswerFor(rows: StandardSchemaV1 | undefined): RecordAnswer {
|
|
26
26
|
if (!answersRecords(rows)) return (answer) => jsonResponse(answer, datesOf(answer));
|
|
27
27
|
return (answer) => {
|
|
28
|
-
const records = collect(rows, isPage(answer) ? answer.rows : answer);
|
|
29
|
-
return jsonResponse(encodeRecordEnvelope(answer, records), {
|
|
28
|
+
const { records, order } = collect(rows, isPage(answer) ? answer.rows : answer);
|
|
29
|
+
return jsonResponse(encodeRecordEnvelope(answer, records, undefined, order), {
|
|
30
30
|
headers: { [RECORDS_HEADER]: '1', ...datesOf(answer).headers },
|
|
31
31
|
});
|
|
32
32
|
};
|
|
@@ -49,10 +49,12 @@ function datesOf(data: unknown): { readonly headers: Record<string, string> } {
|
|
|
49
49
|
*/
|
|
50
50
|
export function recordRowAnswerFor(rows: StandardSchemaV1 | undefined): (row: object) => Response {
|
|
51
51
|
if (!answersRecords(rows)) return (row) => jsonResponse(row, datesOf(row));
|
|
52
|
-
return (row) =>
|
|
53
|
-
|
|
52
|
+
return (row) => {
|
|
53
|
+
const { records, order } = collect(rows, [row]);
|
|
54
|
+
return jsonResponse(encodeRecordEnvelope(row, records, undefined, order), {
|
|
54
55
|
headers: { [RECORDS_HEADER]: '1', ...datesOf(row).headers },
|
|
55
56
|
});
|
|
57
|
+
};
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** The one predicate the route and the OpenAPI projection both ask: can this read answer records? */
|
|
@@ -69,11 +71,18 @@ function isPage(answer: readonly object[] | Page<object>): answer is Page<object
|
|
|
69
71
|
* Every entity row across the answer's rows: record type -> record key -> row. First sighting wins,
|
|
70
72
|
* `rowsOf`'s own rule — one record shown twice is one record. Built in `Map`s and handed out as
|
|
71
73
|
* null-prototype objects: a record type and a record key are data, and `__proto__` must stay a key.
|
|
74
|
+
*
|
|
75
|
+
* `order` is each type's keys as the answer met them. An integer-like key is an array index to
|
|
76
|
+
* every object, so the records object re-sorts it; `encodeRecordEnvelope` sends the order only for
|
|
77
|
+
* a type whose object lost it, so a uuid-keyed answer's body is what it always was.
|
|
72
78
|
*/
|
|
73
79
|
function collect(
|
|
74
80
|
schema: StandardSchemaV1,
|
|
75
81
|
list: readonly object[],
|
|
76
|
-
):
|
|
82
|
+
): {
|
|
83
|
+
readonly records: Readonly<Record<string, RecordRows>>;
|
|
84
|
+
readonly order: Readonly<Record<string, readonly string[]>>;
|
|
85
|
+
} {
|
|
77
86
|
const out = new Map<string, Map<string, Row>>();
|
|
78
87
|
for (const row of list) {
|
|
79
88
|
for (const [type, byKey] of Object.entries(rowsOf(schema, row))) {
|
|
@@ -83,7 +92,10 @@ function collect(
|
|
|
83
92
|
if (!bucket.has(key)) bucket.set(key, found);
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
|
-
return
|
|
95
|
+
return {
|
|
96
|
+
records: nullProto([...out].map(([type, bucket]) => [type, nullProto(bucket)] as const)),
|
|
97
|
+
order: nullProto([...out].map(([type, bucket]) => [type, [...bucket.keys()]] as const)),
|
|
98
|
+
};
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
function nullProto<V>(entries: Iterable<readonly [string, V]>): Readonly<Record<string, V>> {
|
package/src/registry.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* identifier the source file uses.
|
|
5
5
|
*/
|
|
6
6
|
import { registerPrimitiveRegistrar } from '@ultimat3/core';
|
|
7
|
+
import { assertNoSensitiveOutput } from '@ultimat3/schema';
|
|
7
8
|
import { QueryDuplicateError, QueryPolicyMissingError } from './errors';
|
|
8
9
|
import type { AnyQuery, QueryDescriptor } from './query';
|
|
9
10
|
import { isQuery, nameQuery } from './query';
|
|
@@ -28,6 +29,9 @@ export function registerQuery<Q extends AnyQuery>(name: string, target: Q): Q {
|
|
|
28
29
|
if (target.policy === undefined || target.policy === null) {
|
|
29
30
|
throw new QueryPolicyMissingError(name);
|
|
30
31
|
}
|
|
32
|
+
// Rows are sent back, which `.sensitive()` (`writeOnly`) forbids. PII read back and only hidden
|
|
33
|
+
// in logs is `redactKeys`, not this flag.
|
|
34
|
+
if (target.rows !== undefined) assertNoSensitiveOutput(`query "${name}" rows`, target.rows);
|
|
31
35
|
const named = nameQuery(target, name);
|
|
32
36
|
registry.set(name, named);
|
|
33
37
|
return named;
|