@ai-matrx/records 0.57.0 → 0.57.2
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 +36 -0
- package/dist/core/index.cjs +113 -17
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +37 -3
- package/dist/core/index.d.ts +37 -3
- package/dist/core/index.js +113 -17
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +103 -11
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +61 -2
- package/dist/react/index.d.ts +61 -2
- package/dist/react/index.js +103 -11
- package/dist/react/index.js.map +1 -1
- package/dist/{types-DC2tYa4w.d.cts → types-BpBSMhYq.d.cts} +44 -2
- package/dist/{types-DC2tYa4w.d.ts → types-BpBSMhYq.d.ts} +44 -2
- package/dist/use-cases/index.cjs.map +1 -1
- package/dist/use-cases/index.d.cts +2 -2
- package/dist/use-cases/index.d.ts +2 -2
- package/dist/use-cases/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records
|
|
2
2
|
|
|
3
|
+
## 0.57.2 (unpublished — lane HUB-FIX, 2026-09-23; `custom.table_facts` is live on the main database, ledger 2026-09-23 11:51Z, and `STORE_DOORS` is regenerated to carry it)
|
|
4
|
+
|
|
5
|
+
* `tableList()` folds `custom.table_facts` onto every Table: `visibility` (a record column the
|
|
6
|
+
Table document never carried, so every lane decision fell through to its default) and
|
|
7
|
+
`created_by` — set ONLY to the caller's own id and only on Tables the caller made. The call is
|
|
8
|
+
made only when `STORE_DOORS` carries the door (the door-presence pattern): a build generated
|
|
9
|
+
without it asks nothing and returns the Tables exactly as before. (The code landed inside 0.57.1's commit `7f2a737913` by a sweep; this
|
|
10
|
+
entry is its record.) Test `__tests__/table-list-facts.test.ts`.
|
|
11
|
+
* New `tableArchived({ table_id })`: the archived row for one Table, or `null` — paged over the
|
|
12
|
+
Table kernel's archive, the same door a table's own archive uses.
|
|
13
|
+
|
|
14
|
+
## 0.57.1 (unpublished — lane RECORDS-UI-FIX, guide re-walk 2026-09-23)
|
|
15
|
+
|
|
16
|
+
* **A refusal is not an app error.** The default error sink printed every refusal through
|
|
17
|
+
`console.error`, and matrx-frontend's error capture counts every `console.error`, so one
|
|
18
|
+
refused value lit a red "2 errors" pill. The codes a person causes (`refused_by_rule`,
|
|
19
|
+
`stale_write`, `invalid_argument`, `already_exists`, `missing_reference`) now go to
|
|
20
|
+
`console.info`; everything else stays `console.error`. New exports (core):
|
|
21
|
+
`isAPersonsRefusal`, `A_PERSONS_REFUSAL`. Test: `src/__tests__/a-refusal-is-never-an-app-error.test.ts`.
|
|
22
|
+
* **Every screen hears the writes made on its page.** `recordUpdate`, `recordDelete`,
|
|
23
|
+
`recordRestore`, `restoreVersion` and `valueRestore` announce the record they wrote on
|
|
24
|
+
success. New exports: core `announceRecordChanged` / `onRecordChanged`, react
|
|
25
|
+
`useRecordChangeRevision(recordId)`; `useRecord` re-reads on it.
|
|
26
|
+
|
|
27
|
+
## Unreleased — STORE-RULE-GAPS (2026-09-23)
|
|
28
|
+
|
|
29
|
+
* `FieldRule` gains `example` (a pattern's "how to write it") and `min` (a length's shortest);
|
|
30
|
+
`value` is optional for a min-only length rule. `NewFieldDeclaration.allow_other` (sent as
|
|
31
|
+
`config.allow_other`) and `FieldPatch.allow_other`; `allowsOtherValues(field)`.
|
|
32
|
+
`PredictedRefusal.detail`.
|
|
33
|
+
* The mirror predicts the store's new sentences word for word ("… has to be at least 20
|
|
34
|
+
characters long"; a pattern with an example hints "Enter it like …" with `detail.example`),
|
|
35
|
+
and no longer predicts a refusal for a list value that is not a uuid — the store holds option
|
|
36
|
+
KEYS (CHOICE-VALUE) and a list that takes other values adds an unknown word.
|
|
37
|
+
* Needs matrx-frontend `storerulegaps_an_example_a_shortest_length_and_other_choices.sql`.
|
|
38
|
+
|
|
3
39
|
## 0.57.0
|
|
4
40
|
|
|
5
41
|
**The grid primitives are live, so their doors are in the catalogue.** (published by lane
|
package/dist/core/index.cjs
CHANGED
|
@@ -2199,6 +2199,7 @@ __export(core_exports, {
|
|
|
2199
2199
|
ARCHIVE_LANES: () => ARCHIVE_LANES,
|
|
2200
2200
|
ARCHIVE_LANE_LABEL: () => ARCHIVE_LANE_LABEL,
|
|
2201
2201
|
ARCHIVE_LANE_MEANS: () => ARCHIVE_LANE_MEANS,
|
|
2202
|
+
A_PERSONS_REFUSAL: () => A_PERSONS_REFUSAL,
|
|
2202
2203
|
COMPUTE_ON: () => COMPUTE_ON,
|
|
2203
2204
|
CONTEXT_POLICIES: () => CONTEXT_POLICIES,
|
|
2204
2205
|
DECLARABLE_FIELD_KINDS: () => DECLARABLE_FIELD_KINDS,
|
|
@@ -2245,6 +2246,8 @@ __export(core_exports, {
|
|
|
2245
2246
|
VALUE_ALTERNATE_KEYS: () => VALUE_ALTERNATE_KEYS,
|
|
2246
2247
|
VALUE_ENVELOPE_KEYS: () => VALUE_ENVELOPE_KEYS,
|
|
2247
2248
|
actionRefusals: () => actionRefusals,
|
|
2249
|
+
allowsOtherValues: () => allowsOtherValues,
|
|
2250
|
+
announceRecordChanged: () => announceRecordChanged,
|
|
2248
2251
|
archivedByLine: () => archivedByLine,
|
|
2249
2252
|
archivedWhen: () => archivedWhen,
|
|
2250
2253
|
asPermissionLevel: () => asPermissionLevel,
|
|
@@ -2277,6 +2280,7 @@ __export(core_exports, {
|
|
|
2277
2280
|
importXlsx: () => importXlsx,
|
|
2278
2281
|
indexedDbStore: () => indexedDbStore,
|
|
2279
2282
|
intersectIds: () => intersectIds,
|
|
2283
|
+
isAPersonsRefusal: () => isAPersonsRefusal,
|
|
2280
2284
|
isArchiveLane: () => isArchiveLane,
|
|
2281
2285
|
isDecided: () => isDecided,
|
|
2282
2286
|
isOwnOp: () => isOwnOp,
|
|
@@ -2291,6 +2295,7 @@ __export(core_exports, {
|
|
|
2291
2295
|
mintClientKey: () => mintClientKey,
|
|
2292
2296
|
mintOpId: () => mintOpId,
|
|
2293
2297
|
ok: () => ok,
|
|
2298
|
+
onRecordChanged: () => onRecordChanged,
|
|
2294
2299
|
openCaptureQueue: () => openCaptureQueue,
|
|
2295
2300
|
optionKey: () => optionKey,
|
|
2296
2301
|
optionLabel: () => optionLabel,
|
|
@@ -2439,6 +2444,11 @@ function highestLevel(left, right) {
|
|
|
2439
2444
|
return ORDINAL[left] >= ORDINAL[right] ? left : right;
|
|
2440
2445
|
}
|
|
2441
2446
|
|
|
2447
|
+
// src/field.ts
|
|
2448
|
+
function allowsOtherValues(field) {
|
|
2449
|
+
return field.type === "list" && field.config?.["allow_other"] === true;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2442
2452
|
// src/choice.ts
|
|
2443
2453
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2444
2454
|
function optionKey(option) {
|
|
@@ -3813,6 +3823,7 @@ var STORE_DOORS = [
|
|
|
3813
3823
|
{ name: "table_declare", args: "p_organization_id uuid, p_spec jsonb", returns: "uuid", security: "definer" },
|
|
3814
3824
|
{ name: "table_decorate", args: "p_organization_id uuid, p_table_id uuid, p_path text[], p_value jsonb DEFAULT NULL::jsonb", returns: "jsonb", security: "definer" },
|
|
3815
3825
|
{ name: "table_decorations", args: "p_organization_id uuid, p_table_id uuid", returns: "jsonb", security: "definer" },
|
|
3826
|
+
{ name: "table_facts", args: "p_organization_id uuid", returns: "TABLE(table_id uuid, visibility text, mine boolean)", security: "definer" },
|
|
3816
3827
|
{ name: "table_from_example", args: "p_organization_id uuid, p_home_id uuid, p_example jsonb", returns: "jsonb", security: "definer" },
|
|
3817
3828
|
{ name: "table_has_a_visible_record", args: "p_user uuid, p_organization_id uuid, p_table_id uuid", returns: "boolean", security: "definer" },
|
|
3818
3829
|
{ name: "table_is_live", args: "p_organization_id uuid, p_table_id uuid", returns: "boolean", security: "invoker" },
|
|
@@ -5313,6 +5324,7 @@ var DOORS = {
|
|
|
5313
5324
|
// everywhere in that organization. This is the one question the rail has to
|
|
5314
5325
|
// ask, under the same wall and visibility gate as `portals` itself.
|
|
5315
5326
|
portalTables: "portal_tables",
|
|
5327
|
+
tableFacts: "table_facts",
|
|
5316
5328
|
portalCard: "portal_card",
|
|
5317
5329
|
portalDeclare: "portal_declare",
|
|
5318
5330
|
portalInvite: "portal_invite",
|
|
@@ -5697,6 +5709,22 @@ function staleWriteDetail(error) {
|
|
|
5697
5709
|
};
|
|
5698
5710
|
}
|
|
5699
5711
|
|
|
5712
|
+
// src/core/recordChanges.ts
|
|
5713
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
5714
|
+
function announceRecordChanged(recordId) {
|
|
5715
|
+
for (const listener of [...listeners]) {
|
|
5716
|
+
try {
|
|
5717
|
+
listener(recordId);
|
|
5718
|
+
} catch (thrown) {
|
|
5719
|
+
console.error("[@ai-matrx/records] a record-change listener threw", thrown);
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
function onRecordChanged(listener) {
|
|
5724
|
+
listeners.add(listener);
|
|
5725
|
+
return () => listeners.delete(listener);
|
|
5726
|
+
}
|
|
5727
|
+
|
|
5700
5728
|
// src/core/query.ts
|
|
5701
5729
|
var DEFAULT_PAGE_SIZE = 50;
|
|
5702
5730
|
function planQuery(query) {
|
|
@@ -5738,12 +5766,22 @@ function intersectIds(sets) {
|
|
|
5738
5766
|
|
|
5739
5767
|
// src/core/client.ts
|
|
5740
5768
|
var DEFAULT_SCHEMA = "custom";
|
|
5769
|
+
var A_PERSONS_REFUSAL = /* @__PURE__ */ new Set([
|
|
5770
|
+
"refused_by_rule",
|
|
5771
|
+
"stale_write",
|
|
5772
|
+
"invalid_argument",
|
|
5773
|
+
"already_exists",
|
|
5774
|
+
"missing_reference"
|
|
5775
|
+
]);
|
|
5776
|
+
function isAPersonsRefusal(code) {
|
|
5777
|
+
return A_PERSONS_REFUSAL.has(code);
|
|
5778
|
+
}
|
|
5741
5779
|
function sink(config) {
|
|
5742
|
-
return config.onError ?? ((e) =>
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
console.error(
|
|
5746
|
-
)
|
|
5780
|
+
return config.onError ?? ((e) => {
|
|
5781
|
+
const line = `[@ai-matrx/records] ${e.where}: ${e.code} \u2014 ${e.message}${e.hint ? ` (${e.hint})` : ""}`;
|
|
5782
|
+
if (isAPersonsRefusal(e.code)) console.info(line);
|
|
5783
|
+
else console.error(line);
|
|
5784
|
+
});
|
|
5747
5785
|
}
|
|
5748
5786
|
function createRecordsClient(config) {
|
|
5749
5787
|
const schema = config.schema ?? DEFAULT_SCHEMA;
|
|
@@ -5850,21 +5888,26 @@ function createRecordsClient(config) {
|
|
|
5850
5888
|
},
|
|
5851
5889
|
"recordUpdate"
|
|
5852
5890
|
);
|
|
5891
|
+
if (result.ok) announceRecordChanged(record_id);
|
|
5853
5892
|
return result;
|
|
5854
5893
|
},
|
|
5855
5894
|
async recordDelete({ record_id }) {
|
|
5856
|
-
|
|
5895
|
+
const result = await callDoor(
|
|
5857
5896
|
DOORS.recordDelete,
|
|
5858
5897
|
{ p_organization_id: org, p_record_id: record_id },
|
|
5859
5898
|
"recordDelete"
|
|
5860
5899
|
);
|
|
5900
|
+
if (result.ok) announceRecordChanged(record_id);
|
|
5901
|
+
return result;
|
|
5861
5902
|
},
|
|
5862
5903
|
async recordRestore({ record_id }) {
|
|
5863
|
-
|
|
5904
|
+
const result = await callDoor(
|
|
5864
5905
|
DOORS.recordRestore,
|
|
5865
5906
|
{ p_organization_id: org, p_record_id: record_id },
|
|
5866
5907
|
"recordRestore"
|
|
5867
5908
|
);
|
|
5909
|
+
if (result.ok) announceRecordChanged(record_id);
|
|
5910
|
+
return result;
|
|
5868
5911
|
},
|
|
5869
5912
|
async tableArchive({ table_id, chunk, includeTable }) {
|
|
5870
5913
|
return callDoor(
|
|
@@ -5914,6 +5957,24 @@ function createRecordsClient(config) {
|
|
|
5914
5957
|
})
|
|
5915
5958
|
);
|
|
5916
5959
|
},
|
|
5960
|
+
async tableArchived({ table_id }) {
|
|
5961
|
+
const kernel = await callDoor(DOORS.tableKernelId, {}, "tableArchived.kernel");
|
|
5962
|
+
if (!kernel.ok) return err(kernel.error);
|
|
5963
|
+
const PAGE = 200;
|
|
5964
|
+
for (let page = 0; page < 25; page += 1) {
|
|
5965
|
+
const answered = await this.listArchived({
|
|
5966
|
+
table_id: kernel.data,
|
|
5967
|
+
lane: "org",
|
|
5968
|
+
limit: PAGE,
|
|
5969
|
+
offset: page * PAGE
|
|
5970
|
+
});
|
|
5971
|
+
if (!answered.ok) return err(answered.error);
|
|
5972
|
+
const hit = answered.data.rows.find((row) => row.id === table_id);
|
|
5973
|
+
if (hit) return ok(hit);
|
|
5974
|
+
if (answered.data.total !== null) return ok(null);
|
|
5975
|
+
}
|
|
5976
|
+
return ok(null);
|
|
5977
|
+
},
|
|
5917
5978
|
async listArchived({ table_id, lane = DEFAULT_ARCHIVE_LANE, limit = 50, offset = 0 }) {
|
|
5918
5979
|
const door = await callDoor(
|
|
5919
5980
|
DOORS.readRecordsArchived,
|
|
@@ -6079,6 +6140,23 @@ function createRecordsClient(config) {
|
|
|
6079
6140
|
const { document } = unfold(row.document);
|
|
6080
6141
|
return { ...document, id: row.id, organization_id: org };
|
|
6081
6142
|
});
|
|
6143
|
+
if (doorExists(DOORS.tableFacts)) {
|
|
6144
|
+
const facts = await callDoor(
|
|
6145
|
+
DOORS.tableFacts,
|
|
6146
|
+
{ p_organization_id: org },
|
|
6147
|
+
"tableList.facts"
|
|
6148
|
+
);
|
|
6149
|
+
if (facts.ok) {
|
|
6150
|
+
const byId = new Map((facts.data ?? []).map((f) => [f.table_id, f]));
|
|
6151
|
+
const me = config.actor.user_id ?? null;
|
|
6152
|
+
for (const table of tables) {
|
|
6153
|
+
const fact = byId.get(table.id);
|
|
6154
|
+
if (!fact) continue;
|
|
6155
|
+
table.visibility = fact.visibility;
|
|
6156
|
+
if (fact.mine && me) table.created_by = me;
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6082
6160
|
tables.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
6083
6161
|
return ok(tables);
|
|
6084
6162
|
},
|
|
@@ -6115,9 +6193,11 @@ function createRecordsClient(config) {
|
|
|
6115
6193
|
);
|
|
6116
6194
|
},
|
|
6117
6195
|
async fieldDeclare({ table_id, spec }) {
|
|
6196
|
+
const { allow_other, ...rest } = spec;
|
|
6197
|
+
const p_spec = allow_other === void 0 ? rest : { ...rest, config: { ...rest.config ?? {}, allow_other } };
|
|
6118
6198
|
return callDoor(
|
|
6119
6199
|
DOORS.fieldDeclare,
|
|
6120
|
-
{ p_organization_id: org, p_table_id: table_id, p_spec
|
|
6200
|
+
{ p_organization_id: org, p_table_id: table_id, p_spec },
|
|
6121
6201
|
"fieldDeclare"
|
|
6122
6202
|
);
|
|
6123
6203
|
},
|
|
@@ -7085,14 +7165,16 @@ function createRecordsClient(config) {
|
|
|
7085
7165
|
);
|
|
7086
7166
|
},
|
|
7087
7167
|
async restoreVersion({ record_id, version }) {
|
|
7088
|
-
|
|
7168
|
+
const result = await callDoor(
|
|
7089
7169
|
DOORS.restoreVersion,
|
|
7090
7170
|
{ p_organization_id: org, p_record_id: record_id, p_version: version },
|
|
7091
7171
|
"restoreVersion"
|
|
7092
7172
|
);
|
|
7173
|
+
if (result.ok) announceRecordChanged(record_id);
|
|
7174
|
+
return result;
|
|
7093
7175
|
},
|
|
7094
7176
|
async valueRestore({ record_id, field_key, version }) {
|
|
7095
|
-
|
|
7177
|
+
const answered = await callDoor(
|
|
7096
7178
|
DOORS.valueRestore,
|
|
7097
7179
|
{
|
|
7098
7180
|
p_organization_id: org,
|
|
@@ -7102,6 +7184,8 @@ function createRecordsClient(config) {
|
|
|
7102
7184
|
},
|
|
7103
7185
|
"valueRestore"
|
|
7104
7186
|
);
|
|
7187
|
+
if (answered.ok) announceRecordChanged(record_id);
|
|
7188
|
+
return answered;
|
|
7105
7189
|
},
|
|
7106
7190
|
async commentThread({ record_id, include_resolved }) {
|
|
7107
7191
|
return callDoor(
|
|
@@ -7883,12 +7967,30 @@ function ruleRefusal(field, rule, item, values) {
|
|
|
7883
7967
|
}
|
|
7884
7968
|
return null;
|
|
7885
7969
|
case "length":
|
|
7886
|
-
if (typeof item
|
|
7970
|
+
if (typeof item !== "string") return null;
|
|
7971
|
+
if (rule.value !== void 0 && rule.value !== null && rule.value !== "" && item.length > Number(rule.value)) {
|
|
7887
7972
|
return { ...base, message: `${label} is longer than ${rule.value} characters`, hint: "FLD-3." };
|
|
7888
7973
|
}
|
|
7974
|
+
if (typeof rule.min === "number" && item !== "" && item.length < rule.min) {
|
|
7975
|
+
return {
|
|
7976
|
+
...base,
|
|
7977
|
+
message: `${label} has to be at least ${rule.min} characters long`,
|
|
7978
|
+
hint: "FLD-3.",
|
|
7979
|
+
detail: { field_key: field.key, rule: "length", min: rule.min }
|
|
7980
|
+
};
|
|
7981
|
+
}
|
|
7889
7982
|
return null;
|
|
7890
7983
|
case "pattern":
|
|
7891
7984
|
if (typeof item === "string" && !new RegExp(String(rule.value)).test(item)) {
|
|
7985
|
+
const example = typeof rule.example === "string" ? rule.example.trim() : "";
|
|
7986
|
+
if (example !== "") {
|
|
7987
|
+
return {
|
|
7988
|
+
...base,
|
|
7989
|
+
message: `${label} is not written the way this field expects`,
|
|
7990
|
+
hint: `Enter it like ${example}.`,
|
|
7991
|
+
detail: { field_key: field.key, rule: "pattern", example }
|
|
7992
|
+
};
|
|
7993
|
+
}
|
|
7892
7994
|
return { ...base, message: `${label} is not written the way this field expects`, hint: "FLD-3." };
|
|
7893
7995
|
}
|
|
7894
7996
|
return null;
|
|
@@ -7973,12 +8075,6 @@ function predictValueRefusals(fields, values, recordType) {
|
|
|
7973
8075
|
message: `${label} takes one of its choices, and it was given a ${jsonType(item)}`,
|
|
7974
8076
|
hint: "FLD-5 / FLD-6: a list field stores the id of the option RECORD, because every pick-list is already a Table."
|
|
7975
8077
|
});
|
|
7976
|
-
} else if (!UUID2.test(item)) {
|
|
7977
|
-
found.push({
|
|
7978
|
-
...base,
|
|
7979
|
-
message: `${label} was given a choice that is not one of its choices`,
|
|
7980
|
-
hint: `REC-51: option membership. ${label} stores the id of an option record, and what it was given is not an id at all.`
|
|
7981
|
-
});
|
|
7982
8078
|
}
|
|
7983
8079
|
} else if (field.type === "relation") {
|
|
7984
8080
|
if (typeof item !== "string") {
|