@ai-matrx/records 0.58.117 → 0.58.118

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.
@@ -102,6 +102,25 @@ declare function intersectIds(sets: Uuid[][]): Uuid[];
102
102
  */
103
103
  declare const A_PERSONS_REFUSAL: ReadonlySet<RecordsErrorCode>;
104
104
  declare function isAPersonsRefusal(code: RecordsErrorCode | string): boolean;
105
+ /** One organization the client could not use, with the sentence that says why. */
106
+ interface OrganizationUnavailable {
107
+ organization_id: Uuid;
108
+ reason: string;
109
+ }
110
+ /** `organizationsOpen`: the organizations whose store answers, and those that do not (with why). */
111
+ interface OrganizationsOpenAnswer {
112
+ open: Uuid[];
113
+ unavailable: OrganizationUnavailable[];
114
+ }
115
+ /** `metadataSearch`: every match, each carrying the organization it came from. */
116
+ interface MetadataSearchAnswer {
117
+ matches: Array<Record<string, unknown> & {
118
+ organization_id: Uuid | null;
119
+ }>;
120
+ /** Organizations asked in order (the ones that answered). */
121
+ organizations_covered: Uuid[];
122
+ unavailable: OrganizationUnavailable[];
123
+ }
105
124
  interface RecordsClient {
106
125
  /** The config this client was built with, so a screen can show who it is acting as. */
107
126
  readonly config: RecordsConfig;
@@ -1167,6 +1186,13 @@ interface RecordsClient {
1167
1186
  */
1168
1187
  recordAggregate(args: {
1169
1188
  table_id: Uuid;
1189
+ /**
1190
+ * OPTIONAL FILTER (active-org law): with `config.organizationIds` set, the table is looked
1191
+ * up across all the person's organizations and the aggregate runs in the one that OWNS it
1192
+ * (each row then carries that `organization_id`); this narrows the lookup to one. Absent and
1193
+ * no `organizationIds` ⇒ this client's own organization, exactly as before.
1194
+ */
1195
+ organization_id?: Uuid | null;
1170
1196
  /** Field keys whose values become the groups. */
1171
1197
  groupBy?: string[];
1172
1198
  /** `count` needs no key; every other operation reads one Field. */
@@ -1516,9 +1542,37 @@ interface RecordsClient {
1516
1542
  record_id: Uuid;
1517
1543
  organization_id?: Uuid | null;
1518
1544
  }): Promise<RecordsResult<ReadValue[]>>;
1545
+ /**
1546
+ * Metadata search across EVERY organization the person belongs to (`config.organizationIds`;
1547
+ * `organization_id` narrows to one). Each match carries its own `organization_id`; an
1548
+ * organization whose store is off or refuses is listed in `unavailable` WITH its reason and
1549
+ * never hides the rest. Only when NO organization answers is the whole search a failure (the
1550
+ * first refusal). The door itself is not in the store's catalogue yet, so today every
1551
+ * organization refuses in the door-absent sentence — named, never faked.
1552
+ */
1519
1553
  metadataSearch(args: {
1520
1554
  text: string;
1521
- }): Promise<RecordsResult<never>>;
1555
+ organization_id?: Uuid | null;
1556
+ }): Promise<RecordsResult<MetadataSearchAnswer>>;
1557
+ /**
1558
+ * WHICH of the person's organizations can this client use right now? Asks every organization
1559
+ * in scope (`organization_id` narrows to one) whether its record store is open, so a host can
1560
+ * say "organization X is unavailable: <reason>" instead of silently dropping it. Organization
1561
+ * order is the person's own, never completion order.
1562
+ */
1563
+ organizationsOpen(args?: {
1564
+ organization_id?: Uuid | null;
1565
+ }): Promise<RecordsResult<OrganizationsOpenAnswer>>;
1566
+ /**
1567
+ * The organization that OWNS a table or record — the one a write to it must run in. A
1568
+ * `table_id` is looked up across all the person's organizations; a `record_id` is opened in its
1569
+ * own. `null` = found in none of them. Never a permission: the store's doors still decide.
1570
+ */
1571
+ ownerOrganization(args: {
1572
+ table_id?: Uuid | null;
1573
+ record_id?: Uuid | null;
1574
+ organization_id?: Uuid | null;
1575
+ }): Promise<RecordsResult<Uuid | null>>;
1522
1576
  /**
1523
1577
  * Mint an embed token. `mode` is `read` OR `write` and never both — the store
1524
1578
  * refuses a token carrying two decisions. The secret comes back ONCE, because
@@ -102,6 +102,25 @@ declare function intersectIds(sets: Uuid[][]): Uuid[];
102
102
  */
103
103
  declare const A_PERSONS_REFUSAL: ReadonlySet<RecordsErrorCode>;
104
104
  declare function isAPersonsRefusal(code: RecordsErrorCode | string): boolean;
105
+ /** One organization the client could not use, with the sentence that says why. */
106
+ interface OrganizationUnavailable {
107
+ organization_id: Uuid;
108
+ reason: string;
109
+ }
110
+ /** `organizationsOpen`: the organizations whose store answers, and those that do not (with why). */
111
+ interface OrganizationsOpenAnswer {
112
+ open: Uuid[];
113
+ unavailable: OrganizationUnavailable[];
114
+ }
115
+ /** `metadataSearch`: every match, each carrying the organization it came from. */
116
+ interface MetadataSearchAnswer {
117
+ matches: Array<Record<string, unknown> & {
118
+ organization_id: Uuid | null;
119
+ }>;
120
+ /** Organizations asked in order (the ones that answered). */
121
+ organizations_covered: Uuid[];
122
+ unavailable: OrganizationUnavailable[];
123
+ }
105
124
  interface RecordsClient {
106
125
  /** The config this client was built with, so a screen can show who it is acting as. */
107
126
  readonly config: RecordsConfig;
@@ -1167,6 +1186,13 @@ interface RecordsClient {
1167
1186
  */
1168
1187
  recordAggregate(args: {
1169
1188
  table_id: Uuid;
1189
+ /**
1190
+ * OPTIONAL FILTER (active-org law): with `config.organizationIds` set, the table is looked
1191
+ * up across all the person's organizations and the aggregate runs in the one that OWNS it
1192
+ * (each row then carries that `organization_id`); this narrows the lookup to one. Absent and
1193
+ * no `organizationIds` ⇒ this client's own organization, exactly as before.
1194
+ */
1195
+ organization_id?: Uuid | null;
1170
1196
  /** Field keys whose values become the groups. */
1171
1197
  groupBy?: string[];
1172
1198
  /** `count` needs no key; every other operation reads one Field. */
@@ -1516,9 +1542,37 @@ interface RecordsClient {
1516
1542
  record_id: Uuid;
1517
1543
  organization_id?: Uuid | null;
1518
1544
  }): Promise<RecordsResult<ReadValue[]>>;
1545
+ /**
1546
+ * Metadata search across EVERY organization the person belongs to (`config.organizationIds`;
1547
+ * `organization_id` narrows to one). Each match carries its own `organization_id`; an
1548
+ * organization whose store is off or refuses is listed in `unavailable` WITH its reason and
1549
+ * never hides the rest. Only when NO organization answers is the whole search a failure (the
1550
+ * first refusal). The door itself is not in the store's catalogue yet, so today every
1551
+ * organization refuses in the door-absent sentence — named, never faked.
1552
+ */
1519
1553
  metadataSearch(args: {
1520
1554
  text: string;
1521
- }): Promise<RecordsResult<never>>;
1555
+ organization_id?: Uuid | null;
1556
+ }): Promise<RecordsResult<MetadataSearchAnswer>>;
1557
+ /**
1558
+ * WHICH of the person's organizations can this client use right now? Asks every organization
1559
+ * in scope (`organization_id` narrows to one) whether its record store is open, so a host can
1560
+ * say "organization X is unavailable: <reason>" instead of silently dropping it. Organization
1561
+ * order is the person's own, never completion order.
1562
+ */
1563
+ organizationsOpen(args?: {
1564
+ organization_id?: Uuid | null;
1565
+ }): Promise<RecordsResult<OrganizationsOpenAnswer>>;
1566
+ /**
1567
+ * The organization that OWNS a table or record — the one a write to it must run in. A
1568
+ * `table_id` is looked up across all the person's organizations; a `record_id` is opened in its
1569
+ * own. `null` = found in none of them. Never a permission: the store's doors still decide.
1570
+ */
1571
+ ownerOrganization(args: {
1572
+ table_id?: Uuid | null;
1573
+ record_id?: Uuid | null;
1574
+ organization_id?: Uuid | null;
1575
+ }): Promise<RecordsResult<Uuid | null>>;
1522
1576
  /**
1523
1577
  * Mint an embed token. `mode` is `read` OR `write` and never both — the store
1524
1578
  * refuses a token carrying two decisions. The secret comes back ONCE, because
@@ -40784,6 +40784,13 @@ function createRecordsClient(config) {
40784
40784
  }
40785
40785
  return last;
40786
40786
  }
40787
+ const spans = (filter) => Boolean(filter) || (config.organizationIds?.length ?? 0) > 0;
40788
+ const metadataSearchDoor = () => {
40789
+ const refusal = doorAbsent(DOORS.metadataSearch);
40790
+ scream({ code: refusal.code, message: refusal.message, ...refusal.hint ? { hint: refusal.hint } : {}, where: "metadataSearch" });
40791
+ return err(refusal);
40792
+ };
40793
+ const unavailableReason = (error) => `${error.message}${error.hint ? ` ${error.hint}` : ""}`;
40787
40794
  const client = {
40788
40795
  config,
40789
40796
  async recordWrite({ table_id, data }) {
@@ -42113,7 +42120,23 @@ function createRecordsClient(config) {
42113
42120
  "dashboardDelete"
42114
42121
  );
42115
42122
  },
42116
- async recordAggregate({ table_id, groupBy, measures, bucket, filter, limit, compare }) {
42123
+ async recordAggregate({ table_id, organization_id, groupBy, measures, bucket, filter, limit, compare }) {
42124
+ if (spans(organization_id)) {
42125
+ const owner = await client.ownerOrganization({ table_id, ...organization_id ? { organization_id } : {} });
42126
+ const target = owner.ok && owner.data ? owner.data : organization_id ?? org;
42127
+ if (target !== org) {
42128
+ const answered = await clientFor(target).recordAggregate({
42129
+ table_id,
42130
+ ...groupBy ? { groupBy } : {},
42131
+ ...measures ? { measures } : {},
42132
+ ...bucket !== void 0 ? { bucket } : {},
42133
+ ...filter ? { filter } : {},
42134
+ ...limit !== void 0 ? { limit } : {},
42135
+ ...compare !== void 0 ? { compare } : {}
42136
+ });
42137
+ return answered.ok ? ok(answered.data.map((row) => ({ ...row, organization_id: target }))) : answered;
42138
+ }
42139
+ }
42117
42140
  return callDoor(
42118
42141
  DOORS.recordAggregate,
42119
42142
  {
@@ -43029,10 +43052,80 @@ function createRecordsClient(config) {
43029
43052
  "ruleDeclare"
43030
43053
  );
43031
43054
  },
43032
- async metadataSearch() {
43033
- const refusal = doorAbsent(DOORS.metadataSearch);
43034
- scream({ code: refusal.code, message: refusal.message, ...refusal.hint ? { hint: refusal.hint } : {}, where: "metadataSearch" });
43035
- return err(refusal);
43055
+ async metadataSearch({ text, organization_id }) {
43056
+ if (spans(organization_id) && readOrgs(organization_id).length > 0) {
43057
+ const targets = readOrgs(organization_id).filter((t) => t !== null);
43058
+ const unavailable = [];
43059
+ const answers = await Promise.all(
43060
+ targets.map(async (t) => {
43061
+ const sub = clientFor(t);
43062
+ const open = await sub.storeIsOpen();
43063
+ if (!open.ok) {
43064
+ unavailable.push({ organization_id: t, reason: unavailableReason(open.error) });
43065
+ return null;
43066
+ }
43067
+ if (!open.data) {
43068
+ unavailable.push({ organization_id: t, reason: "The custom data store is switched off for this organization." });
43069
+ return null;
43070
+ }
43071
+ const found = metadataSearchDoor();
43072
+ if (!found.ok) {
43073
+ unavailable.push({ organization_id: t, reason: unavailableReason(found.error) });
43074
+ return { t, found };
43075
+ }
43076
+ return { t, found };
43077
+ })
43078
+ );
43079
+ const matches = [];
43080
+ const covered = [];
43081
+ let firstRefusal = null;
43082
+ for (const answer of answers) {
43083
+ if (!answer) continue;
43084
+ if (!answer.found.ok) {
43085
+ firstRefusal ??= answer.found.error;
43086
+ continue;
43087
+ }
43088
+ covered.push(answer.t);
43089
+ for (const m of answer.found.data.matches) matches.push({ ...m, organization_id: answer.t });
43090
+ }
43091
+ const inOrder = (a, b) => targets.indexOf(a.organization_id) - targets.indexOf(b.organization_id);
43092
+ unavailable.sort(inOrder);
43093
+ if (covered.length === 0 && firstRefusal) return err(firstRefusal);
43094
+ return ok({ matches, organizations_covered: covered, unavailable });
43095
+ }
43096
+ return metadataSearchDoor();
43097
+ },
43098
+ async organizationsOpen(args) {
43099
+ const targets = readOrgs(args?.organization_id).filter((t) => t !== null);
43100
+ const open = [];
43101
+ const unavailable = [];
43102
+ await Promise.all(
43103
+ targets.map(async (t) => {
43104
+ const result = await clientFor(t).storeIsOpen();
43105
+ if (!result.ok) unavailable.push({ organization_id: t, reason: unavailableReason(result.error) });
43106
+ else if (!result.data)
43107
+ unavailable.push({ organization_id: t, reason: "The custom data store is switched off for this organization." });
43108
+ else open.push(t);
43109
+ })
43110
+ );
43111
+ const position = (id) => targets.indexOf(id);
43112
+ open.sort((a, b) => position(a) - position(b));
43113
+ unavailable.sort((a, b) => position(a.organization_id) - position(b.organization_id));
43114
+ return ok({ open, unavailable });
43115
+ },
43116
+ async ownerOrganization({ table_id, record_id, organization_id }) {
43117
+ if (table_id) {
43118
+ const read = await client.tableRead({ table_id, ...organization_id ? { organization_id } : {} });
43119
+ if (!read.ok) return err(read.error);
43120
+ return ok(read.data ? read.data.organization_id ?? null : null);
43121
+ }
43122
+ if (record_id) {
43123
+ const read = await client.recordRead({ record_id, ...organization_id ? { organization_id } : {} });
43124
+ if (!read.ok) return ok(null);
43125
+ const targets = readOrgs(organization_id);
43126
+ return ok(recordOrgs.get(record_id) ?? organization_id ?? (targets.length === 1 ? targets[0] ?? null : null));
43127
+ }
43128
+ return ok(null);
43036
43129
  },
43037
43130
  async fieldPropose() {
43038
43131
  const refusal = doorAbsent(DOORS.fieldPropose);