@m1212e/rumble 0.23.19 → 0.23.21

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 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.19";
219
+ var version = "0.23.21";
215
220
  //#endregion
216
221
  //#region lib/helpers/errorLogging.ts
217
222
  function hasMessage(error) {
@@ -2635,8 +2640,214 @@ function registerRuntimeFiltersPlugin() {
2635
2640
  }
2636
2641
  }
2637
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/emailAddress.ts
2767
+ /**
2768
+ * Validates an email address using graphql-scalars' own format check, then lowercases it.
2769
+ */
2770
+ const normalize$1 = (value) => {
2771
+ try {
2772
+ return graphql_scalars.EmailAddressResolver.parseValue(value).toLowerCase();
2773
+ } catch (error) {
2774
+ throw new RumbleErrorSafe(error instanceof Error ? error.message : `Invalid email address: ${value}`);
2775
+ }
2776
+ };
2777
+ const EmailAddressResolver = new graphql.GraphQLScalarType({
2778
+ name: "EmailAddress",
2779
+ description: `${graphql_scalars.EmailAddressResolver.description} Lowercased.`,
2780
+ specifiedByURL: graphql_scalars.EmailAddressResolver.specifiedByURL,
2781
+ serialize: normalize$1,
2782
+ parseValue: normalize$1,
2783
+ parseLiteral(ast) {
2784
+ if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as email addresses but got a: ${ast.kind}`);
2785
+ return normalize$1(ast.value);
2786
+ }
2787
+ });
2788
+ //#endregion
2789
+ //#region lib/scalars/personName.ts
2790
+ const MAX_LENGTH = 200;
2791
+ const NAME_REGEX = /^[\p{L}\p{M}]+(?:(?:[ '-]|\.\x20?)[\p{L}\p{M}]+)*\.?$/u;
2792
+ /**
2793
+ * Normalizes and validates a person's name.
2794
+ */
2795
+ const normalize = (value) => {
2796
+ if (typeof value !== "string") throw new RumbleErrorSafe(`Name value is not a string: ${value}`);
2797
+ const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\p{Zs}+/gu, " ").trim().replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
2798
+ if (normalized.length === 0) throw new RumbleErrorSafe("Name must not be empty.");
2799
+ if (normalized.length > MAX_LENGTH) throw new RumbleErrorSafe(`Name must not be longer than ${MAX_LENGTH} characters.`);
2800
+ 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.`);
2801
+ return normalized;
2802
+ };
2803
+ const PersonNameResolver = new graphql.GraphQLScalarType({
2804
+ name: "PersonName",
2805
+ 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.",
2806
+ serialize: normalize,
2807
+ parseValue: normalize,
2808
+ parseLiteral(ast) {
2809
+ if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as names but got a: ${ast.kind}`);
2810
+ return normalize(ast.value);
2811
+ }
2812
+ });
2813
+ //#endregion
2814
+ //#region lib/scalars/phoneNumber.ts
2815
+ /**
2816
+ * Parses and validates a phone number using libphonenumber-js (Google's phone number metadata),
2817
+ * which checks real per-country validity (length, area code plausibility, ...) rather than just
2818
+ * the E.164 shape.
2819
+ */
2820
+ const parse = (value) => {
2821
+ if (typeof value !== "string") throw new RumbleErrorSafe(`Phone number value is not a string: ${value}`);
2822
+ const phoneNumber = (0, libphonenumber_js.parsePhoneNumberFromString)(value);
2823
+ if (!phoneNumber?.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${value}. Please provide a valid phone number including its country calling code, e.g. +4915123456789.`);
2824
+ return phoneNumber;
2825
+ };
2826
+ /**
2827
+ * Resolvers may either return an already-parsed PhoneNumber (e.g. one obtained via `parse`
2828
+ * above) or a raw string, which is parsed and validated the same way input values are.
2829
+ */
2830
+ const serialize = (value) => {
2831
+ if (value && typeof value === "object" && typeof value.isValid === "function") {
2832
+ const phoneNumber = value;
2833
+ if (!phoneNumber.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${phoneNumber.number}`);
2834
+ return phoneNumber.number;
2835
+ }
2836
+ return parse(value).number;
2837
+ };
2838
+ const PhoneNumberResolver = new graphql.GraphQLScalarType({
2839
+ name: "PhoneNumber",
2840
+ 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.",
2841
+ serialize,
2842
+ parseValue: parse,
2843
+ parseLiteral(ast) {
2844
+ if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as phone numbers but got a: ${ast.kind}`);
2845
+ return parse(ast.value);
2846
+ }
2847
+ });
2848
+ //#endregion
2638
2849
  //#region lib/schemaBuilder.ts
2639
- const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2850
+ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
2640
2851
  const createSpan = otel?.enabled && otel.tracer ? (0, _pothos_tracing_opentelemetry.createOpenTelemetryWrapper)(otel.tracer, otel.options) : void 0;
2641
2852
  registerRuntimeFiltersPlugin();
2642
2853
  const schemaBuilder = new _pothos_core.default({
@@ -2646,6 +2857,7 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2646
2857
  _pothos_plugin_drizzle.default,
2647
2858
  _pothos_plugin_smart_subscriptions.default,
2648
2859
  _pothos_plugin_tracing.default,
2860
+ _pothos_plugin_validation.default,
2649
2861
  ...pothosConfig?.plugins ?? []
2650
2862
  ],
2651
2863
  drizzle: {
@@ -2691,14 +2903,20 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2691
2903
  }
2692
2904
  },
2693
2905
  otel,
2694
- logger
2906
+ logger,
2907
+ validation
2695
2908
  });
2696
2909
  schemaBuilder.addScalarType("JSON", graphql_scalars.JSONResolver);
2697
2910
  schemaBuilder.addScalarType("Date", graphql_scalars.DateResolver);
2698
2911
  schemaBuilder.addScalarType("DateTime", graphql_scalars.DateTimeISOResolver);
2699
2912
  schemaBuilder.addScalarType("BigInt", graphql_scalars.BigIntResolver);
2700
2913
  schemaBuilder.addScalarType("Bytes", graphql_scalars.ByteResolver);
2914
+ schemaBuilder.addScalarType("EmailAddress", EmailAddressResolver);
2915
+ schemaBuilder.addScalarType("PhoneNumber", PhoneNumberResolver);
2916
+ schemaBuilder.addScalarType("Locale", graphql_scalars.LocaleResolver);
2917
+ schemaBuilder.addScalarType("PersonName", PersonNameResolver);
2701
2918
  implementDefaultWhereInputArgs(schemaBuilder);
2919
+ implementDefaultAddressTypes(schemaBuilder);
2702
2920
  schemaBuilder.queryType({});
2703
2921
  schemaBuilder.subscriptionType({});
2704
2922
  schemaBuilder.mutationType({});