@burojs/hasura 0.4.0 → 0.5.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/README.md +15 -16
- package/dist/index.cjs +125 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -17
- package/dist/index.d.ts +36 -17
- package/dist/index.js +125 -47
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -56,22 +56,21 @@ registry rather than by branching on resource names:
|
|
|
56
56
|
| `dataBody` | route a heavy array payload into/out of a 1:1 sibling table |
|
|
57
57
|
| `live` | opt this resource into subscription-driven invalidation |
|
|
58
58
|
|
|
59
|
-
####
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
error envelope is a product decision this package has not taken.
|
|
59
|
+
#### Errors Hasura sends with HTTP 200
|
|
60
|
+
|
|
61
|
+
Hasura answers every GraphQL-level failure with HTTP 200, so the provider
|
|
62
|
+
classifies the envelope by `extensions.code`: `access-denied` and
|
|
63
|
+
`permission-error` reject as `{ kind: 'forbidden', code }`, `invalid-jwt` as
|
|
64
|
+
`{ kind: 'auth', code: 'invalid-jwt' }`, and every other code as
|
|
65
|
+
`{ kind: 'protocol', code: 'graphql-error' }`. When the app passes an
|
|
66
|
+
`authProvider` to `<BuroProvider>`, an `auth` rejection reaches its `onError`,
|
|
67
|
+
which decides whether to refresh; on `refreshed` the runtime replays a read once
|
|
68
|
+
(a mutation once when it is replay-safe). `config.recoverAuth` remains the hook
|
|
69
|
+
for an HTTP **401**, which a proxy or gateway in front of Hasura answers; it is
|
|
70
|
+
not consulted for the envelope. A mutation whose request was sent and got no
|
|
71
|
+
answer at all rejects as `{ kind: 'sent', code: 'hasura-request-sent',
|
|
72
|
+
retryable: false }`: it may have been applied. Pinned by
|
|
73
|
+
`tests/fixture-fidelity.test.ts` and `tests/error-kinds.test.ts`.
|
|
75
74
|
|
|
76
75
|
Optimistic concurrency is available per call: pass
|
|
77
76
|
`providerOptions.ifUpdatedAt` on an update and the mutation switches to a
|
package/dist/index.cjs
CHANGED
|
@@ -343,6 +343,63 @@ function flattenJsonbSpill(item, spill) {
|
|
|
343
343
|
return out;
|
|
344
344
|
}
|
|
345
345
|
const NOT_AUTHENTICATED_CODE = "not-authenticated";
|
|
346
|
+
const FORBIDDEN_CODES = new Set(["access-denied", "permission-error"]);
|
|
347
|
+
const HASURA_EXTERNAL_FAILURE_CODES = new Set([
|
|
348
|
+
"unexpected",
|
|
349
|
+
"remote-schema-error",
|
|
350
|
+
"parse-failed"
|
|
351
|
+
]);
|
|
352
|
+
const MUTATION_REFUSAL_CODES = new Map([["constraint-violation", "conflict"], ["validation-failed", "validation"]]);
|
|
353
|
+
function graphqlError(errors, operation, external) {
|
|
354
|
+
const message = errors.map((e) => e.message).join("; ");
|
|
355
|
+
const codes = errors.flatMap((e) => typeof e.extensions?.code === "string" ? [e.extensions.code] : []);
|
|
356
|
+
if (codes.includes("invalid-jwt")) return {
|
|
357
|
+
kind: "auth",
|
|
358
|
+
code: "invalid-jwt",
|
|
359
|
+
message,
|
|
360
|
+
details: errors
|
|
361
|
+
};
|
|
362
|
+
const outside = operation === "mutation" && external ? codes.find((code) => HASURA_EXTERNAL_FAILURE_CODES.has(code) || MUTATION_REFUSAL_CODES.has(code) || FORBIDDEN_CODES.has(code)) : void 0;
|
|
363
|
+
if (outside !== void 0) return {
|
|
364
|
+
kind: "sent",
|
|
365
|
+
code: outside,
|
|
366
|
+
message,
|
|
367
|
+
retryable: false,
|
|
368
|
+
details: errors
|
|
369
|
+
};
|
|
370
|
+
const forbidden = codes.find((code) => FORBIDDEN_CODES.has(code));
|
|
371
|
+
if (forbidden !== void 0) return {
|
|
372
|
+
kind: "forbidden",
|
|
373
|
+
code: forbidden,
|
|
374
|
+
message,
|
|
375
|
+
details: errors
|
|
376
|
+
};
|
|
377
|
+
if (operation === "mutation") for (const code of codes) {
|
|
378
|
+
const kind = MUTATION_REFUSAL_CODES.get(code);
|
|
379
|
+
if (kind !== void 0) return {
|
|
380
|
+
kind,
|
|
381
|
+
code,
|
|
382
|
+
message,
|
|
383
|
+
details: errors
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
kind: "protocol",
|
|
388
|
+
code: "graphql-error",
|
|
389
|
+
message,
|
|
390
|
+
details: errors
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function answerLost(code, message, status, cause) {
|
|
394
|
+
return {
|
|
395
|
+
kind: "sent",
|
|
396
|
+
code,
|
|
397
|
+
message,
|
|
398
|
+
status,
|
|
399
|
+
retryable: false,
|
|
400
|
+
...cause === void 0 ? {} : { cause }
|
|
401
|
+
};
|
|
402
|
+
}
|
|
346
403
|
const LOCALE_REQUIRED_CODE = "locale-required";
|
|
347
404
|
function localeWhere(m, locale) {
|
|
348
405
|
if (m.localeField === void 0) return void 0;
|
|
@@ -499,8 +556,12 @@ function createHasuraDataProvider(config) {
|
|
|
499
556
|
const operations = snapshotOperationRegistry(config.operations);
|
|
500
557
|
const hasCustomQueries = hasRegisteredOperationKind(operations, "query");
|
|
501
558
|
const hasCustomMutations = hasRegisteredOperationKind(operations, "mutation");
|
|
502
|
-
async function gql(query, variables, role, signal) {
|
|
559
|
+
async function gql(query, variables, role, signal, operation, external = false) {
|
|
503
560
|
try {
|
|
561
|
+
const requestBody = JSON.stringify({
|
|
562
|
+
query,
|
|
563
|
+
variables
|
|
564
|
+
});
|
|
504
565
|
const request = async (token) => {
|
|
505
566
|
const headers = {
|
|
506
567
|
"Content-Type": "application/json",
|
|
@@ -508,15 +569,24 @@ function createHasuraDataProvider(config) {
|
|
|
508
569
|
};
|
|
509
570
|
const effectiveRole = role === void 0 ? defaultRole : normalizeRole(role);
|
|
510
571
|
if (effectiveRole !== null) headers["X-Hasura-Role"] = effectiveRole;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
})
|
|
572
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
573
|
+
try {
|
|
574
|
+
return await fetchImpl(endpoint, {
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers,
|
|
577
|
+
body: requestBody,
|
|
578
|
+
signal
|
|
579
|
+
});
|
|
580
|
+
} catch (cause) {
|
|
581
|
+
if (operation === "mutation" && !signal.aborted) throw {
|
|
582
|
+
kind: "sent",
|
|
583
|
+
code: "hasura-request-sent",
|
|
584
|
+
message: "Hasura did not answer a mutation; it may have been applied.",
|
|
585
|
+
retryable: false,
|
|
586
|
+
cause
|
|
587
|
+
};
|
|
588
|
+
throw cause;
|
|
589
|
+
}
|
|
520
590
|
};
|
|
521
591
|
let token = await config.getToken();
|
|
522
592
|
if (!token) throw {
|
|
@@ -534,24 +604,32 @@ function createHasuraDataProvider(config) {
|
|
|
534
604
|
};
|
|
535
605
|
res = await request(token);
|
|
536
606
|
}
|
|
537
|
-
if (!res.ok)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
607
|
+
if (!res.ok) {
|
|
608
|
+
if (operation === "mutation" && res.status >= 500) throw answerLost("hasura-answer-lost", `GraphQL HTTP ${res.status} after a mutation was sent; it may have been applied.`, res.status);
|
|
609
|
+
throw {
|
|
610
|
+
kind: "transport",
|
|
611
|
+
code: "graphql-http-error",
|
|
612
|
+
message: `GraphQL HTTP ${res.status}`,
|
|
613
|
+
status: res.status
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
let body;
|
|
617
|
+
try {
|
|
618
|
+
body = await res.json();
|
|
619
|
+
if (body === null || typeof body !== "object") throw new TypeError("The GraphQL answer is not an object.");
|
|
620
|
+
} catch (cause) {
|
|
621
|
+
if (operation === "mutation" && !signal.aborted) throw answerLost("hasura-answer-unreadable", "Hasura answered a mutation with a body that could not be read; it may have been applied.", res.status, cause);
|
|
622
|
+
throw cause;
|
|
623
|
+
}
|
|
624
|
+
if (body.errors?.length) throw graphqlError(body.errors, operation, external);
|
|
625
|
+
if (body.data === void 0) {
|
|
626
|
+
if (operation === "mutation") throw answerLost("hasura-answer-unreadable", "Hasura answered a mutation without data or errors; it may have been applied.", res.status);
|
|
627
|
+
throw {
|
|
628
|
+
kind: "protocol",
|
|
629
|
+
code: "empty-graphql-response",
|
|
630
|
+
message: "Empty GraphQL response."
|
|
631
|
+
};
|
|
632
|
+
}
|
|
555
633
|
return body.data;
|
|
556
634
|
} catch (error) {
|
|
557
635
|
throw hasuraError(error, signal, "Hasura request failed.");
|
|
@@ -577,8 +655,8 @@ function createHasuraDataProvider(config) {
|
|
|
577
655
|
}
|
|
578
656
|
async function executeCustomOperation(resource, operationName, input, kind, signal) {
|
|
579
657
|
const operation = registeredOperation(resource, operationName, kind);
|
|
580
|
-
const result = await gql(operation.document, operation.variables(input), operation.role ?? config.resources[resource]?.role, signal);
|
|
581
|
-
return operation.mapResult
|
|
658
|
+
const result = await gql(operation.document, operation.variables(input), operation.role ?? config.resources[resource]?.role, signal, kind, kind === "mutation");
|
|
659
|
+
return operation.mapResult === void 0 ? result : operation.mapResult(result);
|
|
582
660
|
}
|
|
583
661
|
function composeListWhere(m, descriptor, context) {
|
|
584
662
|
const userWhere = filtersToWhere(coerceInt64Filter(descriptor.filter, m.int64Fields));
|
|
@@ -597,7 +675,7 @@ function createHasuraDataProvider(config) {
|
|
|
597
675
|
}`;
|
|
598
676
|
const aggVars = {};
|
|
599
677
|
if (where) aggVars.where = where;
|
|
600
|
-
return (await gql(metaQuery, where ? aggVars : void 0, m.role, signal)).meta.aggregate.count;
|
|
678
|
+
return (await gql(metaQuery, where ? aggVars : void 0, m.role, signal, "query")).meta.aggregate.count;
|
|
601
679
|
}
|
|
602
680
|
async function upsertDataBody(db, parentId, body, role, signal) {
|
|
603
681
|
await gql(`mutation UpsertDataBody($object: ${db.table}_insert_input!) {
|
|
@@ -608,11 +686,11 @@ function createHasuraDataProvider(config) {
|
|
|
608
686
|
}`, { object: {
|
|
609
687
|
[db.fk]: parentId,
|
|
610
688
|
[db.column]: body
|
|
611
|
-
} }, role, signal);
|
|
689
|
+
} }, role, signal, "mutation");
|
|
612
690
|
}
|
|
613
691
|
function mutationRecord(m, idField, item, resource, expectedId) {
|
|
614
692
|
if (item === null || typeof item !== "object" || Array.isArray(item)) throw {
|
|
615
|
-
kind: "
|
|
693
|
+
kind: "not-found",
|
|
616
694
|
code: "record-not-found",
|
|
617
695
|
message: `Not found: ${resource}`
|
|
618
696
|
};
|
|
@@ -643,11 +721,11 @@ function createHasuraDataProvider(config) {
|
|
|
643
721
|
const where = mergeWhere({ [pkField]: { _eq: id } }, m.fixedFilter);
|
|
644
722
|
return (await gql(`query One_${m.table}($where: ${m.table}_bool_exp) {
|
|
645
723
|
items: ${m.table}(where: $where, limit: 1) { ${fields} }
|
|
646
|
-
}`, { where }, m.role, signal)).items?.[0] ?? null;
|
|
724
|
+
}`, { where }, m.role, signal, "query")).items?.[0] ?? null;
|
|
647
725
|
}
|
|
648
726
|
return (await gql(`query One_${m.table}($id: ${pkType}!) {
|
|
649
727
|
item: ${root}(${pkField}: $id) { ${fields} }
|
|
650
|
-
}`, { id }, m.role, signal)).item;
|
|
728
|
+
}`, { id }, m.role, signal, "query")).item;
|
|
651
729
|
}
|
|
652
730
|
async function listByCursor(m, descriptor, pagination, context) {
|
|
653
731
|
if (pagination.before !== void 0) throw {
|
|
@@ -670,7 +748,7 @@ function createHasuraDataProvider(config) {
|
|
|
670
748
|
}`;
|
|
671
749
|
const itemsVars = { limit: pagination.limit + 1 };
|
|
672
750
|
if (where) itemsVars.where = where;
|
|
673
|
-
const fetched = (await gql(itemsQuery, itemsVars, m.role, context.signal)).items ?? [];
|
|
751
|
+
const fetched = (await gql(itemsQuery, itemsVars, m.role, context.signal, "query")).items ?? [];
|
|
674
752
|
const hasNextPage = fetched.length > pagination.limit;
|
|
675
753
|
const rows = hasNextPage ? fetched.slice(0, pagination.limit) : fetched;
|
|
676
754
|
let total;
|
|
@@ -715,7 +793,7 @@ function createHasuraDataProvider(config) {
|
|
|
715
793
|
offset
|
|
716
794
|
};
|
|
717
795
|
if (where) itemsVars.where = where;
|
|
718
|
-
const itemsData = await gql(itemsQuery, itemsVars, m.role, context.signal);
|
|
796
|
+
const itemsData = await gql(itemsQuery, itemsVars, m.role, context.signal, "query");
|
|
719
797
|
let total;
|
|
720
798
|
const listAggRoot = aggregateRoot(m);
|
|
721
799
|
if (!m.noAggregate && listAggRoot) try {
|
|
@@ -744,7 +822,7 @@ function createHasuraDataProvider(config) {
|
|
|
744
822
|
const fields = m.selectFields.join(" ");
|
|
745
823
|
const data = await gql(`query Many_${m.table}($where: ${m.table}_bool_exp) {
|
|
746
824
|
items: ${m.table}(where: $where) { ${fields} }
|
|
747
|
-
}`, { where }, m.role, context.signal);
|
|
825
|
+
}`, { where }, m.role, context.signal, "query");
|
|
748
826
|
const spill = m.jsonbSpill;
|
|
749
827
|
return { data: spill ? data.items.map((item) => flattenJsonbSpill(item, spill)) : data.items };
|
|
750
828
|
},
|
|
@@ -761,12 +839,12 @@ function createHasuraDataProvider(config) {
|
|
|
761
839
|
const where = mergeWhere(mergeWhere({ [pkField]: { _eq: descriptor.id } }, m.fixedFilter), localeSlice);
|
|
762
840
|
item = (await gql(`query One_${m.table}($where: ${m.table}_bool_exp) {
|
|
763
841
|
items: ${m.table}(where: $where, limit: 1) { ${fields} }
|
|
764
|
-
}`, { where }, m.role, context.signal)).items?.[0] ?? null;
|
|
842
|
+
}`, { where }, m.role, context.signal, "query")).items?.[0] ?? null;
|
|
765
843
|
} else item = (await gql(`query One_${m.table}($id: ${pkType}!) {
|
|
766
844
|
item: ${root}(${pkField}: $id) { ${fields} }
|
|
767
|
-
}`, { id: descriptor.id }, m.role, context.signal)).item;
|
|
845
|
+
}`, { id: descriptor.id }, m.role, context.signal, "query")).item;
|
|
768
846
|
if (item == null) throw {
|
|
769
|
-
kind: "
|
|
847
|
+
kind: "not-found",
|
|
770
848
|
code: "record-not-found",
|
|
771
849
|
message: `Not found: ${descriptor.resource}/${descriptor.id}`
|
|
772
850
|
};
|
|
@@ -802,7 +880,7 @@ function createHasuraDataProvider(config) {
|
|
|
802
880
|
const object = m.jsonbSpill ? partitionJsonbSpill(mainInput, m.jsonbSpill, m.selectFields) : mainInput;
|
|
803
881
|
const data = await gql(`mutation Create_${m.table}($object: ${m.table}_insert_input!) {
|
|
804
882
|
item: insert_${m.table}_one(object: $object) { ${fields} }
|
|
805
|
-
}`, { object }, m.role, context.signal);
|
|
883
|
+
}`, { object }, m.role, context.signal, "mutation");
|
|
806
884
|
const result = mutationRecord(m, descriptor.idField, data.item, descriptor.resource);
|
|
807
885
|
if (m.dataBody && dataBody) await upsertDataBody(m.dataBody, result.id, dataBody, m.role, context.signal);
|
|
808
886
|
return result;
|
|
@@ -831,7 +909,7 @@ function createHasuraDataProvider(config) {
|
|
|
831
909
|
}`, {
|
|
832
910
|
where,
|
|
833
911
|
set
|
|
834
|
-
}, m.role, context.signal);
|
|
912
|
+
}, m.role, context.signal, "mutation");
|
|
835
913
|
if (ifUpdatedAt != null && ((data.result.affected_rows ?? 0) === 0 || (data.result.returning?.length ?? 0) === 0)) throw {
|
|
836
914
|
kind: "conflict",
|
|
837
915
|
code: "stale-record",
|
|
@@ -845,7 +923,7 @@ function createHasuraDataProvider(config) {
|
|
|
845
923
|
}`, {
|
|
846
924
|
id: descriptor.id,
|
|
847
925
|
set
|
|
848
|
-
}, m.role, context.signal)).item;
|
|
926
|
+
}, m.role, context.signal, "mutation")).item;
|
|
849
927
|
}
|
|
850
928
|
const result = mutationRecord(m, descriptor.idField, item, descriptor.resource, descriptor.id);
|
|
851
929
|
if (m.dataBody && dataBody) await upsertDataBody(m.dataBody, result.id, dataBody, m.role, context.signal);
|
|
@@ -860,12 +938,12 @@ function createHasuraDataProvider(config) {
|
|
|
860
938
|
const where = mergeWhere({ [pkField]: { _eq: descriptor.id } }, m.fixedFilter);
|
|
861
939
|
item = (await gql(`mutation Delete_${m.table}($where: ${m.table}_bool_exp!) {
|
|
862
940
|
result: delete_${m.table}(where: $where) { affected_rows returning { ${fields} } }
|
|
863
|
-
}`, { where }, m.role, context.signal)).result.returning?.[0] ?? null;
|
|
941
|
+
}`, { where }, m.role, context.signal, "mutation")).result.returning?.[0] ?? null;
|
|
864
942
|
} else {
|
|
865
943
|
const pkType = m.pkType ?? "bigint";
|
|
866
944
|
item = (await gql(`mutation Delete_${m.table}($id: ${pkType}!) {
|
|
867
945
|
item: delete_${m.table}_by_pk(${pkField}: $id) { ${fields} }
|
|
868
|
-
}`, { id: descriptor.id }, m.role, context.signal)).item;
|
|
946
|
+
}`, { id: descriptor.id }, m.role, context.signal, "mutation")).item;
|
|
869
947
|
}
|
|
870
948
|
mutationRecord(m, descriptor.idField, item, descriptor.resource, descriptor.id);
|
|
871
949
|
return {
|
|
@@ -889,6 +967,7 @@ function createHasuraDataProvider(config) {
|
|
|
889
967
|
exports.DEFAULT_HASURA_ENDPOINT = DEFAULT_HASURA_ENDPOINT;
|
|
890
968
|
exports.FILTER_OPERATOR_UNSUPPORTED_CODE = FILTER_OPERATOR_UNSUPPORTED_CODE;
|
|
891
969
|
exports.HASURA_CURSOR_CODES = HASURA_CURSOR_CODES;
|
|
970
|
+
exports.HASURA_EXTERNAL_FAILURE_CODES = HASURA_EXTERNAL_FAILURE_CODES;
|
|
892
971
|
exports.LOCALE_REQUIRED_CODE = LOCALE_REQUIRED_CODE;
|
|
893
972
|
exports.NOT_AUTHENTICATED_CODE = NOT_AUTHENTICATED_CODE;
|
|
894
973
|
exports.aggregateRoot = aggregateRoot;
|