@ai-matrx/records 0.87.31 → 0.92.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 +98 -0
- package/README.md +115 -0
- package/dist/{client-E0VQ0NPO.d.ts → client-DE9Mq6YX.d.cts} +8 -5
- package/dist/{client-C3ZZNfGH.d.cts → client-vJAb7g9r.d.ts} +8 -5
- package/dist/core/index.cjs +25 -7
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +23 -12
- package/dist/core/index.d.ts +23 -12
- package/dist/core/index.js +25 -7
- package/dist/core/index.js.map +1 -1
- package/dist/data/index.cjs +19 -7
- package/dist/data/index.cjs.map +1 -1
- package/dist/data/index.d.cts +12 -142
- package/dist/data/index.d.ts +12 -142
- package/dist/data/index.js +19 -7
- package/dist/data/index.js.map +1 -1
- package/dist/{declareTable-BK3zufr7.d.cts → declareTable-Btg5bql1.d.cts} +4 -4
- package/dist/{declareTable-B12XybRp.d.ts → declareTable-JKt2q34d.d.ts} +4 -4
- package/dist/entity/index.cjs +984 -0
- package/dist/entity/index.cjs.map +1 -0
- package/dist/entity/index.d.cts +36 -0
- package/dist/entity/index.d.ts +36 -0
- package/dist/entity/index.js +964 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/react.cjs +1024 -0
- package/dist/entity/react.cjs.map +1 -0
- package/dist/entity/react.d.cts +511 -0
- package/dist/entity/react.d.ts +511 -0
- package/dist/entity/react.js +1015 -0
- package/dist/entity/react.js.map +1 -0
- package/dist/{field-BanTVGK2.d.cts → field-BJBnTyJ0.d.cts} +1 -1
- package/dist/{field-BPABOJ2Y.d.ts → field-Ti5iAxFr.d.ts} +1 -1
- package/dist/forms/index.cjs +984 -0
- package/dist/forms/index.cjs.map +1 -0
- package/dist/forms/index.d.cts +333 -0
- package/dist/forms/index.d.ts +333 -0
- package/dist/forms/index.js +961 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index.cjs +10 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -8
- package/dist/index.d.ts +11 -8
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/index.d.cts +7 -6
- package/dist/memory/index.d.ts +7 -6
- package/dist/port-CPVBfDxS.d.cts +301 -0
- package/dist/port-D2i2o7Lg.d.ts +301 -0
- package/dist/portal-Db4CYyHe.d.ts +331 -0
- package/dist/portal-DfeY2Boy.d.cts +331 -0
- package/dist/{ports-TMWX-t9l.d.ts → ports-BWtxAnXh.d.ts} +2 -2
- package/dist/{ports-D8-iuY1P.d.cts → ports-DSU2RsR6.d.cts} +2 -2
- package/dist/react/index.cjs +55 -523
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -1
- package/dist/react/index.d.ts +14 -1
- package/dist/react/index.js +52 -527
- package/dist/react/index.js.map +1 -1
- package/dist/realtime/index.d.cts +2 -2
- package/dist/realtime/index.d.ts +2 -2
- package/dist/{record-DpB2QgAZ.d.cts → record-BQi92d4-.d.cts} +7 -1
- package/dist/{record-DpB2QgAZ.d.ts → record-BQi92d4-.d.ts} +7 -1
- package/dist/relation-DMwkmzgN.d.cts +230 -0
- package/dist/relation-DlcqV_W5.d.ts +230 -0
- package/dist/rule-B8kR0l01.d.ts +250 -0
- package/dist/rule-BMViCLmW.d.cts +250 -0
- package/dist/search/index.cjs +283 -0
- package/dist/search/index.cjs.map +1 -0
- package/dist/search/index.d.cts +44 -0
- package/dist/search/index.d.ts +44 -0
- package/dist/search/index.js +260 -0
- package/dist/search/index.js.map +1 -0
- package/dist/setup/index.cjs +6854 -0
- package/dist/setup/index.cjs.map +1 -0
- package/dist/setup/index.d.cts +87 -0
- package/dist/setup/index.d.ts +87 -0
- package/dist/setup/index.js +6829 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/{subscription-D7KQBHXN.d.ts → subscription-5CLN3Jug.d.ts} +3 -249
- package/dist/{subscription-BD9DnJxX.d.cts → subscription-D-VEzvyy.d.cts} +3 -249
- package/dist/{table-kind-BZu8mtND.d.cts → table-kind-CVp43g6h.d.ts} +3 -558
- package/dist/{table-kind-xKxTHtrV.d.ts → table-kind-Cm6PXkWx.d.cts} +3 -558
- package/dist/templates/index.d.cts +10 -9
- package/dist/templates/index.d.ts +10 -9
- package/dist/typed-table/index.cjs +19 -7
- package/dist/typed-table/index.cjs.map +1 -1
- package/dist/typed-table/index.d.cts +10 -7
- package/dist/typed-table/index.d.ts +10 -7
- package/dist/typed-table/index.js +19 -7
- package/dist/typed-table/index.js.map +1 -1
- package/dist/{types-CLP5H4Wk.d.cts → types-9Q5SHRDJ.d.cts} +2 -2
- package/dist/{types-DmPkWQic.d.ts → types-CfYy-vdV.d.ts} +2 -2
- package/dist/{types-C2uspRtf.d.cts → types-Cs2MyE3u.d.cts} +2 -2
- package/dist/{types-Dv61jStB.d.ts → types-DF-dxB_l.d.ts} +2 -2
- package/dist/types-DTEGaxso.d.cts +139 -0
- package/dist/types-Dji5SpFS.d.ts +139 -0
- package/dist/use-cases/index.d.cts +5 -5
- package/dist/use-cases/index.d.ts +5 -5
- package/dist/{validate-CWVToe1T.d.ts → validate-BydKd-Le.d.ts} +1 -1
- package/dist/{validate-DEB_mn4Y.d.cts → validate-CqJakTKk.d.cts} +1 -1
- package/dist/versions/index.cjs +375 -0
- package/dist/versions/index.cjs.map +1 -0
- package/dist/versions/index.d.cts +160 -0
- package/dist/versions/index.d.ts +160 -0
- package/dist/versions/index.js +353 -0
- package/dist/versions/index.js.map +1 -0
- package/package.json +62 -2
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/entity/react/index.tsx
|
|
5
|
+
import { createContext, createElement, useContext, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { QueryClient, QueryClientContext, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useSeededSource } from "@ai-matrx/records/react";
|
|
8
|
+
import {
|
|
9
|
+
dataKeys,
|
|
10
|
+
hostEvents,
|
|
11
|
+
useRecordsQueryClient,
|
|
12
|
+
useDataColumns,
|
|
13
|
+
useDataMutations,
|
|
14
|
+
useDataRow,
|
|
15
|
+
useDataRows
|
|
16
|
+
} from "@ai-matrx/records/data/react";
|
|
17
|
+
|
|
18
|
+
// src/core/supabase.ts
|
|
19
|
+
function refuse(code, message, hint) {
|
|
20
|
+
return { data: null, error: { code, message, hint, details: "" } };
|
|
21
|
+
}
|
|
22
|
+
function supabaseDataSource(client) {
|
|
23
|
+
if (isBound(client)) return client;
|
|
24
|
+
const supabase = client;
|
|
25
|
+
const hasSchema = typeof supabase.schema === "function";
|
|
26
|
+
const bound = {
|
|
27
|
+
// PAGE-BUNDLE: the real adapter opens a table page with one `custom.table_page_bundle` call.
|
|
28
|
+
opensInOneCall: true,
|
|
29
|
+
rpc(fn, args, options) {
|
|
30
|
+
if (!hasSchema) {
|
|
31
|
+
return Promise.resolve(
|
|
32
|
+
refuse(
|
|
33
|
+
"data_source_unbound",
|
|
34
|
+
`The records client was given something that is not a supabase client: it has no schema() method, so the door ${fn} cannot be called in the schema it lives in.`,
|
|
35
|
+
"Pass a supabase-js v2 client (createClient(...)) to supabaseDataSource()."
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const name = options?.schema;
|
|
40
|
+
if (!name) {
|
|
41
|
+
return Promise.resolve(
|
|
42
|
+
refuse(
|
|
43
|
+
"schema_unsaid",
|
|
44
|
+
`The door ${fn} was called without saying which schema it lives in. Every door of this store is custom.${fn} and every trust door is iam.${fn}; an unqualified call reaches PostgREST's default profile (public) and is refused there.`,
|
|
45
|
+
"This is a bug in the caller, not in your data: /core says the schema on every call."
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return supabase.schema(name).rpc(fn, args);
|
|
50
|
+
},
|
|
51
|
+
schema(name) {
|
|
52
|
+
return supabase.schema(name);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
Object.defineProperty(bound, BOUND, { value: true, enumerable: false });
|
|
56
|
+
return bound;
|
|
57
|
+
}
|
|
58
|
+
var BOUND = /* @__PURE__ */ Symbol.for("@ai-matrx/records/supabaseDataSource");
|
|
59
|
+
function isBound(source) {
|
|
60
|
+
return source[BOUND] === true;
|
|
61
|
+
}
|
|
62
|
+
function asRecordsDataSource(source) {
|
|
63
|
+
if (source === null || typeof source !== "object") return supabaseDataSource({});
|
|
64
|
+
if (isBound(source)) return source;
|
|
65
|
+
const candidate = source;
|
|
66
|
+
const looksLikeSupabaseJs = typeof candidate.schema === "function" && typeof candidate.from === "function" && candidate.auth !== null && typeof candidate.auth === "object";
|
|
67
|
+
if (looksLikeSupabaseJs || typeof candidate.rpc !== "function") return supabaseDataSource(source);
|
|
68
|
+
return source;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/core/pgError.ts
|
|
72
|
+
var BY_SQLSTATE = {
|
|
73
|
+
PT409: "stale_write",
|
|
74
|
+
"42501": "door",
|
|
75
|
+
"23514": "refused_by_rule",
|
|
76
|
+
"22004": "invalid_argument",
|
|
77
|
+
"22023": "invalid_argument",
|
|
78
|
+
// MX005 — a value the store cannot take as asked: since 2026-10-06 (AP-3 U3) a word that names more than one
|
|
79
|
+
// choice or linked record raises it (it used to borrow PT409 and read as somebody else's write winning).
|
|
80
|
+
MX005: "invalid_argument",
|
|
81
|
+
// MXD01 — the held-change door tried the proposed values before filing an
|
|
82
|
+
// approval request and the store refused them. Nothing is waiting for a
|
|
83
|
+
// person to decide; correct the value before asking again, which is the same
|
|
84
|
+
// caller-side condition as the other invalid-argument refusals.
|
|
85
|
+
MXD01: "invalid_argument",
|
|
86
|
+
// 22P02 — a value does not fit the type the store holds for it. The class is
|
|
87
|
+
// real and was hit live: an organization whose copy of a package table grew
|
|
88
|
+
// `status` as a uuid-valued Field was sent the text "open", and the store
|
|
89
|
+
// answered `invalid input syntax for type uuid: "open"`. It is the CALL that
|
|
90
|
+
// is wrong for the shape the store holds, which is what `invalid_argument`
|
|
91
|
+
// already means — the store's own sentence names the value and the type.
|
|
92
|
+
"22P02": "invalid_argument",
|
|
93
|
+
// 42703 — a column that is not there. The store raises it from
|
|
94
|
+
// `custom.entity_table`, which is asked for a column by NAME, so the class is
|
|
95
|
+
// exactly "the call named something the shape does not have": the same thing
|
|
96
|
+
// `invalid_argument` already means, and the store's own sentence names the
|
|
97
|
+
// column. Added 2026-09-20 (lane FORMS) because the package's live-door suite
|
|
98
|
+
// caught it raised by a door this package did not know about — which is the
|
|
99
|
+
// suite working, and is why the map is a census rather than a guess.
|
|
100
|
+
"42703": "invalid_argument",
|
|
101
|
+
// 22007 — a date or a time the store could not read as one. It is raised by
|
|
102
|
+
// `custom.dashboard_window_sql`, which is handed a window a caller wrote
|
|
103
|
+
// ("last 30 days", a literal date), so the class is again "the call named
|
|
104
|
+
// something the shape does not accept" and the store's own sentence names the
|
|
105
|
+
// text it could not read. Added 2026-09-20 (lane SCREEN-BREAK) after the
|
|
106
|
+
// live-door census caught it — the same way 42703 arrived, which is the
|
|
107
|
+
// census working rather than a guess.
|
|
108
|
+
"22007": "invalid_argument",
|
|
109
|
+
// 22003 — a number the store could not hold at the size it was handed. It is
|
|
110
|
+
// raised by `custom.context_resolve`, which is given a window or a depth a
|
|
111
|
+
// caller wrote, so the class is the same one 22P02, 42703 and 22007 already
|
|
112
|
+
// are: the CALL named a value the shape does not accept, and the store's own
|
|
113
|
+
// sentence names the number and the range. Added 2026-09-20 (lane GRID-TWO)
|
|
114
|
+
// when the live-door census caught the release gate red on it — which is the
|
|
115
|
+
// census working, and the fourth time it has been the thing that found a new
|
|
116
|
+
// refusal class rather than a person guessing one.
|
|
117
|
+
"22003": "invalid_argument",
|
|
118
|
+
// 42883 — `custom._template_call` (the template family, 2026-10-03) was handed a step that
|
|
119
|
+
// names a door the store does not have, or an argument that door does not take. The TEMPLATE
|
|
120
|
+
// named something the shape does not have — the same class 42703 is — and the store's own
|
|
121
|
+
// sentence names the door or the argument. Caught by the live-door census on records 0.68.1's
|
|
122
|
+
// tag, the eighth time the census found a class before a person did. (A bare 42883 on a door
|
|
123
|
+
// this package itself asks for is still `door_absent`, mapped before this funnel in
|
|
124
|
+
// `owedDoor`.)
|
|
125
|
+
"42883": "invalid_argument",
|
|
126
|
+
"02000": "not_found",
|
|
127
|
+
// P0002 — `custom.inbox_snooze` / `custom.inbox_clear` (lane S5-PRIME-2, 2026-09-24): "There is
|
|
128
|
+
// nothing in your inbox with that id". An invented id, another person's item and another
|
|
129
|
+
// organization's all answer it alike, so it reads exactly as 02000 does: not found.
|
|
130
|
+
"P0002": "not_found",
|
|
131
|
+
"23503": "missing_reference",
|
|
132
|
+
"23505": "already_exists",
|
|
133
|
+
"0A000": "not_supported",
|
|
134
|
+
"53400": "store_limit",
|
|
135
|
+
"22012": "store_limit",
|
|
136
|
+
// 54001 — `custom.record_delete` stops a cascade that nests past sixty-four
|
|
137
|
+
// levels of containment, because that is a loop somebody built rather than a
|
|
138
|
+
// hierarchy, and saying so beats recursing until the server runs out of
|
|
139
|
+
// stack. It is a LIMIT of the store, and the store's own sentence names what
|
|
140
|
+
// to do about it, so it reads as one.
|
|
141
|
+
"54001": "store_limit",
|
|
142
|
+
// 54000 — the grid primitives' organization ceilings (GRID-PRIMITIVES, 2026-09-23):
|
|
143
|
+
// `custom.action_run` on a selection past its knob, `custom.table_decorate` past the
|
|
144
|
+
// rule count, `custom.table_from_example` past `custom/example_rows_max`,
|
|
145
|
+
// `custom.table_webhook_declare` past `custom/table_webhooks_max`, and
|
|
146
|
+
// `custom.hub_changed_by`. Each names its knob in the hint and writes nothing, so it
|
|
147
|
+
// reads exactly like 54001: a limit of the store, in the store's own sentence.
|
|
148
|
+
"54000": "store_limit",
|
|
149
|
+
// VIS: `custom.has_visibility_at` refuses to answer from a snapshot older
|
|
150
|
+
// than a write this reader has already seen, rather than returning something
|
|
151
|
+
// they just lost access to. It is retryable on a fresher connection, which is
|
|
152
|
+
// a different instruction from every other refusal here.
|
|
153
|
+
// 55000 — `custom.table_move` (SC-1', 2026-09-24) refuses a move the table's present state
|
|
154
|
+
// does not allow: a destination that cannot take it, a table something else holds, contents
|
|
155
|
+
// that point at themselves in a circle. The store's own sentence names the reason and nothing
|
|
156
|
+
// moves, so it reads as a refusal on the merits. Caught by the live-door census on records
|
|
157
|
+
// 0.58.3's tag, the seventh time the census found a class before a person did.
|
|
158
|
+
"55000": "refused_by_rule",
|
|
159
|
+
// 42P17 — a worked-out column that would read itself round a circle (STORE-TAILS-3,
|
|
160
|
+
// 2026-09-25): `custom._field_reads_what_it_reads` refuses the declaration or edit that would
|
|
161
|
+
// close the circle, naming every column in it, and `custom.far_value` refuses a circle stored
|
|
162
|
+
// before that rule when a lookup or rollup is worked out. Refused on its merits, in the store's
|
|
163
|
+
// own sentence, nothing written — so it reads as 55000 does.
|
|
164
|
+
"42P17": "refused_by_rule",
|
|
165
|
+
// P0001 — `custom.context_scope_write` (2026-10-03) refuses a scope whose parent breaks the
|
|
166
|
+
// parent rule in the old trigger's own class and words (manager ruling H6). Refused on its
|
|
167
|
+
// merits, nothing written. Caught by the live-door census on records 0.65.0's tag.
|
|
168
|
+
P0001: "refused_by_rule",
|
|
169
|
+
"40001": "stale_read",
|
|
170
|
+
// 57014 — the store cancelled the statement at its own timeout. Like
|
|
171
|
+
// `stale_read` and unlike every other refusal here, the call was never
|
|
172
|
+
// refused on its merits: the same call on a smaller page, or once the
|
|
173
|
+
// organization's kernel is not being rewritten, can succeed. It gets its own
|
|
174
|
+
// code so a screen can offer "try again" honestly instead of showing a
|
|
175
|
+
// failure sentence the caller cannot act on.
|
|
176
|
+
"57014": "timed_out",
|
|
177
|
+
// PostgREST's own two: the store is not being SERVED, which is a different
|
|
178
|
+
// thing from the store refusing. `custom` absent from `pgrst.db_schemas`, or
|
|
179
|
+
// the grants not copied, land here.
|
|
180
|
+
PGRST202: "store_not_served",
|
|
181
|
+
PGRST205: "store_not_served",
|
|
182
|
+
PGRST301: "door",
|
|
183
|
+
// PT428 — NOT A FAILURE, and the release gate's live census is what found it
|
|
184
|
+
// missing: the store raises it and this package answered `internal`, which is
|
|
185
|
+
// the one classification that tells a screen nothing. A stage gate whose
|
|
186
|
+
// `on_fail` is `require_approval` files the request and leaves the record
|
|
187
|
+
// where it was; the transaction is rolled back to the pipeline's own block, so
|
|
188
|
+
// "nothing was written" is a fact rather than a promise. Landed by lane
|
|
189
|
+
// STAGE-RULES; mapped here by STAGE-RULES-2 the day after, when the census
|
|
190
|
+
// caught it — the sixth time that census has been the thing that found a
|
|
191
|
+
// refusal class rather than a person guessing one.
|
|
192
|
+
PT428: "needs_approval"
|
|
193
|
+
};
|
|
194
|
+
var MAPPED_SQLSTATES = Object.keys(BY_SQLSTATE);
|
|
195
|
+
function parseDetail(details) {
|
|
196
|
+
if (!details) return void 0;
|
|
197
|
+
const trimmed = details.trim();
|
|
198
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return details;
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(trimmed);
|
|
201
|
+
} catch {
|
|
202
|
+
return details;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function isNotASqlstate(code) {
|
|
206
|
+
if (!code) return true;
|
|
207
|
+
return /^[0-9]{3}$/.test(code);
|
|
208
|
+
}
|
|
209
|
+
function mapPgError(error, where) {
|
|
210
|
+
const raw = error.code ?? void 0;
|
|
211
|
+
const sqlstate = raw && raw !== "" ? raw : void 0;
|
|
212
|
+
const mapped = {
|
|
213
|
+
code: "internal",
|
|
214
|
+
message: error.message,
|
|
215
|
+
...error.hint ? { hint: error.hint } : {},
|
|
216
|
+
...sqlstate ? { sqlstate } : {}
|
|
217
|
+
};
|
|
218
|
+
if (isNotASqlstate(sqlstate)) {
|
|
219
|
+
mapped.code = "unreachable";
|
|
220
|
+
} else {
|
|
221
|
+
mapped.code = BY_SQLSTATE[sqlstate] ?? "internal";
|
|
222
|
+
}
|
|
223
|
+
const detail = parseDetail(error.details);
|
|
224
|
+
if (detail !== void 0) mapped.detail = detail;
|
|
225
|
+
if (mapped.code === "internal") {
|
|
226
|
+
mapped.unmapped = true;
|
|
227
|
+
mapped.diagnostic = `@ai-matrx/records did not recognise SQLSTATE ${sqlstate ?? "(none)"} from ${where}; the store's own message is the thing to act on.`;
|
|
228
|
+
} else if (mapped.code === "unreachable") {
|
|
229
|
+
mapped.unmapped = true;
|
|
230
|
+
mapped.diagnostic = `@ai-matrx/records never reached the store from ${where}: ${raw ? `the transport answered ${raw}` : "no response"} \u2014 ${error.message}`;
|
|
231
|
+
mapped.message = "We could not reach your data.";
|
|
232
|
+
delete mapped.hint;
|
|
233
|
+
}
|
|
234
|
+
return mapped;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/entity/core/errors.ts
|
|
238
|
+
var ENTITY_REFUSALS = {
|
|
239
|
+
/** conflict_version: somebody else's write won. */
|
|
240
|
+
MX001: "stale_write",
|
|
241
|
+
/** organization_required: the call needs an organization and carried none. */
|
|
242
|
+
MX002: "organization_required",
|
|
243
|
+
/** forbidden. */
|
|
244
|
+
MX003: "door",
|
|
245
|
+
/** not_found (also: not visible to this person). */
|
|
246
|
+
MX004: "not_found",
|
|
247
|
+
/** validation_refused (detail.reason required / bad_value / ambiguous_word / constraint; `not_supported` is read apart below). */
|
|
248
|
+
MX005: "invalid_argument",
|
|
249
|
+
/** system_column: a write named a column the store owns. */
|
|
250
|
+
MX008: "invalid_argument",
|
|
251
|
+
/** unknown_identifier: a column that is not there or not usable here. */
|
|
252
|
+
MX012: "invalid_argument",
|
|
253
|
+
/** cursor_invalid: the page cursor belongs to another list; the engine restarts the page. */
|
|
254
|
+
MX013: "invalid_argument",
|
|
255
|
+
/** view_not_offered. */
|
|
256
|
+
MX015: "invalid_argument",
|
|
257
|
+
/** invalid_argument: a parameter the door refuses by name. */
|
|
258
|
+
MX016: "invalid_argument"
|
|
259
|
+
};
|
|
260
|
+
function mapEntityError(error, where) {
|
|
261
|
+
const mapped = mapPgError(error, where);
|
|
262
|
+
const state = mapped.sqlstate;
|
|
263
|
+
if (state && /^MX\d{3}$/.test(state)) {
|
|
264
|
+
const code = ENTITY_REFUSALS[state];
|
|
265
|
+
if (code) {
|
|
266
|
+
const reason = mapped.detail?.reason;
|
|
267
|
+
const known = { ...mapped, code: state === "MX005" && reason === "not_supported" ? "not_supported" : code };
|
|
268
|
+
delete known.unmapped;
|
|
269
|
+
delete known.diagnostic;
|
|
270
|
+
return known;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return mapped;
|
|
274
|
+
}
|
|
275
|
+
function refusal(code, message, hint) {
|
|
276
|
+
return { ok: false, error: { code, message, ...hint ? { hint } : {} } };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/entity/core/client.ts
|
|
280
|
+
var OP_ID_HEADER = "x-matrx-op-id";
|
|
281
|
+
var ENTITY_DOOR_SCHEMA = "platform";
|
|
282
|
+
function createEntityClient(options) {
|
|
283
|
+
const source = asRecordsDataSource(options.dataSource ?? options.supabase);
|
|
284
|
+
let warnedHeaderless = false;
|
|
285
|
+
function withHeaders(pending, headers) {
|
|
286
|
+
if (!headers) return;
|
|
287
|
+
const builder = pending;
|
|
288
|
+
if (typeof builder.setHeader === "function") {
|
|
289
|
+
for (const [name, value] of Object.entries(headers)) builder.setHeader(name, value);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!warnedHeaderless) {
|
|
293
|
+
warnedHeaderless = true;
|
|
294
|
+
console.warn(
|
|
295
|
+
`[@ai-matrx/records/entity] This data source cannot carry a per-call header (${Object.keys(headers).join(", ")}), so this browser re-reads the notice of its own writes once.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function call(fn, args, schema = ENTITY_DOOR_SCHEMA, headers) {
|
|
300
|
+
try {
|
|
301
|
+
const pending = source.rpc(fn, args, { schema });
|
|
302
|
+
withHeaders(pending, headers);
|
|
303
|
+
const answer = await pending;
|
|
304
|
+
if (answer.error) return { ok: false, error: mapEntityError(answer.error, `${schema}.${fn}`) };
|
|
305
|
+
return { ok: true, data: answer.data };
|
|
306
|
+
} catch (thrown) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: mapEntityError({ code: "", message: thrown instanceof Error ? thrown.message : String(thrown) }, `${schema}.${fn}`)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
list(request) {
|
|
315
|
+
const args = {
|
|
316
|
+
p_entity_type: request.token,
|
|
317
|
+
p_scope: request.scope ?? null,
|
|
318
|
+
p_filter: request.filter ?? {},
|
|
319
|
+
p_search: request.search ?? null,
|
|
320
|
+
p_sort: request.sort ?? null,
|
|
321
|
+
p_archived: request.archived ?? "active",
|
|
322
|
+
p_page_size: request.pageSize ?? null,
|
|
323
|
+
p_after: request.after ?? null,
|
|
324
|
+
p_with_total: request.withTotal ?? true
|
|
325
|
+
};
|
|
326
|
+
return call("entity_list_scoped", args);
|
|
327
|
+
},
|
|
328
|
+
get(request) {
|
|
329
|
+
return call("entity_get", { p_entity_type: request.token, p_ids: [...request.ids] });
|
|
330
|
+
},
|
|
331
|
+
columns(request) {
|
|
332
|
+
return call("entity_columns", { p_organization_id: request.organizationId ?? null, p_token: request.token });
|
|
333
|
+
},
|
|
334
|
+
insert(request) {
|
|
335
|
+
return call(
|
|
336
|
+
"entity_insert",
|
|
337
|
+
{ p_entity_type: request.token, p_organization_id: request.organizationId, p_rows: request.rows.map((r) => ({ ...r })) },
|
|
338
|
+
ENTITY_DOOR_SCHEMA,
|
|
339
|
+
opHeader(request.opId)
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
update(request) {
|
|
343
|
+
return call(
|
|
344
|
+
"entity_update",
|
|
345
|
+
{ p_entity_type: request.token, p_id: request.id, p_base_version: request.baseVersion, p_patch: request.patch },
|
|
346
|
+
ENTITY_DOOR_SCHEMA,
|
|
347
|
+
opHeader(request.opId)
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
archive(request) {
|
|
351
|
+
return call("entity_archive", { p_entity_type: request.token, p_ids: [...request.ids] }, ENTITY_DOOR_SCHEMA, opHeader(request.opId));
|
|
352
|
+
},
|
|
353
|
+
unarchive(request) {
|
|
354
|
+
return call("entity_unarchive", { p_entity_type: request.token, p_ids: [...request.ids] }, ENTITY_DOOR_SCHEMA, opHeader(request.opId));
|
|
355
|
+
},
|
|
356
|
+
knob(request) {
|
|
357
|
+
return call("knob_resolve", { p_feature: "entity_data", p_key: request.key, p_organization_id: request.organizationId ?? null });
|
|
358
|
+
},
|
|
359
|
+
async memberOrganizations() {
|
|
360
|
+
const answer = await call("my_orgs", {}, "iam");
|
|
361
|
+
if (!answer.ok) return answer;
|
|
362
|
+
const rows = Array.isArray(answer.data) ? answer.data : [];
|
|
363
|
+
const ids = rows.map((row) => typeof row === "string" ? row : row && typeof row === "object" ? Object.values(row)[0] : null).filter((id) => typeof id === "string");
|
|
364
|
+
return { ok: true, data: ids };
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function opHeader(opId) {
|
|
369
|
+
return opId ? { [OP_ID_HEADER]: opId } : void 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/entity/core/realtime.ts
|
|
373
|
+
import {
|
|
374
|
+
defineChannelNamespace,
|
|
375
|
+
subscribeToRealtimeManager
|
|
376
|
+
} from "@ai-matrx/realtime";
|
|
377
|
+
|
|
378
|
+
// src/ops.ts
|
|
379
|
+
import { globalSlot } from "@ai-matrx/kit/global-slot";
|
|
380
|
+
var REMEMBER_MS = 3e4;
|
|
381
|
+
var REMEMBER_MAX = 500;
|
|
382
|
+
var ours = globalSlot("ai-matrx.records.op-ledger", () => /* @__PURE__ */ new Map());
|
|
383
|
+
function trim(now) {
|
|
384
|
+
for (const [id, at] of ours) {
|
|
385
|
+
if (now - at <= REMEMBER_MS) break;
|
|
386
|
+
ours.delete(id);
|
|
387
|
+
}
|
|
388
|
+
while (ours.size > REMEMBER_MAX) {
|
|
389
|
+
const oldest = ours.keys().next();
|
|
390
|
+
if (oldest.done === true) break;
|
|
391
|
+
ours.delete(oldest.value);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function mintOpId() {
|
|
395
|
+
const id = crypto.randomUUID();
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
ours.set(id, now);
|
|
398
|
+
trim(now);
|
|
399
|
+
return id;
|
|
400
|
+
}
|
|
401
|
+
function isOwnOp(opId) {
|
|
402
|
+
if (!opId) return false;
|
|
403
|
+
const at = ours.get(opId);
|
|
404
|
+
if (at === void 0) return false;
|
|
405
|
+
if (Date.now() - at > REMEMBER_MS) {
|
|
406
|
+
ours.delete(opId);
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/entity/core/realtime.ts
|
|
413
|
+
var entityOrgChannel = defineChannelNamespace({
|
|
414
|
+
namespace: "entity-org",
|
|
415
|
+
foreignTopic: "mx:entity-org",
|
|
416
|
+
parts: ["organizationId", "token"],
|
|
417
|
+
description: "One organization's changes to one platform type (contacts, deals, projects, tasks, employees). Carries NOTICES written by platform.entity_org_notice_stmt \u2014 which ids moved, never a value. Admitted by the one RLS policy on realtime.messages to members of that organization."
|
|
418
|
+
});
|
|
419
|
+
var ENTITY_NOTICE_EVENT = "entity_bump";
|
|
420
|
+
var DEFAULT_BATCH_MS = 150;
|
|
421
|
+
var DEFAULT_MAX_WAIT_MS = 1e3;
|
|
422
|
+
var DEFAULT_MAX_ORG_TOPICS = 20;
|
|
423
|
+
function entityNoticeKey(notice) {
|
|
424
|
+
return notice.id ?? (notice.at ? `${notice.organization_id ?? ""}:${notice.entity_type ?? ""}:${notice.at}` : void 0);
|
|
425
|
+
}
|
|
426
|
+
function entityOrgTopic(organizationId, token) {
|
|
427
|
+
return entityOrgChannel.topic({ organizationId, token });
|
|
428
|
+
}
|
|
429
|
+
function entityOrgChannelSpec(organizationId, token, onNotice, onBackfill) {
|
|
430
|
+
return {
|
|
431
|
+
topic: entityOrgTopic(organizationId, token),
|
|
432
|
+
// Database Broadcast is authorized by RLS on realtime.messages; the manager awaits setAuth() first.
|
|
433
|
+
private: true,
|
|
434
|
+
// The sender is Postgres: no Matrx envelope. Our own write is known by the op id it carried.
|
|
435
|
+
wire: { mode: "raw", isOwnMessage: (payload) => isOwnOp(payload?.op_id) },
|
|
436
|
+
eventKey: (_source, payload) => entityNoticeKey(payload ?? {}),
|
|
437
|
+
broadcast: [
|
|
438
|
+
{
|
|
439
|
+
event: ENTITY_NOTICE_EVENT,
|
|
440
|
+
onMessage: (message) => {
|
|
441
|
+
const notice = message.data ?? {};
|
|
442
|
+
if (isOwnOp(notice.op_id)) return;
|
|
443
|
+
onNotice(notice);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
],
|
|
447
|
+
// Realtime has no replay: a channel back from a gap re-reads.
|
|
448
|
+
onBackfill: () => onBackfill()
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function orderOrganizationTopics(input) {
|
|
452
|
+
const members = input.members ? new Set(input.members) : null;
|
|
453
|
+
const out = [];
|
|
454
|
+
const push = (org) => {
|
|
455
|
+
if (!org || out.includes(org)) return;
|
|
456
|
+
if (members && !members.has(org)) return;
|
|
457
|
+
out.push(org);
|
|
458
|
+
};
|
|
459
|
+
push(input.active);
|
|
460
|
+
push(input.filter);
|
|
461
|
+
const held = [...(input.held ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
462
|
+
for (const [org] of held) push(org);
|
|
463
|
+
for (const org of [...input.members ?? []].sort()) push(org);
|
|
464
|
+
return out.slice(0, Math.max(0, input.max));
|
|
465
|
+
}
|
|
466
|
+
function createEntityRealtime(options = {}) {
|
|
467
|
+
const batchMs = options.batchMs ?? DEFAULT_BATCH_MS;
|
|
468
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
469
|
+
const channels = /* @__PURE__ */ new Map();
|
|
470
|
+
const open = (organizationId, token) => {
|
|
471
|
+
const channel = { listeners: /* @__PURE__ */ new Set(), stop: null };
|
|
472
|
+
const spec = entityOrgChannelSpec(
|
|
473
|
+
organizationId,
|
|
474
|
+
token,
|
|
475
|
+
(notice) => {
|
|
476
|
+
for (const listener of channel.listeners) listener.notice(notice);
|
|
477
|
+
},
|
|
478
|
+
() => {
|
|
479
|
+
for (const listener of channel.listeners) listener.backfill();
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
if (options.manager) {
|
|
483
|
+
const handle = options.manager.open(spec);
|
|
484
|
+
channel.stop = () => handle.close();
|
|
485
|
+
} else {
|
|
486
|
+
channel.stop = subscribeToRealtimeManager(() => spec);
|
|
487
|
+
}
|
|
488
|
+
return channel;
|
|
489
|
+
};
|
|
490
|
+
return {
|
|
491
|
+
subscribe({ token, organizationIds }, sink) {
|
|
492
|
+
let timer = null;
|
|
493
|
+
let firstAt = 0;
|
|
494
|
+
let pending = /* @__PURE__ */ new Set();
|
|
495
|
+
let wholeList = false;
|
|
496
|
+
const flush = () => {
|
|
497
|
+
if (timer !== null) clearTimeout(timer);
|
|
498
|
+
timer = null;
|
|
499
|
+
const ids = [...pending];
|
|
500
|
+
const all = wholeList;
|
|
501
|
+
pending = /* @__PURE__ */ new Set();
|
|
502
|
+
wholeList = false;
|
|
503
|
+
if (all) sink.rows(null);
|
|
504
|
+
else if (ids.length > 0) sink.rows(ids);
|
|
505
|
+
};
|
|
506
|
+
const nudge = () => {
|
|
507
|
+
const now = Date.now();
|
|
508
|
+
if (timer === null) firstAt = now;
|
|
509
|
+
else clearTimeout(timer);
|
|
510
|
+
const wait = Math.max(0, Math.min(batchMs, firstAt + maxWaitMs - now));
|
|
511
|
+
timer = setTimeout(flush, wait);
|
|
512
|
+
};
|
|
513
|
+
const listener = {
|
|
514
|
+
notice(notice) {
|
|
515
|
+
if (notice.entity_type && notice.entity_type !== token) return;
|
|
516
|
+
if (notice.ids == null) wholeList = true;
|
|
517
|
+
else for (const id of notice.ids) pending.add(id);
|
|
518
|
+
nudge();
|
|
519
|
+
},
|
|
520
|
+
backfill() {
|
|
521
|
+
wholeList = true;
|
|
522
|
+
flush();
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const keys = [...new Set(organizationIds)].map((org) => {
|
|
526
|
+
const key = entityOrgTopic(org, token);
|
|
527
|
+
const channel = channels.get(key) ?? open(org, token);
|
|
528
|
+
channels.set(key, channel);
|
|
529
|
+
channel.listeners.add(listener);
|
|
530
|
+
return key;
|
|
531
|
+
});
|
|
532
|
+
return () => {
|
|
533
|
+
if (timer !== null) clearTimeout(timer);
|
|
534
|
+
for (const key of keys) {
|
|
535
|
+
const channel = channels.get(key);
|
|
536
|
+
if (!channel) continue;
|
|
537
|
+
channel.listeners.delete(listener);
|
|
538
|
+
if (channel.listeners.size > 0) continue;
|
|
539
|
+
channel.stop?.();
|
|
540
|
+
channels.delete(key);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/entity/port/port.ts
|
|
548
|
+
import { isUuidShape } from "@ai-matrx/kit/uuid";
|
|
549
|
+
|
|
550
|
+
// src/entity/core/wire.ts
|
|
551
|
+
var IDENTITY = {
|
|
552
|
+
_id: "id",
|
|
553
|
+
_version: "version",
|
|
554
|
+
_organization_id: "organization_id"
|
|
555
|
+
};
|
|
556
|
+
var CUSTOM_PREFIX = "_custom.";
|
|
557
|
+
function wireColumn(column, customKeys) {
|
|
558
|
+
const identity = IDENTITY[column];
|
|
559
|
+
if (identity) return identity;
|
|
560
|
+
if (column.startsWith(CUSTOM_PREFIX)) return column;
|
|
561
|
+
return customKeys.has(column) ? `${CUSTOM_PREFIX}${column}` : column;
|
|
562
|
+
}
|
|
563
|
+
function predicate(clause, customKeys) {
|
|
564
|
+
const column = wireColumn(clause.column, customKeys);
|
|
565
|
+
const v = clause.value;
|
|
566
|
+
switch (clause.op) {
|
|
567
|
+
case "eq":
|
|
568
|
+
return v === null || v === void 0 ? { column, op: "empty" } : { column, op: "eq", value: v };
|
|
569
|
+
case "neq":
|
|
570
|
+
return v === null || v === void 0 ? { column, op: "empty", negated: true } : { column, op: "neq", value: v };
|
|
571
|
+
case "lt":
|
|
572
|
+
case "lte":
|
|
573
|
+
case "gt":
|
|
574
|
+
case "gte":
|
|
575
|
+
return { column, op: clause.op, value: v };
|
|
576
|
+
case "in":
|
|
577
|
+
return { column, op: "in", values: Array.isArray(v) ? v : [] };
|
|
578
|
+
case "empty":
|
|
579
|
+
return v === false ? { column, op: "empty", negated: true } : { column, op: "empty" };
|
|
580
|
+
case "contains":
|
|
581
|
+
return { column, op: "contains", value: v };
|
|
582
|
+
default:
|
|
583
|
+
return { column, op: String(clause.op), value: v };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function toWire(query, customKeys = /* @__PURE__ */ new Set()) {
|
|
587
|
+
const q = query ?? {};
|
|
588
|
+
const out = {};
|
|
589
|
+
const scope = {};
|
|
590
|
+
if (q.lane) scope.kind = q.lane;
|
|
591
|
+
if (q.org && q.org !== "all") scope.organization_id = q.org;
|
|
592
|
+
if (scope.kind || scope.organization_id) out.scope = scope;
|
|
593
|
+
if (q.where && q.where.length > 0) out.filter = { all: q.where.map((c) => predicate(c, customKeys)) };
|
|
594
|
+
const search = q.search?.trim();
|
|
595
|
+
if (search) out.search = search;
|
|
596
|
+
if (q.sort && q.sort.length > 0) {
|
|
597
|
+
out.sort = q.sort.map((s) => ({
|
|
598
|
+
column: wireColumn(s.column, customKeys),
|
|
599
|
+
dir: s.direction === "desc" ? "desc" : "asc",
|
|
600
|
+
nulls: "last"
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
if (typeof q.pageSize === "number" && Number.isFinite(q.pageSize)) out.pageSize = Math.max(1, Math.floor(q.pageSize));
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/entity/port/rows.ts
|
|
608
|
+
var IDENTITY_COLUMNS = /* @__PURE__ */ new Set(["id", "version", "organization_id"]);
|
|
609
|
+
var PRESENTATION_KEYS = /* @__PURE__ */ new Set(["_state", "_k"]);
|
|
610
|
+
function isRefList(value) {
|
|
611
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => item !== null && typeof item === "object" && typeof item.token === "string" && typeof item.id === "string");
|
|
612
|
+
}
|
|
613
|
+
function toRefs(value) {
|
|
614
|
+
if (!isRefList(value)) return [];
|
|
615
|
+
return value.map((item) => ({ token: item.token, id: item.id, label: typeof item.label === "string" ? item.label : null }));
|
|
616
|
+
}
|
|
617
|
+
function toRow(wire) {
|
|
618
|
+
const row = {
|
|
619
|
+
_id: wire.id,
|
|
620
|
+
_version: wire.version,
|
|
621
|
+
_organization_id: wire.organization_id,
|
|
622
|
+
_custom: wire._custom && typeof wire._custom === "object" ? { ...wire._custom } : {}
|
|
623
|
+
};
|
|
624
|
+
for (const [key, value] of Object.entries(wire)) {
|
|
625
|
+
if (IDENTITY_COLUMNS.has(key) || PRESENTATION_KEYS.has(key) || key === "_custom") continue;
|
|
626
|
+
row[key] = isRefList(value) ? toRefs(value) : value === void 0 ? null : value;
|
|
627
|
+
}
|
|
628
|
+
return row;
|
|
629
|
+
}
|
|
630
|
+
function toColumn(wire) {
|
|
631
|
+
const column = {
|
|
632
|
+
key: wire.key,
|
|
633
|
+
label: wire.label || wire.key,
|
|
634
|
+
type: wire.type,
|
|
635
|
+
origin: wire.origin === "custom" ? "custom" : "platform",
|
|
636
|
+
required: Boolean(wire.required),
|
|
637
|
+
readOnly: Boolean(wire.readOnly)
|
|
638
|
+
};
|
|
639
|
+
if (wire.choices && wire.choices.length > 0) column.choices = [...wire.choices];
|
|
640
|
+
if (wire.linksTo && wire.linksTo.length > 0) column.linksTo = [...wire.linksTo];
|
|
641
|
+
return column;
|
|
642
|
+
}
|
|
643
|
+
function toColumns(wire) {
|
|
644
|
+
return wire.filter((c) => !IDENTITY_COLUMNS.has(c.key)).map(toColumn);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/entity/port/port.ts
|
|
648
|
+
function toWireValue(value) {
|
|
649
|
+
if (isRef(value)) return value.id;
|
|
650
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isRef)) {
|
|
651
|
+
return value.length === 1 ? value[0].id : value.map((ref) => ref.id);
|
|
652
|
+
}
|
|
653
|
+
return value;
|
|
654
|
+
}
|
|
655
|
+
function isRef(value) {
|
|
656
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && typeof value.token === "string" && typeof value.id === "string";
|
|
657
|
+
}
|
|
658
|
+
function toWireValues(values) {
|
|
659
|
+
const out = {};
|
|
660
|
+
for (const [key, value] of Object.entries(values)) {
|
|
661
|
+
if (value === void 0) continue;
|
|
662
|
+
if (key === "_custom" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
663
|
+
out._custom = Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => [k, toWireValue(v)]));
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
out[key] = Array.isArray(value) && value.length === 0 && key !== "_custom" ? null : toWireValue(value);
|
|
667
|
+
}
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
function createEntityDataPort(options) {
|
|
671
|
+
const client = options.client ?? createEntityClient({ dataSource: options.dataSource });
|
|
672
|
+
const activeOrganization = () => (options.activeOrganizationId ? options.activeOrganizationId() : options.organization) ?? null;
|
|
673
|
+
const gets = /* @__PURE__ */ new Map();
|
|
674
|
+
const aliases = options.aliases;
|
|
675
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
676
|
+
let warnedNotLive = false;
|
|
677
|
+
const held = /* @__PURE__ */ new Map();
|
|
678
|
+
const replans = /* @__PURE__ */ new Map();
|
|
679
|
+
let memberships = null;
|
|
680
|
+
let maxOrgTopics = null;
|
|
681
|
+
function hold(alias, rows) {
|
|
682
|
+
const byId = held.get(alias) ?? /* @__PURE__ */ new Map();
|
|
683
|
+
held.set(alias, byId);
|
|
684
|
+
const before = new Set(byId.values());
|
|
685
|
+
for (const r of rows) {
|
|
686
|
+
if (typeof r.id === "string" && typeof r.organization_id === "string") byId.set(r.id, r.organization_id);
|
|
687
|
+
}
|
|
688
|
+
if ([...byId.values()].some((org) => !before.has(org))) for (const replan of replans.get(alias) ?? []) replan();
|
|
689
|
+
}
|
|
690
|
+
function heldCounts(alias) {
|
|
691
|
+
const counts = /* @__PURE__ */ new Map();
|
|
692
|
+
for (const org of held.get(alias)?.values() ?? []) counts.set(org, (counts.get(org) ?? 0) + 1);
|
|
693
|
+
return counts;
|
|
694
|
+
}
|
|
695
|
+
function membershipsOnce() {
|
|
696
|
+
memberships ??= client.memberOrganizations().then((answer) => {
|
|
697
|
+
if (answer.ok) return answer.data;
|
|
698
|
+
memberships = null;
|
|
699
|
+
console.warn(`[@ai-matrx/records/entity] Could not read your organizations for live updates: ${answer.error.message}`);
|
|
700
|
+
return null;
|
|
701
|
+
});
|
|
702
|
+
return memberships;
|
|
703
|
+
}
|
|
704
|
+
function maxOrgTopicsOnce() {
|
|
705
|
+
maxOrgTopics ??= client.knob({ key: "realtime_max_org_topics", organizationId: activeOrganization() }).then((answer) => {
|
|
706
|
+
const value = answer.ok ? Number(answer.data) : Number.NaN;
|
|
707
|
+
if (Number.isFinite(value) && value >= 0) return value;
|
|
708
|
+
console.warn(`[@ai-matrx/records/entity] knob entity_data.realtime_max_org_topics did not resolve; joining at most ${DEFAULT_MAX_ORG_TOPICS} organizations.`);
|
|
709
|
+
return DEFAULT_MAX_ORG_TOPICS;
|
|
710
|
+
});
|
|
711
|
+
return maxOrgTopics;
|
|
712
|
+
}
|
|
713
|
+
const tokenBindings = /* @__PURE__ */ new Map();
|
|
714
|
+
function bindingOf(alias) {
|
|
715
|
+
if (Object.prototype.hasOwnProperty.call(aliases, alias)) return aliases[alias];
|
|
716
|
+
if (!options.tokensAreAliases || !/^[a-z][a-z0-9_]*$/.test(alias)) return null;
|
|
717
|
+
let bound = tokenBindings.get(alias);
|
|
718
|
+
if (!bound) {
|
|
719
|
+
bound = { kind: "entity", token: alias };
|
|
720
|
+
tokenBindings.set(alias, bound);
|
|
721
|
+
}
|
|
722
|
+
return bound;
|
|
723
|
+
}
|
|
724
|
+
function noSuchAlias(alias) {
|
|
725
|
+
return refusal("not_found", `This Applet has no data source called "${alias}".`, "Use one of the aliases sources() lists.");
|
|
726
|
+
}
|
|
727
|
+
function shapeOf(alias, binding) {
|
|
728
|
+
let held2 = shapes.get(alias);
|
|
729
|
+
if (!held2) {
|
|
730
|
+
held2 = client.columns({ token: binding.token, organizationId: binding.organizationId ?? null }).then((answer) => {
|
|
731
|
+
if (!answer.ok) shapes.delete(alias);
|
|
732
|
+
return answer;
|
|
733
|
+
});
|
|
734
|
+
shapes.set(alias, held2);
|
|
735
|
+
}
|
|
736
|
+
return held2;
|
|
737
|
+
}
|
|
738
|
+
function getCoalesced(alias, token, id) {
|
|
739
|
+
return new Promise((resolve) => {
|
|
740
|
+
let batch = gets.get(alias);
|
|
741
|
+
if (!batch) {
|
|
742
|
+
batch = /* @__PURE__ */ new Map();
|
|
743
|
+
gets.set(alias, batch);
|
|
744
|
+
const asked = batch;
|
|
745
|
+
queueMicrotask(() => {
|
|
746
|
+
gets.delete(alias);
|
|
747
|
+
void flushGets(alias, token, asked);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const waiting = batch.get(id) ?? [];
|
|
751
|
+
waiting.push(resolve);
|
|
752
|
+
batch.set(id, waiting);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
async function flushGets(alias, token, asked) {
|
|
756
|
+
const answer = await client.get({ token, ids: [...asked.keys()] });
|
|
757
|
+
if (answer.ok) hold(alias, answer.data.rows);
|
|
758
|
+
const byId = new Map(answer.ok ? answer.data.rows.map((r) => [r.id, r]) : []);
|
|
759
|
+
for (const [id, waiting] of asked) {
|
|
760
|
+
const found = byId.get(id);
|
|
761
|
+
const wire = found && found["_state"]?.archived === true ? void 0 : found;
|
|
762
|
+
const result = answer.ok ? { ok: true, data: wire ? toRow(wire) : null } : answer;
|
|
763
|
+
for (const resolve of waiting) resolve(result);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function readBack(alias, answer, what) {
|
|
767
|
+
if (!answer.ok) return answer;
|
|
768
|
+
hold(alias, answer.data.rows);
|
|
769
|
+
const wire = answer.data.rows[0];
|
|
770
|
+
if (!wire) {
|
|
771
|
+
return refusal(
|
|
772
|
+
"not_found",
|
|
773
|
+
`${what} was saved, but you cannot open it now, so it is not shown here.`,
|
|
774
|
+
"The row is in the store; who may open it decides whether it appears in this list."
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
return { ok: true, data: toRow(wire) };
|
|
778
|
+
}
|
|
779
|
+
function done(answer) {
|
|
780
|
+
return answer.ok ? { ok: true, data: void 0 } : answer;
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
async sources() {
|
|
784
|
+
const out = [];
|
|
785
|
+
for (const [alias, binding] of Object.entries(aliases)) {
|
|
786
|
+
const shape = await shapeOf(alias, binding);
|
|
787
|
+
if (!shape.ok) return shape;
|
|
788
|
+
out.push({
|
|
789
|
+
alias,
|
|
790
|
+
label: binding.label ?? shape.data.label ?? alias,
|
|
791
|
+
kind: "entity",
|
|
792
|
+
canCreate: Boolean(shape.data.can_create),
|
|
793
|
+
canUpdate: Boolean(shape.data.can_update)
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return { ok: true, data: out };
|
|
797
|
+
},
|
|
798
|
+
async describe(alias) {
|
|
799
|
+
const binding = bindingOf(alias);
|
|
800
|
+
if (!binding) return noSuchAlias(alias);
|
|
801
|
+
const shape = await shapeOf(alias, binding);
|
|
802
|
+
if (!shape.ok) return shape;
|
|
803
|
+
return {
|
|
804
|
+
ok: true,
|
|
805
|
+
data: {
|
|
806
|
+
token: binding.token,
|
|
807
|
+
label: binding.label ?? shape.data.label ?? binding.token,
|
|
808
|
+
canCreate: Boolean(shape.data.can_create),
|
|
809
|
+
canUpdate: Boolean(shape.data.can_update)
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
},
|
|
813
|
+
async columns(alias) {
|
|
814
|
+
const binding = bindingOf(alias);
|
|
815
|
+
if (!binding) return noSuchAlias(alias);
|
|
816
|
+
const shape = await shapeOf(alias, binding);
|
|
817
|
+
return shape.ok ? { ok: true, data: toColumns(shape.data.columns) } : shape;
|
|
818
|
+
},
|
|
819
|
+
async list(alias, q = {}, cursor) {
|
|
820
|
+
const binding = bindingOf(alias);
|
|
821
|
+
if (!binding) return noSuchAlias(alias);
|
|
822
|
+
let customKeys = /* @__PURE__ */ new Set();
|
|
823
|
+
if (q.where && q.where.length > 0 || q.sort && q.sort.length > 0) {
|
|
824
|
+
const shape = await shapeOf(alias, binding);
|
|
825
|
+
if (!shape.ok) return shape;
|
|
826
|
+
customKeys = new Set(shape.data.columns.filter((c) => c.origin === "custom").map((c) => c.key));
|
|
827
|
+
}
|
|
828
|
+
const wire = toWire(q, customKeys);
|
|
829
|
+
if (binding.organizationId) wire.scope = { ...wire.scope, organization_id: binding.organizationId };
|
|
830
|
+
const answer = await client.list({ token: binding.token, ...wire, after: cursor ?? null });
|
|
831
|
+
if (!answer.ok) return answer;
|
|
832
|
+
const { data } = answer;
|
|
833
|
+
if (data.sort_ignored.length > 0) {
|
|
834
|
+
console.warn(`[@ai-matrx/records/entity] "${alias}" sorts by one column for now; ignored: ${data.sort_ignored.join(", ")}.`);
|
|
835
|
+
}
|
|
836
|
+
hold(alias, data.rows);
|
|
837
|
+
const page = {
|
|
838
|
+
rows: data.rows.map(toRow),
|
|
839
|
+
// A count that stopped at the store's cap is a floor, not a count: unknown beats a wrong number.
|
|
840
|
+
total: data.total === null || data.total === void 0 || data.total_is_capped ? null : data.total,
|
|
841
|
+
cursor: data.has_more ? data.next_after : null
|
|
842
|
+
};
|
|
843
|
+
return { ok: true, data: page };
|
|
844
|
+
},
|
|
845
|
+
async get(alias, id) {
|
|
846
|
+
const binding = bindingOf(alias);
|
|
847
|
+
if (!binding) return noSuchAlias(alias);
|
|
848
|
+
if (!isUuidShape(id)) return refusal("invalid_argument", `"${String(id)}" is not a row id.`);
|
|
849
|
+
return getCoalesced(alias, binding.token, id);
|
|
850
|
+
},
|
|
851
|
+
async create(alias, values) {
|
|
852
|
+
const binding = bindingOf(alias);
|
|
853
|
+
if (!binding) return noSuchAlias(alias);
|
|
854
|
+
const organizationId = binding.organizationId ?? activeOrganization();
|
|
855
|
+
if (!organizationId) {
|
|
856
|
+
return refusal(
|
|
857
|
+
"organization_required",
|
|
858
|
+
"Choose the organization this new record belongs to.",
|
|
859
|
+
"Pick an organization, then add the row again. Nothing was saved."
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const answer = await client.insert({ token: binding.token, organizationId, rows: [toWireValues(values)], opId: mintOpId() });
|
|
863
|
+
return readBack(alias, answer, "The new row");
|
|
864
|
+
},
|
|
865
|
+
async update(alias, id, patch, baseVersion) {
|
|
866
|
+
const binding = bindingOf(alias);
|
|
867
|
+
if (!binding) return noSuchAlias(alias);
|
|
868
|
+
if (!isUuidShape(id)) return refusal("invalid_argument", `"${String(id)}" is not a row id.`);
|
|
869
|
+
const answer = await client.update({ token: binding.token, id, baseVersion, patch: toWireValues(patch), opId: mintOpId() });
|
|
870
|
+
return readBack(alias, answer, "The change");
|
|
871
|
+
},
|
|
872
|
+
async archive(alias, id) {
|
|
873
|
+
const binding = bindingOf(alias);
|
|
874
|
+
if (!binding) return noSuchAlias(alias);
|
|
875
|
+
if (!isUuidShape(id)) return refusal("invalid_argument", `"${String(id)}" is not a row id.`);
|
|
876
|
+
return done(await client.archive({ token: binding.token, ids: [id], opId: mintOpId() }));
|
|
877
|
+
},
|
|
878
|
+
async restore(alias, id) {
|
|
879
|
+
const binding = bindingOf(alias);
|
|
880
|
+
if (!binding) return noSuchAlias(alias);
|
|
881
|
+
if (!isUuidShape(id)) return refusal("invalid_argument", `"${String(id)}" is not a row id.`);
|
|
882
|
+
return done(await client.unarchive({ token: binding.token, ids: [id], opId: mintOpId() }));
|
|
883
|
+
},
|
|
884
|
+
subscribe(event, listener) {
|
|
885
|
+
const alias = event.startsWith("rows:") ? event.slice(5) : null;
|
|
886
|
+
const binding = alias ? bindingOf(alias) : null;
|
|
887
|
+
if (!alias || !binding) {
|
|
888
|
+
throw new Error(`DataPort.subscribe: "${event}" is not an event this Applet's port carries. Use rows:<alias>.`);
|
|
889
|
+
}
|
|
890
|
+
const realtime = options.realtime;
|
|
891
|
+
if (!realtime) {
|
|
892
|
+
if (!warnedNotLive) {
|
|
893
|
+
warnedNotLive = true;
|
|
894
|
+
console.warn("[@ai-matrx/records/entity] This host bound no realtime layer, so other people's changes appear on the next read.");
|
|
895
|
+
}
|
|
896
|
+
return () => {
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
let stopped = false;
|
|
900
|
+
let joined = [];
|
|
901
|
+
let stopLive = null;
|
|
902
|
+
const sink = { rows: (ids) => listener({ ids: ids === null ? null : [...ids] }) };
|
|
903
|
+
const plan = async () => {
|
|
904
|
+
const organizationIds = binding.organizationId ? [binding.organizationId] : orderOrganizationTopics({
|
|
905
|
+
active: activeOrganization(),
|
|
906
|
+
held: heldCounts(alias),
|
|
907
|
+
members: await membershipsOnce(),
|
|
908
|
+
max: await maxOrgTopicsOnce()
|
|
909
|
+
});
|
|
910
|
+
if (stopped) return;
|
|
911
|
+
if (organizationIds.length === joined.length && organizationIds.every((org, i) => org === joined[i])) return;
|
|
912
|
+
const previous = stopLive;
|
|
913
|
+
joined = organizationIds;
|
|
914
|
+
stopLive = realtime.subscribe({ token: binding.token, organizationIds }, sink);
|
|
915
|
+
previous?.();
|
|
916
|
+
};
|
|
917
|
+
const replan = () => void plan();
|
|
918
|
+
const set = replans.get(alias) ?? /* @__PURE__ */ new Set();
|
|
919
|
+
replans.set(alias, set);
|
|
920
|
+
set.add(replan);
|
|
921
|
+
replan();
|
|
922
|
+
return () => {
|
|
923
|
+
stopped = true;
|
|
924
|
+
set.delete(replan);
|
|
925
|
+
stopLive?.();
|
|
926
|
+
stopLive = null;
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function createSeedPort(dataSource) {
|
|
932
|
+
return createEntityDataPort({ client: createEntityClient({ dataSource }), aliases: {}, tokensAreAliases: true, activeOrganizationId: () => null });
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/entity/react/index.tsx
|
|
936
|
+
var ENTITY_PORT_KEY = "entity-data";
|
|
937
|
+
var EntityScopeContext = createContext(null);
|
|
938
|
+
function EntityDataProvider({ dataSource, client, activeOrganizationId, realtime, portKey = ENTITY_PORT_KEY, children }) {
|
|
939
|
+
const active = useRef(activeOrganizationId);
|
|
940
|
+
active.current = activeOrganizationId;
|
|
941
|
+
const scope = useMemo(() => {
|
|
942
|
+
const bound = client ?? createEntityClient({ dataSource });
|
|
943
|
+
const live = realtime === false ? void 0 : realtime ?? createEntityRealtime();
|
|
944
|
+
const port = createEntityDataPort({
|
|
945
|
+
client: bound,
|
|
946
|
+
aliases: {},
|
|
947
|
+
tokensAreAliases: true,
|
|
948
|
+
activeOrganizationId: () => active.current,
|
|
949
|
+
...live ? { realtime: live } : {}
|
|
950
|
+
});
|
|
951
|
+
return { port, portKey, events: hostEvents(port) };
|
|
952
|
+
}, [client, dataSource, realtime, portKey]);
|
|
953
|
+
const above = useContext(QueryClientContext);
|
|
954
|
+
const [serverClient] = useState(() => typeof window === "undefined" && !above ? new QueryClient() : null);
|
|
955
|
+
const inner = createElement(EntityScopeContext.Provider, { value: scope }, children);
|
|
956
|
+
return serverClient ? createElement(QueryClientProvider, { client: serverClient }, inner) : inner;
|
|
957
|
+
}
|
|
958
|
+
function useOptionalEntityScope() {
|
|
959
|
+
return useContext(EntityScopeContext);
|
|
960
|
+
}
|
|
961
|
+
function useEntityScope() {
|
|
962
|
+
const scope = useContext(EntityScopeContext);
|
|
963
|
+
if (!scope) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
"An entity hook was called outside <EntityDataProvider>. Mount it once, high, with the supabase client that carries this person's session and the active organization."
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
return scope;
|
|
969
|
+
}
|
|
970
|
+
function useEntityList(token, query = {}) {
|
|
971
|
+
const seeded = useSeededSource(`entity:${token}:rows:${JSON.stringify(query)}`, (source) => createSeedPort(source).list(token, query));
|
|
972
|
+
return useDataRows(token, query, useEntityScope(), seeded);
|
|
973
|
+
}
|
|
974
|
+
function useEntityColumns(token) {
|
|
975
|
+
const seeded = useSeededSource(`entity:${token}:columns`, (source) => createSeedPort(source).columns(token));
|
|
976
|
+
return useDataColumns(token, useEntityScope(), seeded);
|
|
977
|
+
}
|
|
978
|
+
function useEntityType(token) {
|
|
979
|
+
const scope = useEntityScope();
|
|
980
|
+
const qc = useRecordsQueryClient();
|
|
981
|
+
const seeded = useSeededSource(`entity:${token}:type`, (source) => createSeedPort(source).describe(token));
|
|
982
|
+
const q = useQuery(
|
|
983
|
+
{
|
|
984
|
+
...seeded ? { initialData: seeded.data, initialDataUpdatedAt: seeded.at } : {},
|
|
985
|
+
queryKey: [...dataKeys.alias(scope.portKey, token), "type"],
|
|
986
|
+
queryFn: async () => {
|
|
987
|
+
const answer = await scope.port.describe(token);
|
|
988
|
+
if (!answer.ok) throw Object.assign(new Error(answer.error.message), { error: answer.error });
|
|
989
|
+
return answer.data;
|
|
990
|
+
},
|
|
991
|
+
retry: false
|
|
992
|
+
},
|
|
993
|
+
qc
|
|
994
|
+
);
|
|
995
|
+
const failed = q.error;
|
|
996
|
+
return { data: q.data, loading: q.isPending, error: failed ? failed.error ?? { code: "internal", message: failed.message } : null, refresh: () => void q.refetch() };
|
|
997
|
+
}
|
|
998
|
+
function useEntityRow(token, id) {
|
|
999
|
+
return useDataRow(token, id, useEntityScope());
|
|
1000
|
+
}
|
|
1001
|
+
function useEntityWrite(token) {
|
|
1002
|
+
return useDataMutations(token, useEntityScope());
|
|
1003
|
+
}
|
|
1004
|
+
export {
|
|
1005
|
+
ENTITY_PORT_KEY,
|
|
1006
|
+
EntityDataProvider,
|
|
1007
|
+
useEntityColumns,
|
|
1008
|
+
useEntityList,
|
|
1009
|
+
useEntityRow,
|
|
1010
|
+
useEntityScope,
|
|
1011
|
+
useEntityType,
|
|
1012
|
+
useEntityWrite,
|
|
1013
|
+
useOptionalEntityScope
|
|
1014
|
+
};
|
|
1015
|
+
//# sourceMappingURL=react.js.map
|