@burojs/hasura 0.2.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/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/index.cjs +854 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +466 -0
- package/dist/index.d.ts +466 -0
- package/dist/index.js +839 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { defineDataProvider, normalizeDataError, isDataError } from '@burojs/core';
|
|
2
|
+
import { createClient } from 'graphql-ws';
|
|
3
|
+
|
|
4
|
+
// src/data-provider.ts
|
|
5
|
+
|
|
6
|
+
// src/endpoint.ts
|
|
7
|
+
var DEFAULT_HASURA_ENDPOINT = "/gql/v1/graphql";
|
|
8
|
+
function hasuraWsUrl(endpoint) {
|
|
9
|
+
if (typeof window === "undefined") return endpoint;
|
|
10
|
+
const url = new URL(endpoint, window.location.origin);
|
|
11
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
14
|
+
function listSubscription(mapping) {
|
|
15
|
+
const where = mapping.fixedFilter;
|
|
16
|
+
return {
|
|
17
|
+
query: where ? `subscription Live_${mapping.table}_list($where: ${mapping.table}_bool_exp) {
|
|
18
|
+
${mapping.table}_aggregate(where: $where) { aggregate { count max { updated_at } } }
|
|
19
|
+
}` : `subscription Live_${mapping.table}_list {
|
|
20
|
+
${mapping.table}_aggregate { aggregate { count max { updated_at } } }
|
|
21
|
+
}`,
|
|
22
|
+
...where === void 0 ? {} : { variables: { where } }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function oneSubscription(mapping, id) {
|
|
26
|
+
const pkField = mapping.pkField ?? "id";
|
|
27
|
+
const pkType = mapping.pkType ?? "bigint";
|
|
28
|
+
const root = mapping.byPkRoot === false ? null : mapping.byPkRoot ?? `${mapping.table}_by_pk`;
|
|
29
|
+
if (root === null) return null;
|
|
30
|
+
return {
|
|
31
|
+
query: `subscription Live_${mapping.table}_one($id: ${pkType}!) {
|
|
32
|
+
${root}(${pkField}: $id) { ${pkField} updated_at }
|
|
33
|
+
}`,
|
|
34
|
+
variables: { id }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function createHasuraLiveProvider(config) {
|
|
38
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
39
|
+
const endpoint = config.endpoint ?? DEFAULT_HASURA_ENDPOINT;
|
|
40
|
+
const role = config.role ?? null;
|
|
41
|
+
let client;
|
|
42
|
+
const status = (value) => {
|
|
43
|
+
for (const listener of listeners) listener(value);
|
|
44
|
+
};
|
|
45
|
+
function getClient() {
|
|
46
|
+
if (client !== void 0) return client;
|
|
47
|
+
const options = {
|
|
48
|
+
url: hasuraWsUrl(endpoint),
|
|
49
|
+
lazy: true,
|
|
50
|
+
keepAlive: 1e4,
|
|
51
|
+
retryAttempts: Number.POSITIVE_INFINITY,
|
|
52
|
+
shouldRetry: () => true,
|
|
53
|
+
connectionParams: async () => {
|
|
54
|
+
const token = await config.getToken();
|
|
55
|
+
return {
|
|
56
|
+
headers: {
|
|
57
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
58
|
+
...role ? { "X-Hasura-Role": role } : {}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
on: {
|
|
63
|
+
connecting: () => status("connecting"),
|
|
64
|
+
connected: () => status("connected"),
|
|
65
|
+
closed: () => status("degraded")
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
client = config.clientFactory?.(options) ?? createClient(options);
|
|
69
|
+
return client;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
async open(descriptor, openContext) {
|
|
73
|
+
if (openContext.signal.aborted) return null;
|
|
74
|
+
const mapping = config.resources[descriptor.resource];
|
|
75
|
+
if (mapping?.live !== true) return null;
|
|
76
|
+
const payload = descriptor.kind === "list" ? listSubscription(mapping) : descriptor.kind === "one" ? oneSubscription(mapping, descriptor.id) : null;
|
|
77
|
+
if (payload === null) return null;
|
|
78
|
+
return {
|
|
79
|
+
kind: "invalidate",
|
|
80
|
+
initial: "query",
|
|
81
|
+
subscribe(observer, subscribeContext) {
|
|
82
|
+
if (openContext.signal.aborted || subscribeContext.signal.aborted) return () => void 0;
|
|
83
|
+
let primed = false;
|
|
84
|
+
let closed = false;
|
|
85
|
+
let upstream = void 0;
|
|
86
|
+
const close = () => {
|
|
87
|
+
if (closed) return;
|
|
88
|
+
closed = true;
|
|
89
|
+
listeners.delete(onStatus);
|
|
90
|
+
openContext.signal.removeEventListener("abort", close);
|
|
91
|
+
subscribeContext.signal.removeEventListener("abort", close);
|
|
92
|
+
upstream?.();
|
|
93
|
+
};
|
|
94
|
+
const onStatus = (value) => {
|
|
95
|
+
if (!closed) observer.status(value);
|
|
96
|
+
};
|
|
97
|
+
const sink = {
|
|
98
|
+
next: () => {
|
|
99
|
+
if (closed) return;
|
|
100
|
+
if (!primed) {
|
|
101
|
+
primed = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
observer.next(void 0);
|
|
105
|
+
},
|
|
106
|
+
error: (error) => {
|
|
107
|
+
if (closed) return;
|
|
108
|
+
observer.error(normalizeDataError(error));
|
|
109
|
+
close();
|
|
110
|
+
},
|
|
111
|
+
complete: () => {
|
|
112
|
+
if (!closed) observer.status("degraded");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
listeners.add(onStatus);
|
|
116
|
+
openContext.signal.addEventListener("abort", close, { once: true });
|
|
117
|
+
subscribeContext.signal.addEventListener("abort", close, { once: true });
|
|
118
|
+
upstream = getClient().subscribe(payload, sink);
|
|
119
|
+
if (closed) upstream();
|
|
120
|
+
if (openContext.signal.aborted || subscribeContext.signal.aborted) close();
|
|
121
|
+
return Promise.resolve(close);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/data-provider.ts
|
|
129
|
+
function setNestedPath(obj, path, value) {
|
|
130
|
+
const parts = path.split(".");
|
|
131
|
+
const leaf = parts.pop();
|
|
132
|
+
if (leaf === void 0) return;
|
|
133
|
+
let cur = obj;
|
|
134
|
+
for (const p of parts) {
|
|
135
|
+
const next = cur[p];
|
|
136
|
+
if (typeof next !== "object" || next === null) cur[p] = {};
|
|
137
|
+
cur = cur[p];
|
|
138
|
+
}
|
|
139
|
+
cur[leaf] = value;
|
|
140
|
+
}
|
|
141
|
+
function getNestedPath(obj, path) {
|
|
142
|
+
let cur = obj;
|
|
143
|
+
for (const p of path.split(".")) {
|
|
144
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
145
|
+
cur = cur[p];
|
|
146
|
+
}
|
|
147
|
+
return cur;
|
|
148
|
+
}
|
|
149
|
+
function partitionDataBody(values, cfg) {
|
|
150
|
+
const keySet = new Set(cfg.keys);
|
|
151
|
+
const dataBody = {};
|
|
152
|
+
const rest = {};
|
|
153
|
+
let hasDataBody = false;
|
|
154
|
+
for (const [key, value] of Object.entries(values)) {
|
|
155
|
+
if (key === cfg.relationship) continue;
|
|
156
|
+
if (keySet.has(key)) {
|
|
157
|
+
if (value !== void 0) {
|
|
158
|
+
dataBody[key] = value;
|
|
159
|
+
hasDataBody = true;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
rest[key] = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { dataBody, rest, hasDataBody };
|
|
166
|
+
}
|
|
167
|
+
function partitionJsonbSpill(values, spill, baseColumns) {
|
|
168
|
+
const baseSet = new Set(baseColumns);
|
|
169
|
+
const metaSet = new Set(spill.metaKeys);
|
|
170
|
+
const nestedKeys = spill.nestedKeys ?? {};
|
|
171
|
+
const base = {};
|
|
172
|
+
const headExplicit = {};
|
|
173
|
+
const metaExplicit = {};
|
|
174
|
+
const headSpill = {};
|
|
175
|
+
const metaSpill = {};
|
|
176
|
+
const nestedSpill = [];
|
|
177
|
+
for (const [key, value] of Object.entries(values)) {
|
|
178
|
+
if (value === void 0) continue;
|
|
179
|
+
const nestedPath = nestedKeys[key];
|
|
180
|
+
if (key === spill.column) {
|
|
181
|
+
if (value && typeof value === "object")
|
|
182
|
+
Object.assign(headExplicit, value);
|
|
183
|
+
} else if (key === spill.metaColumn) {
|
|
184
|
+
if (value && typeof value === "object")
|
|
185
|
+
Object.assign(metaExplicit, value);
|
|
186
|
+
} else if (metaSet.has(key)) {
|
|
187
|
+
metaSpill[key] = value;
|
|
188
|
+
} else if (nestedPath) {
|
|
189
|
+
nestedSpill.push([nestedPath, value]);
|
|
190
|
+
} else if (baseSet.has(key)) {
|
|
191
|
+
base[key] = value;
|
|
192
|
+
} else {
|
|
193
|
+
headSpill[key] = value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const head = { ...headExplicit, ...headSpill };
|
|
197
|
+
for (const [path, value] of nestedSpill) setNestedPath(head, path, value);
|
|
198
|
+
const meta = { ...metaExplicit, ...metaSpill };
|
|
199
|
+
const out = { ...base };
|
|
200
|
+
if (Object.keys(head).length > 0) out[spill.column] = head;
|
|
201
|
+
if (Object.keys(meta).length > 0) out[spill.metaColumn] = meta;
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
function flattenJsonbSpill(item, spill) {
|
|
205
|
+
if (!item || typeof item !== "object") return item;
|
|
206
|
+
const rec = item;
|
|
207
|
+
const headObj = rec[spill.column];
|
|
208
|
+
const metaObj = rec[spill.metaColumn];
|
|
209
|
+
const out = {
|
|
210
|
+
...headObj && typeof headObj === "object" ? headObj : {},
|
|
211
|
+
...metaObj && typeof metaObj === "object" ? metaObj : {},
|
|
212
|
+
...rec
|
|
213
|
+
};
|
|
214
|
+
if (spill.nestedKeys && headObj && typeof headObj === "object") {
|
|
215
|
+
for (const [flatKey, path] of Object.entries(spill.nestedKeys)) {
|
|
216
|
+
const v = getNestedPath(headObj, path);
|
|
217
|
+
if (v !== void 0) out[flatKey] = v;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
var NOT_AUTHENTICATED_CODE = "not-authenticated";
|
|
223
|
+
var OP_MAP = {
|
|
224
|
+
eq: "_eq",
|
|
225
|
+
neq: "_neq",
|
|
226
|
+
ne: "_neq",
|
|
227
|
+
lt: "_lt",
|
|
228
|
+
lte: "_lte",
|
|
229
|
+
gt: "_gt",
|
|
230
|
+
gte: "_gte",
|
|
231
|
+
in: "_in",
|
|
232
|
+
notIn: "_nin",
|
|
233
|
+
nin: "_nin",
|
|
234
|
+
contains: "_ilike",
|
|
235
|
+
containss: "_like",
|
|
236
|
+
startsWith: "_ilike",
|
|
237
|
+
endsWith: "_ilike",
|
|
238
|
+
startswith: "_ilike",
|
|
239
|
+
endswith: "_ilike",
|
|
240
|
+
null: "_is_null"
|
|
241
|
+
};
|
|
242
|
+
var FILTER_OPERATOR_UNSUPPORTED_CODE = "filter-operator-unsupported";
|
|
243
|
+
var UNEXPRESSIBLE_OPS = {
|
|
244
|
+
has: "a collection-membership test",
|
|
245
|
+
hasAny: "a collection-membership test",
|
|
246
|
+
hasAll: "a collection-membership test"
|
|
247
|
+
};
|
|
248
|
+
function unsupportedOperator(field, op) {
|
|
249
|
+
const why = UNEXPRESSIBLE_OPS[op];
|
|
250
|
+
return {
|
|
251
|
+
kind: "capability",
|
|
252
|
+
code: FILTER_OPERATOR_UNSUPPORTED_CODE,
|
|
253
|
+
message: why ? `@burojs/hasura cannot express the '${op}' filter operator on '${field}': ${why}, whose Hasura encoding depends on whether the column is jsonb or a Postgres array \u2014 something no ResourceMapping field declares. Declare it and translate explicitly, or filter on a scalar column.` : `@burojs/hasura does not support the filter operator '${op}' (on '${field}').`
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function scopeToWhere(scope, scopeFields) {
|
|
257
|
+
const clauses = [];
|
|
258
|
+
for (const key of scopeFields) {
|
|
259
|
+
const v = scope[key];
|
|
260
|
+
if (v == null || v === "") continue;
|
|
261
|
+
clauses.push({ [key]: { _eq: v } });
|
|
262
|
+
}
|
|
263
|
+
if (clauses.length === 0) return void 0;
|
|
264
|
+
if (clauses.length === 1) return clauses[0];
|
|
265
|
+
return { _and: clauses };
|
|
266
|
+
}
|
|
267
|
+
function mergeWhere(a, b) {
|
|
268
|
+
if (!a) return b;
|
|
269
|
+
if (!b) return a;
|
|
270
|
+
return { _and: [a, b] };
|
|
271
|
+
}
|
|
272
|
+
function filtersToWhere(filter) {
|
|
273
|
+
if (!filter || typeof filter !== "object") return void 0;
|
|
274
|
+
if ("and" in filter) {
|
|
275
|
+
if (filter.and.length === 0) return { _and: [] };
|
|
276
|
+
const clauses = filter.and.map(filtersToWhere).filter((item) => item !== void 0);
|
|
277
|
+
if (clauses.length === 0) return void 0;
|
|
278
|
+
return clauses.length === 1 ? clauses[0] : { _and: clauses };
|
|
279
|
+
}
|
|
280
|
+
if ("or" in filter) {
|
|
281
|
+
if (filter.or.length === 0) return { _or: [] };
|
|
282
|
+
const clauses = filter.or.map(filtersToWhere).filter((item) => item !== void 0);
|
|
283
|
+
if (clauses.length === 0) return void 0;
|
|
284
|
+
return clauses.length === 1 ? clauses[0] : { _or: clauses };
|
|
285
|
+
}
|
|
286
|
+
if ("not" in filter) {
|
|
287
|
+
const clause = filtersToWhere(filter.not);
|
|
288
|
+
return clause === void 0 ? void 0 : { _not: clause };
|
|
289
|
+
}
|
|
290
|
+
if (!("field" in filter) || typeof filter.field !== "string" || !("op" in filter))
|
|
291
|
+
return void 0;
|
|
292
|
+
const { field, op } = filter;
|
|
293
|
+
if (op === "isNull") return { [field]: { _is_null: true } };
|
|
294
|
+
if (op === "isNotNull") return { [field]: { _is_null: false } };
|
|
295
|
+
if (!("value" in filter) || filter.value === void 0) return void 0;
|
|
296
|
+
const operator = OP_MAP[op];
|
|
297
|
+
if (operator === void 0) throw unsupportedOperator(field, op);
|
|
298
|
+
let value = filter.value;
|
|
299
|
+
if (operator === "_ilike" || operator === "_like") {
|
|
300
|
+
const text = String(value);
|
|
301
|
+
value = op === "startsWith" ? `${text}%` : op === "endsWith" ? `%${text}` : `%${text}%`;
|
|
302
|
+
}
|
|
303
|
+
return { [field]: { [operator]: value } };
|
|
304
|
+
}
|
|
305
|
+
function coerceInt64Filter(filter, fields) {
|
|
306
|
+
if (!filter || !fields?.length) return filter;
|
|
307
|
+
if ("and" in filter)
|
|
308
|
+
return {
|
|
309
|
+
and: filter.and.map(
|
|
310
|
+
(entry) => coerceInt64Filter(entry, fields)
|
|
311
|
+
)
|
|
312
|
+
};
|
|
313
|
+
if ("or" in filter)
|
|
314
|
+
return {
|
|
315
|
+
or: filter.or.map(
|
|
316
|
+
(entry) => coerceInt64Filter(entry, fields)
|
|
317
|
+
)
|
|
318
|
+
};
|
|
319
|
+
if ("not" in filter)
|
|
320
|
+
return { not: coerceInt64Filter(filter.not, fields) };
|
|
321
|
+
if ("field" in filter && fields.includes(filter.field) && "value" in filter && filter.value != null) {
|
|
322
|
+
const value = Array.isArray(filter.value) ? filter.value.map(String) : String(filter.value);
|
|
323
|
+
return { ...filter, value };
|
|
324
|
+
}
|
|
325
|
+
return filter;
|
|
326
|
+
}
|
|
327
|
+
function hasuraError(error, signal, fallback) {
|
|
328
|
+
if (isDataError(error)) return error;
|
|
329
|
+
if (signal.aborted)
|
|
330
|
+
return {
|
|
331
|
+
kind: "cancelled",
|
|
332
|
+
code: "request-aborted",
|
|
333
|
+
message: "Hasura request was aborted.",
|
|
334
|
+
cause: error
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
kind: "transport",
|
|
338
|
+
code: "hasura-request-failed",
|
|
339
|
+
message: error instanceof Error && error.message ? error.message : fallback,
|
|
340
|
+
cause: error
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function aggregateRoot(m) {
|
|
344
|
+
if (m.aggregate === false) return null;
|
|
345
|
+
return typeof m.aggregate === "string" ? m.aggregate : `${m.table}_aggregate`;
|
|
346
|
+
}
|
|
347
|
+
function byPkRoot(m) {
|
|
348
|
+
if (m.byPkRoot === false) return null;
|
|
349
|
+
return m.byPkRoot ?? `${m.table}_by_pk`;
|
|
350
|
+
}
|
|
351
|
+
function snapshotOperationRegistry(source) {
|
|
352
|
+
if (source === void 0) return Object.freeze({});
|
|
353
|
+
const resources = {};
|
|
354
|
+
for (const [resource, operations] of Object.entries(source)) {
|
|
355
|
+
const entries = {};
|
|
356
|
+
for (const [name, operation] of Object.entries(operations)) {
|
|
357
|
+
entries[name] = Object.freeze({ ...operation });
|
|
358
|
+
}
|
|
359
|
+
resources[resource] = Object.freeze(entries);
|
|
360
|
+
}
|
|
361
|
+
return Object.freeze(resources);
|
|
362
|
+
}
|
|
363
|
+
function hasRegisteredOperationKind(operations, kind) {
|
|
364
|
+
return Object.values(operations).some(
|
|
365
|
+
(byName) => Object.values(byName).some((operation) => operation.kind === kind)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
function normalizeRole(role) {
|
|
369
|
+
return role === void 0 || role === null || role === "" ? null : role;
|
|
370
|
+
}
|
|
371
|
+
function createHasuraDataProvider(config) {
|
|
372
|
+
const endpoint = config.endpoint ?? DEFAULT_HASURA_ENDPOINT;
|
|
373
|
+
const defaultRole = normalizeRole(config.defaultRole);
|
|
374
|
+
const operations = snapshotOperationRegistry(config.operations);
|
|
375
|
+
const hasCustomQueries = hasRegisteredOperationKind(operations, "query");
|
|
376
|
+
const hasCustomMutations = hasRegisteredOperationKind(operations, "mutation");
|
|
377
|
+
async function gql(query, variables, role, signal) {
|
|
378
|
+
try {
|
|
379
|
+
const request = async (token2) => {
|
|
380
|
+
const headers = {
|
|
381
|
+
"Content-Type": "application/json",
|
|
382
|
+
Authorization: `Bearer ${token2}`
|
|
383
|
+
};
|
|
384
|
+
const effectiveRole = role === void 0 ? defaultRole : normalizeRole(role);
|
|
385
|
+
if (effectiveRole !== null) headers["X-Hasura-Role"] = effectiveRole;
|
|
386
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
387
|
+
return fetchImpl(endpoint, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers,
|
|
390
|
+
body: JSON.stringify({ query, variables }),
|
|
391
|
+
signal
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
let token = await config.getToken();
|
|
395
|
+
if (!token)
|
|
396
|
+
throw {
|
|
397
|
+
kind: "auth",
|
|
398
|
+
code: NOT_AUTHENTICATED_CODE,
|
|
399
|
+
message: "Not authenticated."
|
|
400
|
+
};
|
|
401
|
+
let res = await request(token);
|
|
402
|
+
if (res.status === 401 && config.recoverAuth && await config.recoverAuth()) {
|
|
403
|
+
token = await config.getToken();
|
|
404
|
+
if (!token)
|
|
405
|
+
throw {
|
|
406
|
+
kind: "auth",
|
|
407
|
+
code: NOT_AUTHENTICATED_CODE,
|
|
408
|
+
message: "Not authenticated."
|
|
409
|
+
};
|
|
410
|
+
res = await request(token);
|
|
411
|
+
}
|
|
412
|
+
if (!res.ok)
|
|
413
|
+
throw {
|
|
414
|
+
kind: "transport",
|
|
415
|
+
code: "graphql-http-error",
|
|
416
|
+
message: `GraphQL HTTP ${res.status}`,
|
|
417
|
+
status: res.status
|
|
418
|
+
};
|
|
419
|
+
const body = await res.json();
|
|
420
|
+
if (body.errors?.length)
|
|
421
|
+
throw {
|
|
422
|
+
kind: "protocol",
|
|
423
|
+
code: "graphql-error",
|
|
424
|
+
message: body.errors.map((e) => e.message).join("; "),
|
|
425
|
+
details: body.errors
|
|
426
|
+
};
|
|
427
|
+
if (body.data === void 0)
|
|
428
|
+
throw {
|
|
429
|
+
kind: "protocol",
|
|
430
|
+
code: "empty-graphql-response",
|
|
431
|
+
message: "Empty GraphQL response."
|
|
432
|
+
};
|
|
433
|
+
return body.data;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
throw hasuraError(error, signal, "Hasura request failed.");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function mapping(resource) {
|
|
439
|
+
const m = config.resources[resource];
|
|
440
|
+
if (!m)
|
|
441
|
+
throw {
|
|
442
|
+
kind: "configuration",
|
|
443
|
+
code: "unknown-resource",
|
|
444
|
+
message: `Unknown resource: ${resource}`
|
|
445
|
+
};
|
|
446
|
+
return m;
|
|
447
|
+
}
|
|
448
|
+
function registeredOperation(resource, operation, kind) {
|
|
449
|
+
const registered = operations[resource]?.[operation];
|
|
450
|
+
if (registered === void 0 || registered.kind !== kind) {
|
|
451
|
+
throw {
|
|
452
|
+
kind: "capability",
|
|
453
|
+
code: "hasura-custom-operation-unsupported",
|
|
454
|
+
message: `Hasura ${kind} operation '${operation}' is not declared for '${resource}'.`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return registered;
|
|
458
|
+
}
|
|
459
|
+
async function executeCustomOperation(resource, operationName, input, kind, signal) {
|
|
460
|
+
const operation = registeredOperation(resource, operationName, kind);
|
|
461
|
+
const result = await gql(
|
|
462
|
+
operation.document,
|
|
463
|
+
operation.variables(input),
|
|
464
|
+
operation.role ?? config.resources[resource]?.role,
|
|
465
|
+
signal
|
|
466
|
+
);
|
|
467
|
+
return operation.mapResult?.(result) ?? result;
|
|
468
|
+
}
|
|
469
|
+
function composeListWhere(m, descriptor, context) {
|
|
470
|
+
const userWhere = filtersToWhere(coerceInt64Filter(descriptor.filter, m.int64Fields));
|
|
471
|
+
const ignoreScope = descriptor.providerOptions?.ignoreScope === true;
|
|
472
|
+
const sessionScope = context.session.scopeKey !== null && typeof context.session.scopeKey === "object" && !Array.isArray(context.session.scopeKey) ? context.session.scopeKey : {};
|
|
473
|
+
const scope = ignoreScope ? {} : { ...sessionScope, ...config.getScope?.(context) ?? {} };
|
|
474
|
+
const scopeWhere = scopeToWhere(scope, m.scopeFields ?? []);
|
|
475
|
+
return mergeWhere(mergeWhere(userWhere, scopeWhere), m.fixedFilter);
|
|
476
|
+
}
|
|
477
|
+
async function runAggregateCount(m, root, where, signal) {
|
|
478
|
+
const aggWhereArg = where ? "($where: " + m.table + "_bool_exp)" : "";
|
|
479
|
+
const aggWhereParam = where ? "(where: $where)" : "";
|
|
480
|
+
const metaQuery = `query Count_${m.table} ${aggWhereArg} {
|
|
481
|
+
meta: ${root} ${aggWhereParam} { aggregate { count } }
|
|
482
|
+
}`;
|
|
483
|
+
const aggVars = {};
|
|
484
|
+
if (where) aggVars["where"] = where;
|
|
485
|
+
const metaData = await gql(
|
|
486
|
+
metaQuery,
|
|
487
|
+
where ? aggVars : void 0,
|
|
488
|
+
m.role,
|
|
489
|
+
signal
|
|
490
|
+
);
|
|
491
|
+
return metaData.meta.aggregate.count;
|
|
492
|
+
}
|
|
493
|
+
async function upsertDataBody(db, parentId, body, role, signal) {
|
|
494
|
+
const mutation = `mutation UpsertDataBody($object: ${db.table}_insert_input!) {
|
|
495
|
+
result: insert_${db.table}_one(
|
|
496
|
+
object: $object,
|
|
497
|
+
on_conflict: { constraint: ${db.constraint}, update_columns: [${db.column}] }
|
|
498
|
+
) { ${db.fk} }
|
|
499
|
+
}`;
|
|
500
|
+
await gql(mutation, { object: { [db.fk]: parentId, [db.column]: body } }, role, signal);
|
|
501
|
+
}
|
|
502
|
+
function mutationRecord(m, idField, item, resource, expectedId) {
|
|
503
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
504
|
+
throw {
|
|
505
|
+
kind: "protocol",
|
|
506
|
+
code: "record-not-found",
|
|
507
|
+
message: `Not found: ${resource}`
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const record = item;
|
|
511
|
+
const id = record[m.pkField ?? idField];
|
|
512
|
+
if (typeof id !== "string" && (typeof id !== "number" || !Number.isFinite(id))) {
|
|
513
|
+
throw {
|
|
514
|
+
kind: "protocol",
|
|
515
|
+
code: "mutation-record-id-missing",
|
|
516
|
+
message: `Mutation for '${resource}' returned no valid '${m.pkField ?? idField}'.`
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (expectedId !== void 0 && id !== expectedId) {
|
|
520
|
+
throw {
|
|
521
|
+
kind: "protocol",
|
|
522
|
+
code: "unexpected-record-id",
|
|
523
|
+
message: `Mutation for '${resource}' returned '${String(id)}' instead of '${String(expectedId)}'.`
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
return { kind: "record", id, record: item, completeness: "partial" };
|
|
527
|
+
}
|
|
528
|
+
async function readMutationRecord(m, id, signal) {
|
|
529
|
+
const fields = m.selectFields.join(" ");
|
|
530
|
+
const pkType = m.pkType ?? "bigint";
|
|
531
|
+
const pkField = m.pkField ?? "id";
|
|
532
|
+
const root = byPkRoot(m);
|
|
533
|
+
if (m.fixedFilter || root === null) {
|
|
534
|
+
const where = mergeWhere({ [pkField]: { _eq: id } }, m.fixedFilter);
|
|
535
|
+
const query2 = `query One_${m.table}($where: ${m.table}_bool_exp) {
|
|
536
|
+
items: ${m.table}(where: $where, limit: 1) { ${fields} }
|
|
537
|
+
}`;
|
|
538
|
+
const data2 = await gql(query2, { where }, m.role, signal);
|
|
539
|
+
return data2.items?.[0] ?? null;
|
|
540
|
+
}
|
|
541
|
+
const query = `query One_${m.table}($id: ${pkType}!) {
|
|
542
|
+
item: ${root}(${pkField}: $id) { ${fields} }
|
|
543
|
+
}`;
|
|
544
|
+
const data = await gql(query, { id }, m.role, signal);
|
|
545
|
+
return data.item;
|
|
546
|
+
}
|
|
547
|
+
return defineDataProvider({
|
|
548
|
+
queries: {
|
|
549
|
+
async list(descriptor, context) {
|
|
550
|
+
const m = mapping(descriptor.resource);
|
|
551
|
+
if (descriptor.pagination?.kind === "cursor") {
|
|
552
|
+
throw {
|
|
553
|
+
kind: "capability",
|
|
554
|
+
code: "cursor-pagination-unsupported",
|
|
555
|
+
message: "Hasura provider supports offset pagination only."
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const limit = descriptor.pagination?.kind === "offset" ? descriptor.pagination.pageSize : 25;
|
|
559
|
+
const page = descriptor.pagination?.kind === "offset" ? descriptor.pagination.page : 1;
|
|
560
|
+
const offset = (page - 1) * limit;
|
|
561
|
+
const where = composeListWhere(m, descriptor, context);
|
|
562
|
+
const sorters = descriptor.sort?.filter((s) => s.field);
|
|
563
|
+
let orderByExpr;
|
|
564
|
+
if (sorters && sorters.length > 0) {
|
|
565
|
+
orderByExpr = `[${sorters.map((s) => `{${s.field}: ${s.direction}}`).join(", ")}]`;
|
|
566
|
+
} else {
|
|
567
|
+
const defaultField = m.orderByField ?? "id";
|
|
568
|
+
orderByExpr = `{${defaultField}: desc}`;
|
|
569
|
+
}
|
|
570
|
+
const fields = m.selectFields.join(" ");
|
|
571
|
+
const whereArg = where ? ", $where: " + m.table + "_bool_exp" : "";
|
|
572
|
+
const whereParam = where ? ", where: $where" : "";
|
|
573
|
+
const itemsQuery = `query List_${m.table}($limit: Int, $offset: Int${whereArg}) {
|
|
574
|
+
items: ${m.table}(limit: $limit, offset: $offset, order_by: ${orderByExpr}${whereParam}) { ${fields} }
|
|
575
|
+
}`;
|
|
576
|
+
const itemsVars = { limit, offset };
|
|
577
|
+
if (where) itemsVars["where"] = where;
|
|
578
|
+
const itemsData = await gql(
|
|
579
|
+
itemsQuery,
|
|
580
|
+
itemsVars,
|
|
581
|
+
m.role,
|
|
582
|
+
context.signal
|
|
583
|
+
);
|
|
584
|
+
let total;
|
|
585
|
+
const listAggRoot = aggregateRoot(m);
|
|
586
|
+
if (!m.noAggregate && listAggRoot) {
|
|
587
|
+
try {
|
|
588
|
+
total = await runAggregateCount(m, listAggRoot, where, context.signal);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
console.warn(`[hasura] count failed for ${m.table}; rendering unknown-total`, err);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const spill = m.jsonbSpill;
|
|
594
|
+
const items = spill ? itemsData.items.map((it) => flattenJsonbSpill(it, spill)) : itemsData.items;
|
|
595
|
+
return { data: items, total };
|
|
596
|
+
},
|
|
597
|
+
/**
|
|
598
|
+
* On-demand row count — the answer to "how many are there?" on the lists
|
|
599
|
+
* that page without a grand total (`noAggregate`). Never fired implicitly:
|
|
600
|
+
* one source app measured ~55 s for a single COUNT, which is exactly why
|
|
601
|
+
* the list stopped issuing one on every page.
|
|
602
|
+
*
|
|
603
|
+
* `{ total: null }` means "not countable here at all" — the registry says
|
|
604
|
+
* this table has no aggregate root, or the resource isn't mapped by this
|
|
605
|
+
* provider. It is a DECISION, and the UI latches the affordance off for the
|
|
606
|
+
* session. A transient backend failure REJECTS instead (it propagates out of
|
|
607
|
+
* `gql`), so a retry stays possible; never conflate the two.
|
|
608
|
+
*
|
|
609
|
+
* The `where` comes from the same {@link composeListWhere} `getList` uses —
|
|
610
|
+
* same filters, same scope, same `fixedFilter` — so the number always
|
|
611
|
+
* describes the rows the list is paging through.
|
|
612
|
+
*/
|
|
613
|
+
async count(descriptor, context) {
|
|
614
|
+
const m = config.resources[descriptor.resource];
|
|
615
|
+
if (!m) return { total: null };
|
|
616
|
+
const root = aggregateRoot(m);
|
|
617
|
+
if (!root) return { total: null };
|
|
618
|
+
return {
|
|
619
|
+
total: await runAggregateCount(
|
|
620
|
+
m,
|
|
621
|
+
root,
|
|
622
|
+
composeListWhere(m, descriptor, context),
|
|
623
|
+
context.signal
|
|
624
|
+
)
|
|
625
|
+
};
|
|
626
|
+
},
|
|
627
|
+
async many(descriptor, context) {
|
|
628
|
+
const m = mapping(descriptor.resource);
|
|
629
|
+
if (descriptor.ids.length === 0) return { data: [] };
|
|
630
|
+
const pkField = m.pkField ?? "id";
|
|
631
|
+
const idsWhere = { [pkField]: { _in: descriptor.ids } };
|
|
632
|
+
const where = mergeWhere(
|
|
633
|
+
idsWhere,
|
|
634
|
+
composeListWhere(m, descriptor, context)
|
|
635
|
+
);
|
|
636
|
+
const fields = m.selectFields.join(" ");
|
|
637
|
+
const query = `query Many_${m.table}($where: ${m.table}_bool_exp) {
|
|
638
|
+
items: ${m.table}(where: $where) { ${fields} }
|
|
639
|
+
}`;
|
|
640
|
+
const data = await gql(query, { where }, m.role, context.signal);
|
|
641
|
+
const spill = m.jsonbSpill;
|
|
642
|
+
const items = spill ? data.items.map((item) => flattenJsonbSpill(item, spill)) : data.items;
|
|
643
|
+
return { data: items };
|
|
644
|
+
},
|
|
645
|
+
async one(descriptor, context) {
|
|
646
|
+
const m = mapping(descriptor.resource);
|
|
647
|
+
const dbSelect = m.dataBody ? ` ${m.dataBody.relationship} { ${m.dataBody.column} }` : "";
|
|
648
|
+
const fields = m.selectFields.join(" ") + dbSelect;
|
|
649
|
+
const pkType = m.pkType ?? "bigint";
|
|
650
|
+
const pkField = m.pkField ?? "id";
|
|
651
|
+
let item;
|
|
652
|
+
const root = byPkRoot(m);
|
|
653
|
+
if (m.fixedFilter || root === null) {
|
|
654
|
+
const where = mergeWhere({ [pkField]: { _eq: descriptor.id } }, m.fixedFilter);
|
|
655
|
+
const query = `query One_${m.table}($where: ${m.table}_bool_exp) {
|
|
656
|
+
items: ${m.table}(where: $where, limit: 1) { ${fields} }
|
|
657
|
+
}`;
|
|
658
|
+
const data = await gql(query, { where }, m.role, context.signal);
|
|
659
|
+
item = data.items?.[0] ?? null;
|
|
660
|
+
} else {
|
|
661
|
+
const query = `query One_${m.table}($id: ${pkType}!) {
|
|
662
|
+
item: ${root}(${pkField}: $id) { ${fields} }
|
|
663
|
+
}`;
|
|
664
|
+
const data = await gql(
|
|
665
|
+
query,
|
|
666
|
+
{ id: descriptor.id },
|
|
667
|
+
m.role,
|
|
668
|
+
context.signal
|
|
669
|
+
);
|
|
670
|
+
item = data.item;
|
|
671
|
+
}
|
|
672
|
+
if (item == null)
|
|
673
|
+
throw {
|
|
674
|
+
kind: "protocol",
|
|
675
|
+
code: "record-not-found",
|
|
676
|
+
message: `Not found: ${descriptor.resource}/${descriptor.id}`
|
|
677
|
+
};
|
|
678
|
+
let result = m.jsonbSpill ? flattenJsonbSpill(item, m.jsonbSpill) : item;
|
|
679
|
+
if (m.dataBody && result && typeof result === "object") {
|
|
680
|
+
const rec = { ...result };
|
|
681
|
+
const sibling = rec[m.dataBody.relationship];
|
|
682
|
+
const body = sibling && typeof sibling === "object" ? sibling[m.dataBody.column] : null;
|
|
683
|
+
delete rec[m.dataBody.relationship];
|
|
684
|
+
result = body && typeof body === "object" ? { ...rec, ...body } : rec;
|
|
685
|
+
}
|
|
686
|
+
return { data: result };
|
|
687
|
+
},
|
|
688
|
+
...hasCustomQueries ? {
|
|
689
|
+
async custom(descriptor, context) {
|
|
690
|
+
return executeCustomOperation(
|
|
691
|
+
descriptor.resource,
|
|
692
|
+
descriptor.operation,
|
|
693
|
+
descriptor.input,
|
|
694
|
+
"query",
|
|
695
|
+
context.signal
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
} : {}
|
|
699
|
+
},
|
|
700
|
+
mutations: {
|
|
701
|
+
async createOne(descriptor, context) {
|
|
702
|
+
const m = mapping(descriptor.resource);
|
|
703
|
+
const fields = m.selectFields.join(" ");
|
|
704
|
+
const rawInput = descriptor.input;
|
|
705
|
+
let mainInput = rawInput;
|
|
706
|
+
let dataBody;
|
|
707
|
+
if (m.dataBody) {
|
|
708
|
+
const split = partitionDataBody(rawInput, m.dataBody);
|
|
709
|
+
mainInput = split.rest;
|
|
710
|
+
dataBody = split.hasDataBody ? split.dataBody : void 0;
|
|
711
|
+
}
|
|
712
|
+
const object = m.jsonbSpill ? partitionJsonbSpill(mainInput, m.jsonbSpill, m.selectFields) : mainInput;
|
|
713
|
+
const query = `mutation Create_${m.table}($object: ${m.table}_insert_input!) {
|
|
714
|
+
item: insert_${m.table}_one(object: $object) { ${fields} }
|
|
715
|
+
}`;
|
|
716
|
+
const data = await gql(query, { object }, m.role, context.signal);
|
|
717
|
+
const result = mutationRecord(m, descriptor.idField, data.item, descriptor.resource);
|
|
718
|
+
if (m.dataBody && dataBody)
|
|
719
|
+
await upsertDataBody(m.dataBody, result.id, dataBody, m.role, context.signal);
|
|
720
|
+
return result;
|
|
721
|
+
},
|
|
722
|
+
async updateOne(descriptor, context) {
|
|
723
|
+
const m = mapping(descriptor.resource);
|
|
724
|
+
const fields = m.selectFields.join(" ");
|
|
725
|
+
const rawInput = descriptor.input;
|
|
726
|
+
let mainInput = rawInput;
|
|
727
|
+
let dataBody;
|
|
728
|
+
if (m.dataBody) {
|
|
729
|
+
const split = partitionDataBody(rawInput, m.dataBody);
|
|
730
|
+
mainInput = split.rest;
|
|
731
|
+
dataBody = split.hasDataBody ? split.dataBody : void 0;
|
|
732
|
+
}
|
|
733
|
+
const set = m.jsonbSpill ? partitionJsonbSpill(mainInput, m.jsonbSpill, m.selectFields) : mainInput;
|
|
734
|
+
const pkField = m.pkField ?? descriptor.idField;
|
|
735
|
+
const ifUpdatedAt = descriptor.providerOptions?.ifUpdatedAt;
|
|
736
|
+
let item;
|
|
737
|
+
if (Object.keys(set).length === 0) {
|
|
738
|
+
item = await readMutationRecord(m, descriptor.id, context.signal);
|
|
739
|
+
} else if (m.fixedFilter || ifUpdatedAt != null) {
|
|
740
|
+
let where = mergeWhere({ [pkField]: { _eq: descriptor.id } }, m.fixedFilter);
|
|
741
|
+
if (ifUpdatedAt != null) where = mergeWhere(where, { updated_at: { _eq: ifUpdatedAt } });
|
|
742
|
+
const query = `mutation Update_${m.table}($where: ${m.table}_bool_exp!, $set: ${m.table}_set_input!) {
|
|
743
|
+
result: update_${m.table}(where: $where, _set: $set) { affected_rows returning { ${fields} } }
|
|
744
|
+
}`;
|
|
745
|
+
const data = await gql(
|
|
746
|
+
query,
|
|
747
|
+
{ where, set },
|
|
748
|
+
m.role,
|
|
749
|
+
context.signal
|
|
750
|
+
);
|
|
751
|
+
if (ifUpdatedAt != null && ((data.result.affected_rows ?? 0) === 0 || (data.result.returning?.length ?? 0) === 0)) {
|
|
752
|
+
throw {
|
|
753
|
+
kind: "conflict",
|
|
754
|
+
code: "stale-record",
|
|
755
|
+
message: `Update for '${descriptor.resource}/${descriptor.id}' matched no row: the record was changed since it was loaded.`
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
item = data.result.returning?.[0] ?? null;
|
|
759
|
+
} else {
|
|
760
|
+
const pkType = m.pkType ?? "bigint";
|
|
761
|
+
const query = `mutation Update_${m.table}($id: ${pkType}!, $set: ${m.table}_set_input!) {
|
|
762
|
+
item: update_${m.table}_by_pk(pk_columns: {${pkField}: $id}, _set: $set) { ${fields} }
|
|
763
|
+
}`;
|
|
764
|
+
const data = await gql(
|
|
765
|
+
query,
|
|
766
|
+
{ id: descriptor.id, set },
|
|
767
|
+
m.role,
|
|
768
|
+
context.signal
|
|
769
|
+
);
|
|
770
|
+
item = data.item;
|
|
771
|
+
}
|
|
772
|
+
const result = mutationRecord(
|
|
773
|
+
m,
|
|
774
|
+
descriptor.idField,
|
|
775
|
+
item,
|
|
776
|
+
descriptor.resource,
|
|
777
|
+
descriptor.id
|
|
778
|
+
);
|
|
779
|
+
if (m.dataBody && dataBody)
|
|
780
|
+
await upsertDataBody(m.dataBody, result.id, dataBody, m.role, context.signal);
|
|
781
|
+
return result;
|
|
782
|
+
},
|
|
783
|
+
async deleteOne(descriptor, context) {
|
|
784
|
+
const m = mapping(descriptor.resource);
|
|
785
|
+
const fields = m.selectFields.join(" ");
|
|
786
|
+
const pkField = m.pkField ?? descriptor.idField;
|
|
787
|
+
let item;
|
|
788
|
+
if (m.fixedFilter) {
|
|
789
|
+
const where = mergeWhere({ [pkField]: { _eq: descriptor.id } }, m.fixedFilter);
|
|
790
|
+
const query = `mutation Delete_${m.table}($where: ${m.table}_bool_exp!) {
|
|
791
|
+
result: delete_${m.table}(where: $where) { affected_rows returning { ${fields} } }
|
|
792
|
+
}`;
|
|
793
|
+
const data = await gql(
|
|
794
|
+
query,
|
|
795
|
+
{ where },
|
|
796
|
+
m.role,
|
|
797
|
+
context.signal
|
|
798
|
+
);
|
|
799
|
+
item = data.result.returning?.[0] ?? null;
|
|
800
|
+
} else {
|
|
801
|
+
const pkType = m.pkType ?? "bigint";
|
|
802
|
+
const query = `mutation Delete_${m.table}($id: ${pkType}!) {
|
|
803
|
+
item: delete_${m.table}_by_pk(${pkField}: $id) { ${fields} }
|
|
804
|
+
}`;
|
|
805
|
+
const data = await gql(
|
|
806
|
+
query,
|
|
807
|
+
{ id: descriptor.id },
|
|
808
|
+
m.role,
|
|
809
|
+
context.signal
|
|
810
|
+
);
|
|
811
|
+
item = data.item;
|
|
812
|
+
}
|
|
813
|
+
mutationRecord(m, descriptor.idField, item, descriptor.resource, descriptor.id);
|
|
814
|
+
return { kind: "identity", id: descriptor.id };
|
|
815
|
+
},
|
|
816
|
+
...hasCustomMutations ? {
|
|
817
|
+
async custom(descriptor, context) {
|
|
818
|
+
return executeCustomOperation(
|
|
819
|
+
descriptor.resource,
|
|
820
|
+
descriptor.operation,
|
|
821
|
+
descriptor.input,
|
|
822
|
+
"mutation",
|
|
823
|
+
context.signal
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
} : {}
|
|
827
|
+
},
|
|
828
|
+
live: createHasuraLiveProvider({
|
|
829
|
+
resources: config.resources,
|
|
830
|
+
getToken: config.getToken,
|
|
831
|
+
endpoint,
|
|
832
|
+
role: defaultRole
|
|
833
|
+
})
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export { DEFAULT_HASURA_ENDPOINT, FILTER_OPERATOR_UNSUPPORTED_CODE, NOT_AUTHENTICATED_CODE, aggregateRoot, byPkRoot, createHasuraDataProvider, createHasuraLiveProvider, filtersToWhere, flattenJsonbSpill, hasuraWsUrl, mergeWhere, partitionDataBody, partitionJsonbSpill, scopeToWhere };
|
|
838
|
+
//# sourceMappingURL=index.js.map
|
|
839
|
+
//# sourceMappingURL=index.js.map
|