diff --git a/package-lock.json b/package-lock.json index 777c4b1..6e693e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@convex-dev/twilio", "version": "0.2.0", "license": "Apache-2.0", + "dependencies": { + "convex-helpers": "^0.1.106" + }, "devDependencies": { "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "^3.3.1", @@ -70,7 +73,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -316,7 +318,6 @@ "integrity": "sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@edge-runtime/primitives": "6.0.0" }, @@ -331,7 +332,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -348,7 +348,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -365,7 +364,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -382,7 +380,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -399,7 +396,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -416,7 +412,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -433,7 +428,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -450,7 +444,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -467,7 +460,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -484,7 +476,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -501,7 +492,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -518,7 +508,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -535,7 +524,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -552,7 +540,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -569,7 +556,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -586,7 +572,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -603,7 +588,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -620,7 +604,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -637,7 +620,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -654,7 +636,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -671,7 +652,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -688,7 +668,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -705,7 +684,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -722,7 +700,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -739,7 +716,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1405,7 +1381,6 @@ "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1456,7 +1431,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1803,7 +1777,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1969,7 +1942,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2186,9 +2158,7 @@ "version": "1.29.0", "resolved": "https://registry.npmjs.org/convex/-/convex-1.29.0.tgz", "integrity": "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA==", - "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" @@ -2217,6 +2187,40 @@ } } }, + "node_modules/convex-helpers": { + "version": "0.1.106", + "resolved": "https://registry.npmjs.org/convex-helpers/-/convex-helpers-0.1.106.tgz", + "integrity": "sha512-hWRe3yDaAVHMe4CUYw1YoQLiPZ1KIx6Kbf0w6UcRDx1BXpJgMCl3GVIMiSeYiA0PkbwjnIwGWIvoUVKloG5Tyw==", + "license": "Apache-2.0", + "bin": { + "convex-helpers": "bin.cjs" + }, + "peerDependencies": { + "@standard-schema/spec": "^1.0.0", + "convex": "^1.25.4", + "hono": "^4.0.5", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "typescript": "^5.5", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@standard-schema/spec": { + "optional": true + }, + "hono": { + "optional": true + }, + "react": { + "optional": true + }, + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/convex-test": { "version": "0.0.38", "resolved": "https://registry.npmjs.org/convex-test/-/convex-test-0.0.38.tgz", @@ -2312,7 +2316,6 @@ "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2378,7 +2381,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3459,7 +3461,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -3842,7 +3843,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3923,9 +3923,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4012,7 +4011,6 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4129,7 +4127,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4445,9 +4442,8 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index c109076..0db3475 100644 --- a/package.json +++ b/package.json @@ -82,5 +82,8 @@ "vitest": "^3.2.4" }, "types": "./dist/client/index.d.ts", - "module": "./dist/client/index.js" + "module": "./dist/client/index.js", + "dependencies": { + "convex-helpers": "^0.1.106" + } } diff --git a/src/component/phone_numbers.ts b/src/component/phone_numbers.ts index 75b3e22..5972e55 100644 --- a/src/component/phone_numbers.ts +++ b/src/component/phone_numbers.ts @@ -6,7 +6,7 @@ import { internalQuery, } from "./_generated/server.js"; import { internal } from "./_generated/api.js"; -import { twilioRequest } from "./utils.js"; +import { twilioRequest, attemptToParse } from "./utils.js"; import schema from "./schema.js"; export const create = action({ @@ -40,7 +40,18 @@ export const insert = internalMutation({ }, returns: v.id("phone_numbers"), handler: async (ctx, args) => { - return await ctx.db.insert("phone_numbers", args.phone_number); + // It might have extra fields so we need to sanitize it before inserting into the table + const result = attemptToParse( + schema.tables.phone_numbers.validator, + args.phone_number, + ); + + if (result.kind === "error") { + console.error("Failed to parse phone number data:", result.error); + throw new Error("Invalid phone number data"); + } + + return await ctx.db.insert("phone_numbers", result.data); }, }); diff --git a/src/component/schema.ts b/src/component/schema.ts index 7a53fc0..b77a5e3 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -39,10 +39,10 @@ export default defineSchema({ phone_numbers: defineTable({ account_sid: v.string(), address_requirements: v.string(), - address_sid: v.null(), + address_sid: v.union(v.string(), v.null()), api_version: v.string(), beta: v.boolean(), - bundle_sid: v.null(), + bundle_sid: v.union(v.string(), v.null()), capabilities: v.object({ fax: v.boolean(), mms: v.boolean(), @@ -51,11 +51,11 @@ export default defineSchema({ }), date_created: v.string(), date_updated: v.string(), - emergency_address_sid: v.null(), + emergency_address_sid: v.union(v.string(), v.null()), emergency_address_status: v.string(), emergency_status: v.string(), friendly_name: v.string(), - identity_sid: v.null(), + identity_sid: v.union(v.string(), v.null()), origin: v.string(), phone_number: v.string(), sid: v.string(), @@ -70,7 +70,7 @@ export default defineSchema({ subresource_uris: v.object({ assigned_add_ons: v.string(), }), - trunk_sid: v.null(), + trunk_sid: v.union(v.string(), v.null()), uri: v.string(), voice_application_sid: v.string(), voice_caller_id_lookup: v.boolean(), @@ -78,6 +78,7 @@ export default defineSchema({ voice_fallback_url: v.string(), voice_method: v.string(), voice_url: v.string(), + voice_receive_mode: v.optional(v.string()), }) .index("by_phone_number", ["account_sid", "phone_number"]) .index("by_sid", ["account_sid", "sid"]), diff --git a/src/component/utils.ts b/src/component/utils.ts index e5abbc4..0e02f34 100644 --- a/src/component/utils.ts +++ b/src/component/utils.ts @@ -1,3 +1,6 @@ +import { parse } from "convex-helpers/validators"; +import type { Validator, Infer } from "convex/values"; + export const twilioRequest = async function ( path: string, account_sid: string, @@ -32,3 +35,23 @@ export const twilioRequest = async function ( } return await response.json(); }; + +/** + * Generic function to attempt parsing with proper TypeScript type narrowing + */ +export function attemptToParse>( + validator: T, + value: unknown, +): { kind: "success"; data: Infer } | { kind: "error"; error: unknown } { + try { + return { + kind: "success", + data: parse(validator, value), + }; + } catch (error) { + return { + kind: "error", + error, + }; + } +}