@m1212e/rumble 0.23.18 → 0.23.20
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/out/index.cjs +228 -9
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +136 -1
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +136 -1
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +229 -11
- package/out/index.mjs.map +1 -1
- package/package.json +6 -2
package/out/index.cjs
CHANGED
|
@@ -46,9 +46,14 @@ let _pothos_plugin_smart_subscriptions = require("@pothos/plugin-smart-subscript
|
|
|
46
46
|
_pothos_plugin_smart_subscriptions = __toESM(_pothos_plugin_smart_subscriptions, 1);
|
|
47
47
|
let _pothos_plugin_tracing = require("@pothos/plugin-tracing");
|
|
48
48
|
_pothos_plugin_tracing = __toESM(_pothos_plugin_tracing, 1);
|
|
49
|
+
let _pothos_plugin_validation = require("@pothos/plugin-validation");
|
|
50
|
+
_pothos_plugin_validation = __toESM(_pothos_plugin_validation, 1);
|
|
49
51
|
let graphql_scalars = require("graphql-scalars");
|
|
50
52
|
let dataloader = require("dataloader");
|
|
51
53
|
dataloader = __toESM(dataloader, 1);
|
|
54
|
+
let lib_address = require("lib-address");
|
|
55
|
+
let zod = require("zod");
|
|
56
|
+
let libphonenumber_js = require("libphonenumber-js");
|
|
52
57
|
//#region lib/types/rumbleError.ts
|
|
53
58
|
/**
|
|
54
59
|
* An error that gets raised by rumble whenever something does not go according to plan.
|
|
@@ -211,7 +216,7 @@ function mapNullFieldsToUndefined(obj) {
|
|
|
211
216
|
}
|
|
212
217
|
//#endregion
|
|
213
218
|
//#region package.json
|
|
214
|
-
var version = "0.23.
|
|
219
|
+
var version = "0.23.20";
|
|
215
220
|
//#endregion
|
|
216
221
|
//#region lib/helpers/errorLogging.ts
|
|
217
222
|
function hasMessage(error) {
|
|
@@ -592,6 +597,24 @@ function recordSpanErrors(span, errors) {
|
|
|
592
597
|
function recordSpanError(span, error) {
|
|
593
598
|
recordSpanErrors(span, [error]);
|
|
594
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Errors the operation wrapper has already reported.
|
|
602
|
+
*
|
|
603
|
+
* A transport can see the same failure twice: sofa, for instance, hands
|
|
604
|
+
* `result.errors` to its error handler *after* execute has returned, so without
|
|
605
|
+
* this the very same error would produce two log lines and two exception events,
|
|
606
|
+
* which quietly doubles error counts in dashboards. A WeakSet keeps this free of
|
|
607
|
+
* bookkeeping — entries disappear with the errors themselves.
|
|
608
|
+
*/
|
|
609
|
+
const reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
610
|
+
function markErrorsReported(errors) {
|
|
611
|
+
for (const error of errors) if (typeof error === "object" && error !== null) reportedErrors.add(error);
|
|
612
|
+
}
|
|
613
|
+
/** True when every error was already reported by the operation wrapper. */
|
|
614
|
+
function errorsAlreadyReported(errors) {
|
|
615
|
+
if (errors.length === 0) return false;
|
|
616
|
+
return errors.every((error) => typeof error === "object" && error !== null && reportedErrors.has(error));
|
|
617
|
+
}
|
|
595
618
|
function safeJsonStringify(value) {
|
|
596
619
|
try {
|
|
597
620
|
return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner) ?? "null";
|
|
@@ -1632,6 +1655,7 @@ function buildTracedExecute(executeFn, rumbleInput, transport) {
|
|
|
1632
1655
|
...errorsLogField(result.errors)
|
|
1633
1656
|
}, "graphql execute completed with errors");
|
|
1634
1657
|
recordSpanErrors(span, result.errors);
|
|
1658
|
+
markErrorsReported(result.errors);
|
|
1635
1659
|
} else log?.info({
|
|
1636
1660
|
...telemetry.logFields,
|
|
1637
1661
|
[FIELD_DURATION_MS]: durationMs(start)
|
|
@@ -1644,6 +1668,7 @@ function buildTracedExecute(executeFn, rumbleInput, transport) {
|
|
|
1644
1668
|
...errorLogField(error)
|
|
1645
1669
|
}, "graphql execute threw");
|
|
1646
1670
|
recordSpanError(span, error);
|
|
1671
|
+
markErrorsReported([error]);
|
|
1647
1672
|
throw error;
|
|
1648
1673
|
}
|
|
1649
1674
|
};
|
|
@@ -1685,6 +1710,7 @@ function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
|
|
|
1685
1710
|
...errorsLogField(execResult.errors)
|
|
1686
1711
|
}, "graphql subscribe completed with errors");
|
|
1687
1712
|
recordSpanErrors(span, execResult.errors);
|
|
1713
|
+
markErrorsReported(execResult.errors);
|
|
1688
1714
|
}
|
|
1689
1715
|
return execResult;
|
|
1690
1716
|
}
|
|
@@ -1706,6 +1732,7 @@ function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
|
|
|
1706
1732
|
...errorLogField(error)
|
|
1707
1733
|
}, "graphql subscribe threw");
|
|
1708
1734
|
recordSpanError(span, error);
|
|
1735
|
+
markErrorsReported([error]);
|
|
1709
1736
|
throw error;
|
|
1710
1737
|
}
|
|
1711
1738
|
};
|
|
@@ -2613,8 +2640,191 @@ function registerRuntimeFiltersPlugin() {
|
|
|
2613
2640
|
}
|
|
2614
2641
|
}
|
|
2615
2642
|
//#endregion
|
|
2643
|
+
//#region lib/scalars/addressValidation.ts
|
|
2644
|
+
const FIELD_PATH_MAP = {
|
|
2645
|
+
addressLine1: "streetAddress",
|
|
2646
|
+
addressLine2: "streetAddress",
|
|
2647
|
+
addressLine3: "streetAddress",
|
|
2648
|
+
city: "locality",
|
|
2649
|
+
dependentLocality: "locality",
|
|
2650
|
+
state: "region",
|
|
2651
|
+
zip: "postalCode",
|
|
2652
|
+
sortingCode: "postalCode"
|
|
2653
|
+
};
|
|
2654
|
+
/**
|
|
2655
|
+
* Fixes the unambiguous, purely-cosmetic issues (stray surrounding whitespace, wrong case)
|
|
2656
|
+
* before validation runs, rather than rejecting a request over them. This runs as a Zod
|
|
2657
|
+
* `transform`, and `@pothos/plugin-validation` uses a transform's *output* as the field's
|
|
2658
|
+
* actual value — so the corrected value, not the original one, is what resolvers receive.
|
|
2659
|
+
*
|
|
2660
|
+
* `countryCode` and `postalCode` are uppercased since their canonical forms always are (ISO
|
|
2661
|
+
* 3166-1 codes, and postal codes in every country that mixes letters and digits, e.g. GB/CA/NL).
|
|
2662
|
+
* `region`/`locality` are only trimmed, not case-changed: unlike country/postal codes, a region
|
|
2663
|
+
* is sometimes a free-text place name rather than a code (lib-address's own subdivision lookup
|
|
2664
|
+
* is already case-insensitive, so case coercion there wouldn't fix anything a rejection-worthy
|
|
2665
|
+
* case wouldn't already pass).
|
|
2666
|
+
*/
|
|
2667
|
+
const normalizeAddress = (address) => ({
|
|
2668
|
+
streetAddress: address.streetAddress.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n") || null,
|
|
2669
|
+
locality: address.locality?.trim() || null,
|
|
2670
|
+
region: address.region?.trim() || null,
|
|
2671
|
+
postalCode: address.postalCode?.trim().toUpperCase() || null,
|
|
2672
|
+
countryCode: address.countryCode.trim().toUpperCase()
|
|
2673
|
+
});
|
|
2674
|
+
const addressValidationSchema = zod.z.object({
|
|
2675
|
+
streetAddress: zod.z.string(),
|
|
2676
|
+
locality: zod.z.string().nullable().optional(),
|
|
2677
|
+
region: zod.z.string().nullable().optional(),
|
|
2678
|
+
postalCode: zod.z.string().nullable().optional(),
|
|
2679
|
+
countryCode: zod.z.string()
|
|
2680
|
+
}).transform(normalizeAddress).superRefine((address, ctx) => {
|
|
2681
|
+
if (!address.countryCode) {
|
|
2682
|
+
ctx.addIssue({
|
|
2683
|
+
code: "custom",
|
|
2684
|
+
path: ["countryCode"],
|
|
2685
|
+
message: "countryCode is required."
|
|
2686
|
+
});
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
if (!(0, lib_address.isValidCountryCode)(address.countryCode)) {
|
|
2690
|
+
ctx.addIssue({
|
|
2691
|
+
code: "custom",
|
|
2692
|
+
path: ["countryCode"],
|
|
2693
|
+
message: `Unknown country code: ${address.countryCode}`
|
|
2694
|
+
});
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const [addressLine1, addressLine2, addressLine3] = address.streetAddress ? address.streetAddress.split("\n") : [];
|
|
2698
|
+
try {
|
|
2699
|
+
(0, lib_address.validateAddress)({
|
|
2700
|
+
country: address.countryCode,
|
|
2701
|
+
addressLine1,
|
|
2702
|
+
addressLine2,
|
|
2703
|
+
addressLine3,
|
|
2704
|
+
city: address.locality ?? void 0,
|
|
2705
|
+
state: address.region?.toUpperCase() ?? void 0,
|
|
2706
|
+
zip: address.postalCode ?? void 0
|
|
2707
|
+
});
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
if (error instanceof lib_address.CountryMissingError) {
|
|
2710
|
+
ctx.addIssue({
|
|
2711
|
+
code: "custom",
|
|
2712
|
+
path: ["countryCode"],
|
|
2713
|
+
message: error.message
|
|
2714
|
+
});
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
if (!(error instanceof lib_address.AddressValidationError)) throw error;
|
|
2718
|
+
for (const subError of error.errors) if (subError instanceof lib_address.MissingFieldError) {
|
|
2719
|
+
const path = FIELD_PATH_MAP[subError.field];
|
|
2720
|
+
if (!path) continue;
|
|
2721
|
+
ctx.addIssue({
|
|
2722
|
+
code: "custom",
|
|
2723
|
+
path: [path],
|
|
2724
|
+
message: `${path} is required for country ${address.countryCode}.`
|
|
2725
|
+
});
|
|
2726
|
+
} else if (subError instanceof lib_address.InvalidZipError) ctx.addIssue({
|
|
2727
|
+
code: "custom",
|
|
2728
|
+
path: ["postalCode"],
|
|
2729
|
+
message: `Invalid postal code for country ${address.countryCode}.`
|
|
2730
|
+
});
|
|
2731
|
+
else if (subError instanceof lib_address.InvalidStateError) ctx.addIssue({
|
|
2732
|
+
code: "custom",
|
|
2733
|
+
path: ["region"],
|
|
2734
|
+
message: `Invalid region for country ${address.countryCode}.`
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
//#endregion
|
|
2739
|
+
//#region lib/scalars/address.ts
|
|
2740
|
+
function implementDefaultAddressTypes(schemaBuilder) {
|
|
2741
|
+
return {
|
|
2742
|
+
Address: schemaBuilder.objectRef("Address").implement({
|
|
2743
|
+
description: "A postal address, modeled after the OIDC `address` claim and schema.org's PostalAddress.",
|
|
2744
|
+
fields: (t) => ({
|
|
2745
|
+
streetAddress: t.exposeString("streetAddress", { nullable: true }),
|
|
2746
|
+
locality: t.exposeString("locality", { nullable: true }),
|
|
2747
|
+
region: t.exposeString("region", { nullable: true }),
|
|
2748
|
+
postalCode: t.exposeString("postalCode", { nullable: true }),
|
|
2749
|
+
countryCode: t.exposeString("countryCode", { nullable: true })
|
|
2750
|
+
})
|
|
2751
|
+
}),
|
|
2752
|
+
AddressInput: schemaBuilder.inputRef("AddressInput").implement({
|
|
2753
|
+
description: "Input counterpart of Address. countryCode and streetAddress are required — countryCode because there is nothing to validate the other fields against without it, and streetAddress because it's the one field required in every country. The remaining fields are optional since their requiredness genuinely varies by country; validated as a whole using Google's libaddressinput metadata (offline).",
|
|
2754
|
+
validate: addressValidationSchema,
|
|
2755
|
+
fields: (t) => ({
|
|
2756
|
+
streetAddress: t.string({ required: true }),
|
|
2757
|
+
locality: t.string({ required: false }),
|
|
2758
|
+
region: t.string({ required: false }),
|
|
2759
|
+
postalCode: t.string({ required: false }),
|
|
2760
|
+
countryCode: t.string({ required: true })
|
|
2761
|
+
})
|
|
2762
|
+
})
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
//#endregion
|
|
2766
|
+
//#region lib/scalars/personName.ts
|
|
2767
|
+
const MAX_LENGTH = 200;
|
|
2768
|
+
const NAME_REGEX = /^[\p{L}\p{M}]+(?:(?:[ '-]|\.\x20?)[\p{L}\p{M}]+)*\.?$/u;
|
|
2769
|
+
/**
|
|
2770
|
+
* Normalizes and validates a person's name.
|
|
2771
|
+
*/
|
|
2772
|
+
const normalize = (value) => {
|
|
2773
|
+
if (typeof value !== "string") throw new RumbleErrorSafe(`Name value is not a string: ${value}`);
|
|
2774
|
+
const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\p{Zs}+/gu, " ").trim().replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
|
|
2775
|
+
if (normalized.length === 0) throw new RumbleErrorSafe("Name must not be empty.");
|
|
2776
|
+
if (normalized.length > MAX_LENGTH) throw new RumbleErrorSafe(`Name must not be longer than ${MAX_LENGTH} characters.`);
|
|
2777
|
+
if (!NAME_REGEX.test(normalized)) throw new RumbleErrorSafe(`Invalid name: ${value}. Names may only contain letters and the separators space, apostrophe, hyphen and period, and may not start with or repeat a separator.`);
|
|
2778
|
+
return normalized;
|
|
2779
|
+
};
|
|
2780
|
+
const PersonNameResolver = new graphql.GraphQLScalarType({
|
|
2781
|
+
name: "PersonName",
|
|
2782
|
+
description: "A person's name. Unicode letters from any script are allowed, along with space, apostrophe, hyphen and period as separators between name parts. The value is Unicode NFC normalized, has invisible/control characters stripped, has its whitespace collapsed, and has unambiguous typos (a doubled or stray leading/trailing hyphen/apostrophe) silently corrected rather than rejected.",
|
|
2783
|
+
serialize: normalize,
|
|
2784
|
+
parseValue: normalize,
|
|
2785
|
+
parseLiteral(ast) {
|
|
2786
|
+
if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as names but got a: ${ast.kind}`);
|
|
2787
|
+
return normalize(ast.value);
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
//#endregion
|
|
2791
|
+
//#region lib/scalars/phoneNumber.ts
|
|
2792
|
+
/**
|
|
2793
|
+
* Parses and validates a phone number using libphonenumber-js (Google's phone number metadata),
|
|
2794
|
+
* which checks real per-country validity (length, area code plausibility, ...) rather than just
|
|
2795
|
+
* the E.164 shape.
|
|
2796
|
+
*/
|
|
2797
|
+
const parse = (value) => {
|
|
2798
|
+
if (typeof value !== "string") throw new RumbleErrorSafe(`Phone number value is not a string: ${value}`);
|
|
2799
|
+
const phoneNumber = (0, libphonenumber_js.parsePhoneNumberFromString)(value);
|
|
2800
|
+
if (!phoneNumber?.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${value}. Please provide a valid phone number including its country calling code, e.g. +4915123456789.`);
|
|
2801
|
+
return phoneNumber;
|
|
2802
|
+
};
|
|
2803
|
+
/**
|
|
2804
|
+
* Resolvers may either return an already-parsed PhoneNumber (e.g. one obtained via `parse`
|
|
2805
|
+
* above) or a raw string, which is parsed and validated the same way input values are.
|
|
2806
|
+
*/
|
|
2807
|
+
const serialize = (value) => {
|
|
2808
|
+
if (value && typeof value === "object" && typeof value.isValid === "function") {
|
|
2809
|
+
const phoneNumber = value;
|
|
2810
|
+
if (!phoneNumber.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${phoneNumber.number}`);
|
|
2811
|
+
return phoneNumber.number;
|
|
2812
|
+
}
|
|
2813
|
+
return parse(value).number;
|
|
2814
|
+
};
|
|
2815
|
+
const PhoneNumberResolver = new graphql.GraphQLScalarType({
|
|
2816
|
+
name: "PhoneNumber",
|
|
2817
|
+
description: "A phone number, validated using real per-country validity rules and represented as a parsed libphonenumber-js PhoneNumber. On the wire it is transported as the E.164 format (https://en.wikipedia.org/wiki/E.164), e.g. +4915123456789. The country calling code is required.",
|
|
2818
|
+
serialize,
|
|
2819
|
+
parseValue: parse,
|
|
2820
|
+
parseLiteral(ast) {
|
|
2821
|
+
if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as phone numbers but got a: ${ast.kind}`);
|
|
2822
|
+
return parse(ast.value);
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
//#endregion
|
|
2616
2826
|
//#region lib/schemaBuilder.ts
|
|
2617
|
-
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
2827
|
+
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
|
|
2618
2828
|
const createSpan = otel?.enabled && otel.tracer ? (0, _pothos_tracing_opentelemetry.createOpenTelemetryWrapper)(otel.tracer, otel.options) : void 0;
|
|
2619
2829
|
registerRuntimeFiltersPlugin();
|
|
2620
2830
|
const schemaBuilder = new _pothos_core.default({
|
|
@@ -2624,6 +2834,7 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2624
2834
|
_pothos_plugin_drizzle.default,
|
|
2625
2835
|
_pothos_plugin_smart_subscriptions.default,
|
|
2626
2836
|
_pothos_plugin_tracing.default,
|
|
2837
|
+
_pothos_plugin_validation.default,
|
|
2627
2838
|
...pothosConfig?.plugins ?? []
|
|
2628
2839
|
],
|
|
2629
2840
|
drizzle: {
|
|
@@ -2669,14 +2880,20 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2669
2880
|
}
|
|
2670
2881
|
},
|
|
2671
2882
|
otel,
|
|
2672
|
-
logger
|
|
2883
|
+
logger,
|
|
2884
|
+
validation
|
|
2673
2885
|
});
|
|
2674
2886
|
schemaBuilder.addScalarType("JSON", graphql_scalars.JSONResolver);
|
|
2675
2887
|
schemaBuilder.addScalarType("Date", graphql_scalars.DateResolver);
|
|
2676
2888
|
schemaBuilder.addScalarType("DateTime", graphql_scalars.DateTimeISOResolver);
|
|
2677
2889
|
schemaBuilder.addScalarType("BigInt", graphql_scalars.BigIntResolver);
|
|
2678
2890
|
schemaBuilder.addScalarType("Bytes", graphql_scalars.ByteResolver);
|
|
2891
|
+
schemaBuilder.addScalarType("EmailAddress", graphql_scalars.EmailAddressResolver);
|
|
2892
|
+
schemaBuilder.addScalarType("PhoneNumber", PhoneNumberResolver);
|
|
2893
|
+
schemaBuilder.addScalarType("Locale", graphql_scalars.LocaleResolver);
|
|
2894
|
+
schemaBuilder.addScalarType("PersonName", PersonNameResolver);
|
|
2679
2895
|
implementDefaultWhereInputArgs(schemaBuilder);
|
|
2896
|
+
implementDefaultAddressTypes(schemaBuilder);
|
|
2680
2897
|
schemaBuilder.queryType({});
|
|
2681
2898
|
schemaBuilder.subscriptionType({});
|
|
2682
2899
|
schemaBuilder.mutationType({});
|
|
@@ -2870,12 +3087,14 @@ export const r = rumble({
|
|
|
2870
3087
|
execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput, "rest"),
|
|
2871
3088
|
subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput, "rest"),
|
|
2872
3089
|
errorHandler(errors) {
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3090
|
+
if (!errorsAlreadyReported(errors)) {
|
|
3091
|
+
recordSpanErrors(_opentelemetry_api.trace.getActiveSpan(), errors);
|
|
3092
|
+
telemetryLogger(rumbleInput)?.error({
|
|
3093
|
+
[ATTR_TRANSPORT]: "rest",
|
|
3094
|
+
...traceCorrelationFields(rumbleInput),
|
|
3095
|
+
...errorsLogField(errors)
|
|
3096
|
+
}, "rest request failed");
|
|
3097
|
+
}
|
|
2879
3098
|
return userErrorHandler?.(errors) ?? sofaErrorResponse(errors);
|
|
2880
3099
|
}
|
|
2881
3100
|
});
|