@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.
@@ -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
- jsonResponse(encodeRecordEnvelope(row, collect(rows, [row])), {
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
- ): Readonly<Record<string, RecordRows>> {
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 nullProto([...out].map(([type, bucket]) => [type, nullProto(bucket)] as const));
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;