diff --git a/package.json b/package.json index b556a9d72..9bee7edb9 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@babel/preset-typescript": "7.14.5", "@graphql-tools/merge": "6.2.14", "@graphql-tools/schema": "7.1.5", + "@types/jest": "26.0.24", "@types/mongodb": "3.6.20", "@types/node": "14.17.5", "@typescript-eslint/eslint-plugin": "4.28.4", @@ -77,6 +78,7 @@ "typescript": "4.3.5" }, "dependencies": { + "bson": "4.4.1", "tslib": "~2.3.0" }, "peerDependencies": { diff --git a/src/scalars/ObjectID.ts b/src/scalars/ObjectID.ts index 74b28cdfe..96d44f19b 100644 --- a/src/scalars/ObjectID.ts +++ b/src/scalars/ObjectID.ts @@ -1,6 +1,5 @@ import { Kind, GraphQLError, GraphQLScalarType, ValueNode } from 'graphql'; - -const MONGODB_OBJECTID_REGEX = /*#__PURE__*/ /^[A-Fa-f0-9]{24}$/; +import { ObjectId } from 'bson/lib/objectid'; export const GraphQLObjectID = /*#__PURE__*/ new GraphQLScalarType({ name: 'ObjectID', @@ -8,39 +7,39 @@ export const GraphQLObjectID = /*#__PURE__*/ new GraphQLScalarType({ description: 'A field whose value conforms with the standard mongodb object ID as described here: https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectId. Example: 5e5677d71bdc2ae76344968c', - serialize(value: string) { - if (!MONGODB_OBJECTID_REGEX.test(value)) { + serialize(value: ObjectId): string { + if (!ObjectId.isValid(value)) { throw new TypeError( `Value is not a valid mongodb object id of form: ${value}`, ); } - return value; + return value.toHexString(); }, - parseValue(value: string) { - if (!MONGODB_OBJECTID_REGEX.test(value)) { + parseValue(value: string): ObjectId { + if (!ObjectId.isValid(value)) { throw new TypeError( `Value is not a valid mongodb object id of form: ${value}`, ); } - return value; + return new ObjectId(value); }, - parseLiteral(ast: ValueNode) { + parseLiteral(ast: ValueNode): ObjectId { if (ast.kind !== Kind.STRING) { throw new GraphQLError( `Can only validate strings as mongodb object id but got a: ${ast.kind}`, ); } - if (!MONGODB_OBJECTID_REGEX.test(ast.value)) { + if (!ObjectId.isValid(ast.value)) { throw new TypeError( `Value is not a valid mongodb object id of form: ${ast.value}`, ); } - return ast.value; + return new ObjectId(ast.value); }, }); diff --git a/tests/ObjectID.test.ts b/tests/ObjectID.test.ts index e568419e4..eed5fe470 100644 --- a/tests/ObjectID.test.ts +++ b/tests/ObjectID.test.ts @@ -3,19 +3,22 @@ import { Kind } from 'graphql/language'; import { GraphQLObjectID } from '../src/scalars/ObjectID'; +import { ObjectId } from 'bson'; describe('ObjectId', () => { describe('valid', () => { test('serialize', () => { - expect(GraphQLObjectID.serialize('5e5677d71bdc2ae76344968c')).toBe( - '5e5677d71bdc2ae76344968c', - ); + const id = new ObjectId('5e5677d71bdc2ae76344968c'); + + expect(GraphQLObjectID.serialize(id)).toBe(id.toHexString()); }); test('parseValue', () => { - expect(GraphQLObjectID.parseValue('5e5677d71bdc2ae76344968c')).toBe( - '5e5677d71bdc2ae76344968c', - ); + const id = '5e5677d71bdc2ae76344968c'; + const parsed = GraphQLObjectID.parseValue(id); + + expect(parsed).toBeInstanceOf(ObjectId); + expect(parsed.toHexString()).toBe(id); }); test('parseLiteral', () => { @@ -24,7 +27,7 @@ describe('ObjectId', () => { { value: '5e5677d71bdc2ae76344968c', kind: Kind.STRING }, undefined, ), // undefined as prescribed by the Maybe type - ).toBe('5e5677d71bdc2ae76344968c'); + ).toStrictEqual(new ObjectId('5e5677d71bdc2ae76344968c')); }); }); diff --git a/types/bson.d.ts b/types/bson.d.ts new file mode 100644 index 000000000..cd3751681 --- /dev/null +++ b/types/bson.d.ts @@ -0,0 +1,85 @@ +/** + * Unfortunately, the file `bson/lib/objectid` does not have its own + * type definitions. This causes TypeScript to throw an error when we + * try and use it instead of the whole bson module. This is mostly + * copy/pasted from the `bson.d.ts` in the installed package. + */ +declare module 'bson/lib/objectid' { + /** @public */ + export interface ObjectIdLike { + id: string | Buffer; + __id?: string; + toHexString(): string; + } + /** + * A class representation of the BSON ObjectId type. + * @public + */ + export class ObjectId { + _bsontype: 'ObjectId'; + /* Excluded from this release type: index */ + static cacheHexString: boolean; + /* Excluded from this release type: [kId] */ + /* Excluded from this release type: __id */ + /** + * Create an ObjectId type + * + * @param id - Can be a 24 character hex string, 12 byte binary Buffer, or a number. + */ + constructor(id?: string | Buffer | number | ObjectIdLike | ObjectId); + /* + * The ObjectId bytes + * @readonly + */ + id: Buffer; + /* + * The generation time of this ObjectId instance + * @deprecated Please use getTimestamp / createFromTime which returns an int32 epoch + */ + generationTime: number; + /** Returns the ObjectId id as a 24 character hex string representation */ + toHexString(): string; + /* Excluded from this release type: getInc */ + /** + * Generate a 12 byte id buffer used in ObjectId's + * + * @param time - pass in a second based timestamp. + */ + static generate(time?: number): Buffer; + /* Excluded from this release type: toString */ + /* Excluded from this release type: toJSON */ + /** + * Compares the equality of this ObjectId with `otherID`. + * + * @param otherId - ObjectId instance to compare against. + */ + equals(otherId: string | ObjectId | ObjectIdLike): boolean; + /** Returns the generation date (accurate up to the second) that this ID was generated. */ + getTimestamp(): Date; + /* Excluded from this release type: createPk */ + /** + * Creates an ObjectId from a second based number, with the rest of the ObjectId zeroed out. Used for comparisons or sorting the ObjectId. + * + * @param time - an integer number representing a number of seconds. + */ + static createFromTime(time: number): ObjectId; + /** + * Creates an ObjectId from a hex string representation of an ObjectId. + * + * @param hexString - create a ObjectId from a passed in 24 character hexstring. + */ + static createFromHexString(hexString: string): ObjectId; + /** + * Checks if a value is a valid bson ObjectId + * + * @param id - ObjectId instance to validate. + */ + static isValid( + id: number | string | ObjectId | Uint8Array | ObjectIdLike, + ): boolean; + + /* Excluded from this release type: toExtendedJSON */ + /* Excluded from this release type: fromExtendedJSON */ + inspect(): string; + } +} diff --git a/website/docs/scalars/object-id.md b/website/docs/scalars/object-id.md index 32d610732..7dff9e349 100644 --- a/website/docs/scalars/object-id.md +++ b/website/docs/scalars/object-id.md @@ -4,5 +4,4 @@ title: ObjectID sidebar_label: ObjectID --- -A field whose value conforms to the mongodb object id format as explained in the [documentation](https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectId) - +A field whose value conforms to the mongodb object id format as explained in the [documentation](https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectId). This resolves to an [ObjectId](https://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html) instance in your resolvers. diff --git a/yarn.lock b/yarn.lock index 0a6bd4027..fb7b949ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1585,6 +1585,17 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@jest/types@^27.0.1": version "27.0.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.1.tgz#631738c942e70045ebbf42a3f9b433036d3845e4" @@ -1738,6 +1749,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@26.0.24": + version "26.0.24" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" + integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/json-schema@^7.0.7": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" @@ -1798,6 +1817,13 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" integrity sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== +"@types/yargs@^15.0.0": + version "15.0.14" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" + integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^16.0.0": version "16.0.3" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.3.tgz#4b6d35bb8e680510a7dc2308518a80ee1ef27e01" @@ -2209,7 +2235,7 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@^4.4.0: +bson@4.4.1, bson@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/bson/-/bson-4.4.1.tgz#682c3cb8b90b222414ce14ef8398154ba2cc21bc" integrity sha512-Uu4OCZa0jouQJCKOk1EmmyqtdWAP5HVLru4lQxTwzJzxT+sJ13lVpEZU/MATDxtHiekWMAL84oQY3Xn1LpJVSg== @@ -2583,6 +2609,11 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diff-sequences@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" @@ -3611,6 +3642,16 @@ jest-config@^27.0.6: micromatch "^4.0.4" pretty-format "^27.0.6" +jest-diff@^26.0.0: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-diff@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.0.6.tgz#4a7a19ee6f04ad70e0e3388f35829394a44c7b5e" @@ -3664,6 +3705,11 @@ jest-environment-node@^27.0.6: jest-mock "^27.0.6" jest-util "^27.0.6" +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== + jest-get-type@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" @@ -4681,6 +4727,16 @@ prettier@2.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +pretty-format@^26.0.0, pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-format@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.0.6.tgz#ab770c47b2c6f893a21aefc57b75da63ef49a11f"