@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 +221 -3
- 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 +222 -5
- package/out/index.mjs.map +1 -1
- package/package.json +6 -2
package/out/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as lazy } from "./lazy-C7IyM4M4.mjs";
|
|
2
2
|
import { t as generateFromSchema } from "./generate-0QFVc3Sg.mjs";
|
|
3
|
-
import { GraphQLError, GraphQLSchema, execute as defaultExecute, getOperationAST, print, subscribe as defaultSubscribe } from "graphql";
|
|
3
|
+
import { GraphQLError, GraphQLScalarType, GraphQLSchema, Kind, execute as defaultExecute, getOperationAST, print, subscribe as defaultSubscribe } from "graphql";
|
|
4
4
|
import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
|
|
5
5
|
import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
|
|
6
6
|
import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
|
|
@@ -18,8 +18,12 @@ import SchemaBuilder, { BasePlugin } from "@pothos/core";
|
|
|
18
18
|
import DrizzlePlugin from "@pothos/plugin-drizzle";
|
|
19
19
|
import SmartSubscriptionsPlugin, { subscribeOptionsFromIterator } from "@pothos/plugin-smart-subscriptions";
|
|
20
20
|
import TracingPlugin, { isRootField, wrapResolver } from "@pothos/plugin-tracing";
|
|
21
|
-
import
|
|
21
|
+
import ValidationPlugin from "@pothos/plugin-validation";
|
|
22
|
+
import { BigIntResolver, ByteResolver, DateResolver, DateTimeISOResolver, EmailAddressResolver, JSONResolver, LocaleResolver } from "graphql-scalars";
|
|
22
23
|
import DataLoader from "dataloader";
|
|
24
|
+
import { AddressValidationError, CountryMissingError, InvalidStateError, InvalidZipError, MissingFieldError, isValidCountryCode, validateAddress } from "lib-address";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { parsePhoneNumberFromString } from "libphonenumber-js";
|
|
23
27
|
//#region lib/types/rumbleError.ts
|
|
24
28
|
/**
|
|
25
29
|
* An error that gets raised by rumble whenever something does not go according to plan.
|
|
@@ -182,7 +186,7 @@ function mapNullFieldsToUndefined(obj) {
|
|
|
182
186
|
}
|
|
183
187
|
//#endregion
|
|
184
188
|
//#region package.json
|
|
185
|
-
var version = "0.23.
|
|
189
|
+
var version = "0.23.21";
|
|
186
190
|
//#endregion
|
|
187
191
|
//#region lib/helpers/errorLogging.ts
|
|
188
192
|
function hasMessage(error) {
|
|
@@ -2606,8 +2610,214 @@ function registerRuntimeFiltersPlugin() {
|
|
|
2606
2610
|
}
|
|
2607
2611
|
}
|
|
2608
2612
|
//#endregion
|
|
2613
|
+
//#region lib/scalars/addressValidation.ts
|
|
2614
|
+
const FIELD_PATH_MAP = {
|
|
2615
|
+
addressLine1: "streetAddress",
|
|
2616
|
+
addressLine2: "streetAddress",
|
|
2617
|
+
addressLine3: "streetAddress",
|
|
2618
|
+
city: "locality",
|
|
2619
|
+
dependentLocality: "locality",
|
|
2620
|
+
state: "region",
|
|
2621
|
+
zip: "postalCode",
|
|
2622
|
+
sortingCode: "postalCode"
|
|
2623
|
+
};
|
|
2624
|
+
/**
|
|
2625
|
+
* Fixes the unambiguous, purely-cosmetic issues (stray surrounding whitespace, wrong case)
|
|
2626
|
+
* before validation runs, rather than rejecting a request over them. This runs as a Zod
|
|
2627
|
+
* `transform`, and `@pothos/plugin-validation` uses a transform's *output* as the field's
|
|
2628
|
+
* actual value — so the corrected value, not the original one, is what resolvers receive.
|
|
2629
|
+
*
|
|
2630
|
+
* `countryCode` and `postalCode` are uppercased since their canonical forms always are (ISO
|
|
2631
|
+
* 3166-1 codes, and postal codes in every country that mixes letters and digits, e.g. GB/CA/NL).
|
|
2632
|
+
* `region`/`locality` are only trimmed, not case-changed: unlike country/postal codes, a region
|
|
2633
|
+
* is sometimes a free-text place name rather than a code (lib-address's own subdivision lookup
|
|
2634
|
+
* is already case-insensitive, so case coercion there wouldn't fix anything a rejection-worthy
|
|
2635
|
+
* case wouldn't already pass).
|
|
2636
|
+
*/
|
|
2637
|
+
const normalizeAddress = (address) => ({
|
|
2638
|
+
streetAddress: address.streetAddress.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n") || null,
|
|
2639
|
+
locality: address.locality?.trim() || null,
|
|
2640
|
+
region: address.region?.trim() || null,
|
|
2641
|
+
postalCode: address.postalCode?.trim().toUpperCase() || null,
|
|
2642
|
+
countryCode: address.countryCode.trim().toUpperCase()
|
|
2643
|
+
});
|
|
2644
|
+
const addressValidationSchema = z.object({
|
|
2645
|
+
streetAddress: z.string(),
|
|
2646
|
+
locality: z.string().nullable().optional(),
|
|
2647
|
+
region: z.string().nullable().optional(),
|
|
2648
|
+
postalCode: z.string().nullable().optional(),
|
|
2649
|
+
countryCode: z.string()
|
|
2650
|
+
}).transform(normalizeAddress).superRefine((address, ctx) => {
|
|
2651
|
+
if (!address.countryCode) {
|
|
2652
|
+
ctx.addIssue({
|
|
2653
|
+
code: "custom",
|
|
2654
|
+
path: ["countryCode"],
|
|
2655
|
+
message: "countryCode is required."
|
|
2656
|
+
});
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
if (!isValidCountryCode(address.countryCode)) {
|
|
2660
|
+
ctx.addIssue({
|
|
2661
|
+
code: "custom",
|
|
2662
|
+
path: ["countryCode"],
|
|
2663
|
+
message: `Unknown country code: ${address.countryCode}`
|
|
2664
|
+
});
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
const [addressLine1, addressLine2, addressLine3] = address.streetAddress ? address.streetAddress.split("\n") : [];
|
|
2668
|
+
try {
|
|
2669
|
+
validateAddress({
|
|
2670
|
+
country: address.countryCode,
|
|
2671
|
+
addressLine1,
|
|
2672
|
+
addressLine2,
|
|
2673
|
+
addressLine3,
|
|
2674
|
+
city: address.locality ?? void 0,
|
|
2675
|
+
state: address.region?.toUpperCase() ?? void 0,
|
|
2676
|
+
zip: address.postalCode ?? void 0
|
|
2677
|
+
});
|
|
2678
|
+
} catch (error) {
|
|
2679
|
+
if (error instanceof CountryMissingError) {
|
|
2680
|
+
ctx.addIssue({
|
|
2681
|
+
code: "custom",
|
|
2682
|
+
path: ["countryCode"],
|
|
2683
|
+
message: error.message
|
|
2684
|
+
});
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (!(error instanceof AddressValidationError)) throw error;
|
|
2688
|
+
for (const subError of error.errors) if (subError instanceof MissingFieldError) {
|
|
2689
|
+
const path = FIELD_PATH_MAP[subError.field];
|
|
2690
|
+
if (!path) continue;
|
|
2691
|
+
ctx.addIssue({
|
|
2692
|
+
code: "custom",
|
|
2693
|
+
path: [path],
|
|
2694
|
+
message: `${path} is required for country ${address.countryCode}.`
|
|
2695
|
+
});
|
|
2696
|
+
} else if (subError instanceof InvalidZipError) ctx.addIssue({
|
|
2697
|
+
code: "custom",
|
|
2698
|
+
path: ["postalCode"],
|
|
2699
|
+
message: `Invalid postal code for country ${address.countryCode}.`
|
|
2700
|
+
});
|
|
2701
|
+
else if (subError instanceof InvalidStateError) ctx.addIssue({
|
|
2702
|
+
code: "custom",
|
|
2703
|
+
path: ["region"],
|
|
2704
|
+
message: `Invalid region for country ${address.countryCode}.`
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
});
|
|
2708
|
+
//#endregion
|
|
2709
|
+
//#region lib/scalars/address.ts
|
|
2710
|
+
function implementDefaultAddressTypes(schemaBuilder) {
|
|
2711
|
+
return {
|
|
2712
|
+
Address: schemaBuilder.objectRef("Address").implement({
|
|
2713
|
+
description: "A postal address, modeled after the OIDC `address` claim and schema.org's PostalAddress.",
|
|
2714
|
+
fields: (t) => ({
|
|
2715
|
+
streetAddress: t.exposeString("streetAddress", { nullable: true }),
|
|
2716
|
+
locality: t.exposeString("locality", { nullable: true }),
|
|
2717
|
+
region: t.exposeString("region", { nullable: true }),
|
|
2718
|
+
postalCode: t.exposeString("postalCode", { nullable: true }),
|
|
2719
|
+
countryCode: t.exposeString("countryCode", { nullable: true })
|
|
2720
|
+
})
|
|
2721
|
+
}),
|
|
2722
|
+
AddressInput: schemaBuilder.inputRef("AddressInput").implement({
|
|
2723
|
+
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).",
|
|
2724
|
+
validate: addressValidationSchema,
|
|
2725
|
+
fields: (t) => ({
|
|
2726
|
+
streetAddress: t.string({ required: true }),
|
|
2727
|
+
locality: t.string({ required: false }),
|
|
2728
|
+
region: t.string({ required: false }),
|
|
2729
|
+
postalCode: t.string({ required: false }),
|
|
2730
|
+
countryCode: t.string({ required: true })
|
|
2731
|
+
})
|
|
2732
|
+
})
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
//#endregion
|
|
2736
|
+
//#region lib/scalars/emailAddress.ts
|
|
2737
|
+
/**
|
|
2738
|
+
* Validates an email address using graphql-scalars' own format check, then lowercases it.
|
|
2739
|
+
*/
|
|
2740
|
+
const normalize$1 = (value) => {
|
|
2741
|
+
try {
|
|
2742
|
+
return EmailAddressResolver.parseValue(value).toLowerCase();
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
throw new RumbleErrorSafe(error instanceof Error ? error.message : `Invalid email address: ${value}`);
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
const EmailAddressResolver$1 = new GraphQLScalarType({
|
|
2748
|
+
name: "EmailAddress",
|
|
2749
|
+
description: `${EmailAddressResolver.description} Lowercased.`,
|
|
2750
|
+
specifiedByURL: EmailAddressResolver.specifiedByURL,
|
|
2751
|
+
serialize: normalize$1,
|
|
2752
|
+
parseValue: normalize$1,
|
|
2753
|
+
parseLiteral(ast) {
|
|
2754
|
+
if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as email addresses but got a: ${ast.kind}`);
|
|
2755
|
+
return normalize$1(ast.value);
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
//#endregion
|
|
2759
|
+
//#region lib/scalars/personName.ts
|
|
2760
|
+
const MAX_LENGTH = 200;
|
|
2761
|
+
const NAME_REGEX = /^[\p{L}\p{M}]+(?:(?:[ '-]|\.\x20?)[\p{L}\p{M}]+)*\.?$/u;
|
|
2762
|
+
/**
|
|
2763
|
+
* Normalizes and validates a person's name.
|
|
2764
|
+
*/
|
|
2765
|
+
const normalize = (value) => {
|
|
2766
|
+
if (typeof value !== "string") throw new RumbleErrorSafe(`Name value is not a string: ${value}`);
|
|
2767
|
+
const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\p{Zs}+/gu, " ").trim().replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
|
|
2768
|
+
if (normalized.length === 0) throw new RumbleErrorSafe("Name must not be empty.");
|
|
2769
|
+
if (normalized.length > MAX_LENGTH) throw new RumbleErrorSafe(`Name must not be longer than ${MAX_LENGTH} characters.`);
|
|
2770
|
+
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.`);
|
|
2771
|
+
return normalized;
|
|
2772
|
+
};
|
|
2773
|
+
const PersonNameResolver = new GraphQLScalarType({
|
|
2774
|
+
name: "PersonName",
|
|
2775
|
+
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.",
|
|
2776
|
+
serialize: normalize,
|
|
2777
|
+
parseValue: normalize,
|
|
2778
|
+
parseLiteral(ast) {
|
|
2779
|
+
if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as names but got a: ${ast.kind}`);
|
|
2780
|
+
return normalize(ast.value);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
//#endregion
|
|
2784
|
+
//#region lib/scalars/phoneNumber.ts
|
|
2785
|
+
/**
|
|
2786
|
+
* Parses and validates a phone number using libphonenumber-js (Google's phone number metadata),
|
|
2787
|
+
* which checks real per-country validity (length, area code plausibility, ...) rather than just
|
|
2788
|
+
* the E.164 shape.
|
|
2789
|
+
*/
|
|
2790
|
+
const parse = (value) => {
|
|
2791
|
+
if (typeof value !== "string") throw new RumbleErrorSafe(`Phone number value is not a string: ${value}`);
|
|
2792
|
+
const phoneNumber = parsePhoneNumberFromString(value);
|
|
2793
|
+
if (!phoneNumber?.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${value}. Please provide a valid phone number including its country calling code, e.g. +4915123456789.`);
|
|
2794
|
+
return phoneNumber;
|
|
2795
|
+
};
|
|
2796
|
+
/**
|
|
2797
|
+
* Resolvers may either return an already-parsed PhoneNumber (e.g. one obtained via `parse`
|
|
2798
|
+
* above) or a raw string, which is parsed and validated the same way input values are.
|
|
2799
|
+
*/
|
|
2800
|
+
const serialize = (value) => {
|
|
2801
|
+
if (value && typeof value === "object" && typeof value.isValid === "function") {
|
|
2802
|
+
const phoneNumber = value;
|
|
2803
|
+
if (!phoneNumber.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${phoneNumber.number}`);
|
|
2804
|
+
return phoneNumber.number;
|
|
2805
|
+
}
|
|
2806
|
+
return parse(value).number;
|
|
2807
|
+
};
|
|
2808
|
+
const PhoneNumberResolver = new GraphQLScalarType({
|
|
2809
|
+
name: "PhoneNumber",
|
|
2810
|
+
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.",
|
|
2811
|
+
serialize,
|
|
2812
|
+
parseValue: parse,
|
|
2813
|
+
parseLiteral(ast) {
|
|
2814
|
+
if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as phone numbers but got a: ${ast.kind}`);
|
|
2815
|
+
return parse(ast.value);
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
//#endregion
|
|
2609
2819
|
//#region lib/schemaBuilder.ts
|
|
2610
|
-
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
2820
|
+
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
|
|
2611
2821
|
const createSpan = otel?.enabled && otel.tracer ? createOpenTelemetryWrapper(otel.tracer, otel.options) : void 0;
|
|
2612
2822
|
registerRuntimeFiltersPlugin();
|
|
2613
2823
|
const schemaBuilder = new SchemaBuilder({
|
|
@@ -2617,6 +2827,7 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2617
2827
|
DrizzlePlugin,
|
|
2618
2828
|
SmartSubscriptionsPlugin,
|
|
2619
2829
|
TracingPlugin,
|
|
2830
|
+
ValidationPlugin,
|
|
2620
2831
|
...pothosConfig?.plugins ?? []
|
|
2621
2832
|
],
|
|
2622
2833
|
drizzle: {
|
|
@@ -2662,14 +2873,20 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2662
2873
|
}
|
|
2663
2874
|
},
|
|
2664
2875
|
otel,
|
|
2665
|
-
logger
|
|
2876
|
+
logger,
|
|
2877
|
+
validation
|
|
2666
2878
|
});
|
|
2667
2879
|
schemaBuilder.addScalarType("JSON", JSONResolver);
|
|
2668
2880
|
schemaBuilder.addScalarType("Date", DateResolver);
|
|
2669
2881
|
schemaBuilder.addScalarType("DateTime", DateTimeISOResolver);
|
|
2670
2882
|
schemaBuilder.addScalarType("BigInt", BigIntResolver);
|
|
2671
2883
|
schemaBuilder.addScalarType("Bytes", ByteResolver);
|
|
2884
|
+
schemaBuilder.addScalarType("EmailAddress", EmailAddressResolver$1);
|
|
2885
|
+
schemaBuilder.addScalarType("PhoneNumber", PhoneNumberResolver);
|
|
2886
|
+
schemaBuilder.addScalarType("Locale", LocaleResolver);
|
|
2887
|
+
schemaBuilder.addScalarType("PersonName", PersonNameResolver);
|
|
2672
2888
|
implementDefaultWhereInputArgs(schemaBuilder);
|
|
2889
|
+
implementDefaultAddressTypes(schemaBuilder);
|
|
2673
2890
|
schemaBuilder.queryType({});
|
|
2674
2891
|
schemaBuilder.subscriptionType({});
|
|
2675
2892
|
schemaBuilder.mutationType({});
|