@ai-matrx/records 0.48.0 → 0.49.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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog — @ai-matrx/records
2
2
 
3
+ ## 0.49.0
4
+
5
+ **A number on a dashboard can be walked into.** (lane DRILL.)
6
+
7
+ * **`custom.read_records_matching` — the store's filtered read door**, and `client.list` now
8
+ asks it for EVERY page, filtered or not, with `{}` when nobody asked a question. The grid,
9
+ the board, the record list and a dashboard drill-through are therefore one door, decided by
10
+ one ladder: a narrowed screen can never be a differently-judged screen. `list({ filter })`
11
+ and `useRecords(tableId, { filter })` are how a caller asks.
12
+ * **`RecordFilter` — the ONE name for the store's filter shape.** It is not new and never was
13
+ the aggregate's private shape: `platform.saved_view` has carried exactly this object of
14
+ Field key → value as `definition -> 'filters'` since DOOR-18, `custom.view_declare` writes
15
+ it, `custom.agg_view_admits` reads it, and `custom.record_aggregate` takes it as `p_filter`.
16
+ A scalar is an equality, `null` is UNSET (the third state of a tick box), an object is a
17
+ half-open time window. `SavedViewRow.filters` and `SavedViewSpecIn.filters` were
18
+ `Record<string, unknown>` and are this now — an `unknown` there is a digest that quietly
19
+ notifies about the wrong records.
20
+ * **One evaluator in the database, not two.** `custom.agg_sql`'s filter loop moved verbatim
21
+ into `custom.record_filter_sql` and the aggregate CALLS it, so the number a chart draws and
22
+ the rows a drill lands on are produced by the same bytes. Measured on Rincon Plumbing Co's
23
+ 41 live jobs: the chart says Cancelled 4, the door hands back RPC-1036…1039 and nothing
24
+ else; the rendered choice label and the stored key both land on the same four; a window on
25
+ `scheduled_date` answers 5 through both doors.
26
+ * **Why it is a new name and not a sixth argument** — the REALTIME-2 ruling, again: a defaulted
27
+ `p_filter` beside the live five-argument `custom.read_records` makes every existing call
28
+ ambiguous, and the only way to add it in place is to drop a live door.
29
+ * **`stableFilterKey`** — key-sorted JSON of a `RecordFilter`, so a React parent that rebuilds
30
+ `{ status: "Cancelled" }` on every render re-reads the page when the QUESTION changes rather
31
+ than on every render forever.
32
+
3
33
  ## 0.48.0
4
34
 
5
35
  **A portal principal carries the link the office copies for them.** (lane TAILS-5.)
@@ -124,6 +124,7 @@ __export(core_exports, {
124
124
  publicFormPath: () => publicFormPath,
125
125
  rememberedOpCount: () => rememberedOpCount,
126
126
  scopeChip: () => scopeChip,
127
+ stableFilterKey: () => stableFilterKey,
127
128
  staleWriteDetail: () => staleWriteDetail,
128
129
  supabaseDataSource: () => supabaseDataSource,
129
130
  toAoa: () => toAoa
@@ -484,6 +485,20 @@ function portalPath(slug) {
484
485
  function measureKey(measure) {
485
486
  return measure.op === "count" ? "count" : `${measure.op}_${measure.key ?? ""}`;
486
487
  }
488
+ function stableFilterKey(filter) {
489
+ if (!filter) return "{}";
490
+ const keys = Object.keys(filter).sort();
491
+ if (keys.length === 0) return "{}";
492
+ const parts = keys.map((key) => {
493
+ const value = filter[key];
494
+ if (value !== null && typeof value === "object") {
495
+ const window2 = value;
496
+ return `${JSON.stringify(key)}:{"from":${JSON.stringify(window2.from ?? null)},"to":${JSON.stringify(window2.to ?? null)}}`;
497
+ }
498
+ return `${JSON.stringify(key)}:${JSON.stringify(value ?? null)}`;
499
+ });
500
+ return `{${parts.join(",")}}`;
501
+ }
487
502
 
488
503
  // src/capture-queue.ts
489
504
  var DB_NAME = "matrx-capture";
@@ -1258,6 +1273,7 @@ var STORE_DOORS = [
1258
1273
  { name: "read_record", args: "p_organization_id uuid, p_record_id uuid, p_by_id boolean DEFAULT false", returns: "jsonb", security: "definer" },
1259
1274
  { name: "read_records", args: "p_organization_id uuid, p_table_id uuid, p_by_id boolean DEFAULT false, p_limit integer DEFAULT 200, p_offset integer DEFAULT 0", returns: "TABLE(id uuid, document jsonb, level permission_level)", security: "definer" },
1260
1275
  { name: "read_records_by_ids", args: "p_organization_id uuid, p_table_id uuid, p_record_ids uuid[], p_by_id boolean DEFAULT false", returns: "TABLE(id uuid, document jsonb, level permission_level)", security: "definer" },
1276
+ { name: "read_records_matching", args: "p_organization_id uuid, p_table_id uuid, p_filter jsonb DEFAULT '{}'::jsonb, p_by_id boolean DEFAULT false, p_limit integer DEFAULT 200, p_offset integer DEFAULT 0", returns: "TABLE(id uuid, document jsonb, level permission_level)", security: "definer" },
1261
1277
  { name: "realtime_topic_admits", args: "p_topic text", returns: "boolean", security: "definer" },
1262
1278
  { name: "record_aggregate", args: "p_organization_id uuid, p_table_id uuid, p_group_by jsonb DEFAULT '[]'::jsonb, p_measures jsonb DEFAULT '[]'::jsonb, p_bucket jsonb DEFAULT NULL::jsonb, p_filter jsonb DEFAULT '{}'::jsonb, p_limit integer DEFAULT 200, p_required text DEFAULT 'viewer'::text", returns: "TABLE(groups jsonb, measures jsonb, row_count bigint)", security: "definer" },
1263
1279
  { name: "record_applicability", args: "p_organization_id uuid, p_record_id uuid", returns: "TABLE(rule_id uuid, rule_name text, rule_version integer, applies boolean, answer jsonb)", security: "invoker" },
@@ -2592,6 +2608,15 @@ var DOORS = {
2592
2608
  // live Postgres function makes every existing call ambiguous — `is not unique`. Same
2593
2609
  // ladder, same level, same masking, same page ceiling.
2594
2610
  readRecordsByIds: "read_records_by_ids",
2611
+ // THE SAME READ DOOR, ADDRESSED BY A QUESTION. `custom.read_records_matching`
2612
+ // takes the store's one filter shape (`RecordFilter` — the object
2613
+ // `platform.saved_view` carries as `filters` and `custom.record_aggregate`
2614
+ // takes as `p_filter`) and narrows the page INSIDE its own query, in the same
2615
+ // WHERE as Visibility. A separate NAME for the same reason as above. It is
2616
+ // what makes a number on a dashboard walkable: the filter is evaluated by
2617
+ // `custom.record_filter_sql`, the one body `custom.agg_sql` also calls, so the
2618
+ // rows it opens are the rows the number counted.
2619
+ readRecordsMatching: "read_records_matching",
2595
2620
  // DOOR-8, the two clocks. `p_recorded_at` is what the store SAID then;
2596
2621
  // `p_world_on` is what was TRUE of the world on that date, read out of dated
2597
2622
  // values. A client that answered either one itself would be guessing.
@@ -3291,12 +3316,13 @@ function createRecordsClient(config) {
3291
3316
  })
3292
3317
  );
3293
3318
  },
3294
- async list({ table_id, limit = 50, offset = 0 }) {
3319
+ async list({ table_id, filter, limit = 50, offset = 0 }) {
3295
3320
  const door = await callDoor(
3296
- DOORS.readRecords,
3321
+ DOORS.readRecordsMatching,
3297
3322
  {
3298
3323
  p_organization_id: org,
3299
3324
  p_table_id: table_id,
3325
+ p_filter: filter ?? {},
3300
3326
  p_limit: limit,
3301
3327
  p_offset: offset
3302
3328
  },