diff --git a/spec/ParseDecimal128.spec.js b/spec/ParseDecimal128.spec.js new file mode 100644 index 0000000000..833615632b --- /dev/null +++ b/spec/ParseDecimal128.spec.js @@ -0,0 +1,373 @@ +'use strict'; + +const request = require('../lib/request'); + +describe('Parse Decimal128', () => { + it('should save and retrieve a Decimal128 value via REST API', async () => { + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestDecimal', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { + __type: 'Decimal128', + value: '12345678901234567890.123456789', + }, + }, + }); + expect(createResponse.data.objectId).toBeDefined(); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/TestDecimal/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.amount).toEqual({ + __type: 'Decimal128', + value: '12345678901234567890.123456789', + }); + }); + + it('should update a Decimal128 value', async () => { + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestDecimal', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { + __type: 'Decimal128', + value: '100.50', + }, + }, + }); + + await request({ + method: 'PUT', + url: `http://localhost:8378/1/classes/TestDecimal/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { + __type: 'Decimal128', + value: '200.75', + }, + }, + }); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/TestDecimal/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.amount).toEqual({ + __type: 'Decimal128', + value: '200.75', + }); + }); + + it('should query with $gt comparison on Decimal128', async () => { + const values = ['10.5', '20.5', '30.5']; + for (const val of values) { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalQuery', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: val }, + }, + }); + } + + const queryResponse = await request({ + url: 'http://localhost:8378/1/classes/DecimalQuery', + qs: { + where: JSON.stringify({ + amount: { $gt: { __type: 'Decimal128', value: '15.0' } }, + }), + }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(queryResponse.data.results.length).toBe(2); + const resultValues = queryResponse.data.results.map(r => r.amount.value).sort(); + expect(resultValues).toEqual(['20.5', '30.5']); + }); + + it('should query with $lt comparison on Decimal128', async () => { + const values = ['100', '200', '300']; + for (const val of values) { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalLt', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: val }, + }, + }); + } + + const queryResponse = await request({ + url: 'http://localhost:8378/1/classes/DecimalLt', + qs: { + where: JSON.stringify({ + amount: { $lt: { __type: 'Decimal128', value: '250' } }, + }), + }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(queryResponse.data.results.length).toBe(2); + const resultValues = queryResponse.data.results.map(r => r.amount.value).sort(); + expect(resultValues).toEqual(['100', '200']); + }); + + it('should handle high-precision Decimal128 values', async () => { + const highPrecisionValue = '12345678901234567890.12345678901234'; + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalPrecision', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: highPrecisionValue }, + }, + }); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/DecimalPrecision/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.amount).toEqual({ + __type: 'Decimal128', + value: highPrecisionValue, + }); + }); + + it('should handle negative Decimal128 values', async () => { + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalNeg', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: '-12345.6789' }, + }, + }); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/DecimalNeg/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.amount).toEqual({ + __type: 'Decimal128', + value: '-12345.6789', + }); + }); + + it('should handle zero Decimal128 value', async () => { + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalZero', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: '0' }, + }, + }); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/DecimalZero/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.amount).toEqual({ + __type: 'Decimal128', + value: '0', + }); + }); + + it('should set Decimal128 field via schema API', async () => { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/schemas/DecimalSchema', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + className: 'DecimalSchema', + fields: { + amount: { type: 'Decimal128' }, + }, + }, + }); + + const schemaResponse = await request({ + url: 'http://localhost:8378/1/schemas/DecimalSchema', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(schemaResponse.data.fields.amount.type).toBe('Decimal128'); + }); + + it('should delete Decimal128 field value', async () => { + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalDel', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: '42.0' }, + }, + }); + + await request({ + method: 'PUT', + url: `http://localhost:8378/1/classes/DecimalDel/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __op: 'Delete' }, + }, + }); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/DecimalDel/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.amount).toBeUndefined(); + }); + + it('should query with equality on Decimal128', async () => { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalEq', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: '99.99' }, + label: 'target', + }, + }); + await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalEq', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + amount: { __type: 'Decimal128', value: '50.00' }, + label: 'other', + }, + }); + + const queryResponse = await request({ + url: 'http://localhost:8378/1/classes/DecimalEq', + qs: { + where: JSON.stringify({ + amount: { __type: 'Decimal128', value: '99.99' }, + }), + }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(queryResponse.data.results.length).toBe(1); + expect(queryResponse.data.results[0].label).toBe('target'); + }); + + it('should handle Decimal128 nested inside an Object field', async () => { + const createResponse = await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/DecimalNested', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + metadata: { + price: { __type: 'Decimal128', value: '19.99' }, + currency: 'USD', + }, + }, + }); + expect(createResponse.data.objectId).toBeDefined(); + + const getResponse = await request({ + url: `http://localhost:8378/1/classes/DecimalNested/${createResponse.data.objectId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(getResponse.data.metadata).toBeDefined(); + expect(getResponse.data.metadata.currency).toBe('USD'); + expect(getResponse.data.metadata.price).toEqual({ + __type: 'Decimal128', + value: '19.99', + }); + }); +}); diff --git a/spec/defaultGraphQLTypes.spec.js b/spec/defaultGraphQLTypes.spec.js index 4e3e311467..72aa573d82 100644 --- a/spec/defaultGraphQLTypes.spec.js +++ b/spec/defaultGraphQLTypes.spec.js @@ -1,4 +1,5 @@ const { Kind } = require('graphql'); +const defaultGraphQLTypes = require('../lib/GraphQL/loaders/defaultGraphQLTypes'); const { TypeValidationError, parseStringValue, @@ -10,9 +11,15 @@ const { parseListValues, parseObjectFields, BYTES, + DECIMAL128, DATE, FILE, -} = require('../lib/GraphQL/loaders/defaultGraphQLTypes'); +} = defaultGraphQLTypes; +const { + transformConstraintTypeToGraphQL, +} = require('../lib/GraphQL/transformers/constraintType'); +const { transformInputTypeToGraphQL } = require('../lib/GraphQL/transformers/inputType'); +const { transformOutputTypeToGraphQL } = require('../lib/GraphQL/transformers/outputType'); function createValue(kind, value, values, fields) { return { @@ -530,6 +537,133 @@ describe('defaultGraphQLTypes', () => { }); }); + describe('Decimal128', () => { + describe('parse literal', () => { + const { parseLiteral } = DECIMAL128; + + it('should parse to Decimal128 if string', () => { + expect(parseLiteral(createValue(Kind.STRING, '123.456'))).toEqual({ + __type: 'Decimal128', + value: '123.456', + }); + }); + + it('should parse to Decimal128 if object', () => { + expect( + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Decimal128' }), + createObjectField('value', { value: '123.456', kind: Kind.STRING }), + ]) + ) + ).toEqual({ + __type: 'Decimal128', + value: '123.456', + }); + }); + + it('should fail if not a valid string or object', () => { + expect(() => parseLiteral({})).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => parseLiteral(createValue(Kind.INT, '123'))).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => parseLiteral([])).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Foo' }), + createObjectField('value', { value: '123.456' }), + ]) + ) + ).toThrow(jasmine.stringMatching('is not a valid Decimal128')); + expect(() => + parseLiteral( + createValue(Kind.OBJECT, undefined, undefined, [ + createObjectField('__type', { value: 'Decimal128' }), + createObjectField('value', { value: '123', kind: Kind.INT }), + ]) + ) + ).toThrow(jasmine.stringMatching('is not a valid Decimal128')); + }); + }); + + describe('parse value', () => { + const { parseValue } = DECIMAL128; + + it('should parse string value', () => { + expect(parseValue('123.456')).toEqual({ + __type: 'Decimal128', + value: '123.456', + }); + }); + + it('should parse object value', () => { + const input = { + __type: 'Decimal128', + value: '123.456', + }; + expect(parseValue(input)).toEqual(input); + }); + + it('should fail if not a valid object or string', () => { + expect(() => parseValue({})).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => + parseValue({ + __type: 'Foo', + value: '123.456', + }) + ).toThrow(jasmine.stringMatching('is not a valid Decimal128')); + expect(() => parseValue([])).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => parseValue(123)).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + }); + }); + + describe('serialize Decimal128 type', () => { + const { serialize } = DECIMAL128; + + it('should do nothing if string', () => { + const str = '123.456'; + expect(serialize(str)).toBe(str); + }); + + it('should return value if object', () => { + const decimal = { + __type: 'Decimal128', + value: '123.456', + }; + expect(serialize(decimal)).toEqual('123.456'); + }); + + it('should fail if not a valid object or string', () => { + expect(() => serialize({})).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => + serialize({ + __type: 'Foo', + value: '123.456', + }) + ).toThrow(jasmine.stringMatching('is not a valid Decimal128')); + expect(() => serialize([])).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + expect(() => serialize(123)).toThrow( + jasmine.stringMatching('is not a valid Decimal128') + ); + }); + }); + }); + describe('File', () => { describe('parse literal', () => { const { parseLiteral } = FILE; @@ -605,4 +739,20 @@ describe('defaultGraphQLTypes', () => { }); }); }); + + describe('Decimal128 GraphQL transformers', () => { + it('should return DECIMAL128_WHERE_INPUT for constraintType', () => { + expect(transformConstraintTypeToGraphQL('Decimal128')).toBe( + defaultGraphQLTypes.DECIMAL128_WHERE_INPUT + ); + }); + + it('should return DECIMAL128 for inputType', () => { + expect(transformInputTypeToGraphQL('Decimal128')).toBe(defaultGraphQLTypes.DECIMAL128); + }); + + it('should return DECIMAL128 for outputType', () => { + expect(transformOutputTypeToGraphQL('Decimal128')).toBe(defaultGraphQLTypes.DECIMAL128); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 45b27f7516..fd664ccc98 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -36,6 +36,8 @@ function mongoFieldToParseSchemaField(type) { return { type: 'Bytes' }; case 'polygon': return { type: 'Polygon' }; + case 'decimal128': + return { type: 'Decimal128' }; } } @@ -149,6 +151,8 @@ function parseFieldTypeToMongoFieldType({ type, targetClass }) { return 'bytes'; case 'Polygon': return 'polygon'; + case 'Decimal128': + return 'decimal128'; } } diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 34481a090b..1ec7433342 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -577,6 +577,8 @@ const transformInteriorAtom = atom => { return DateCoder.JSONToDatabase(atom); } else if (BytesCoder.isValidJSON(atom)) { return BytesCoder.JSONToDatabase(atom); + } else if (Decimal128Coder.isValidJSON(atom)) { + return Decimal128Coder.JSONToDatabase(atom); } else if (typeof atom === 'object' && atom && atom.$regex !== undefined) { return new RegExp(atom.$regex); } else { @@ -635,6 +637,9 @@ function transformTopLevelAtom(atom, field) { if (FileCoder.isValidJSON(atom)) { return FileCoder.JSONToDatabase(atom); } + if (Decimal128Coder.isValidJSON(atom)) { + return Decimal128Coder.JSONToDatabase(atom); + } return CannotTransform; default: @@ -1069,6 +1074,10 @@ const nestedMongoObjectToNestedParseObject = mongoObject => { return mongoObject.value; } + if (Decimal128Coder.isValidDatabaseObject(mongoObject)) { + return Decimal128Coder.databaseToJSON(mongoObject); + } + if (BytesCoder.isValidDatabaseObject(mongoObject)) { return BytesCoder.databaseToJSON(mongoObject); } @@ -1132,6 +1141,10 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { return mongoObject.value; } + if (Decimal128Coder.isValidDatabaseObject(mongoObject)) { + return Decimal128Coder.databaseToJSON(mongoObject); + } + if (BytesCoder.isValidDatabaseObject(mongoObject)) { return BytesCoder.databaseToJSON(mongoObject); } @@ -1269,6 +1282,14 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { restObject[key] = BytesCoder.databaseToJSON(value); break; } + if ( + schema.fields[key] && + schema.fields[key].type === 'Decimal128' && + Decimal128Coder.isValidDatabaseObject(value) + ) { + restObject[key] = Decimal128Coder.databaseToJSON(value); + break; + } } restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]); } @@ -1445,6 +1466,64 @@ var FileCoder = { }, }; +var Decimal128Coder = { + databaseToJSON(object) { + if (object instanceof mongodb.Decimal128) { + return { + __type: 'Decimal128', + value: object.toString(), + }; + } + // Handle deserialized Decimal128 objects (e.g. across BSON boundaries) + const byteArray = new Uint8Array(16); + for (let i = 0; i < 16; i++) { + byteArray[i] = object.bytes[i]; + } + const reconstructed = new mongodb.Decimal128(Buffer.from(byteArray)); + return { + __type: 'Decimal128', + value: reconstructed.toString(), + }; + }, + + isValidDatabaseObject(object) { + if (object instanceof mongodb.Decimal128) { + return true; + } + // Handle Decimal128 objects that have been serialized/deserialized + // across BSON boundaries and lost their prototype + if ( + object && + typeof object === 'object' && + object.bytes && + typeof object.bytes === 'object' && + Object.keys(object).length === 1 + ) { + const keys = Object.keys(object.bytes); + if (keys.length === 16 && keys[0] === '0' && keys[15] === '15') { + return true; + } + } + return false; + }, + + JSONToDatabase(json) { + if (typeof json.value !== 'string') { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'Decimal128 value must be a string'); + } + return mongodb.Decimal128.fromString(json.value); + }, + + isValidJSON(value) { + return ( + typeof value === 'object' && + value !== null && + value.__type === 'Decimal128' && + typeof value.value === 'string' + ); + }, +}; + module.exports = { transformKey, parseObjectToMongoObjectForCreate, diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index e988c9cc19..481bcceaf5 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -46,6 +46,8 @@ const parseTypeToPostgresType = type => { return 'jsonb'; case 'Polygon': return 'polygon'; + case 'Decimal128': + return 'numeric'; case 'Array': if (type.contents && type.contents.type === 'String') { return 'text[]'; @@ -87,6 +89,9 @@ const toPostgresValue = value => { if (value.__type === 'File') { return value.name; } + if (value.__type === 'Decimal128') { + return value.value; + } } return value; }; @@ -418,7 +423,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus index += 3; } else { // TODO: support arrays - values.push(fieldName, fieldValue.$ne); + values.push(fieldName, toPostgresValue(fieldValue.$ne)); index += 2; } } @@ -441,7 +446,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators' ); } else { - values.push(fieldName, fieldValue.$eq); + values.push(fieldName, toPostgresValue(fieldValue.$eq)); patterns.push(`$${index}:name = $${index + 1}`); index += 2; } @@ -488,7 +493,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus values.push(fieldName); baseArray.forEach((listElem, listIndex) => { if (listElem != null) { - values.push(listElem); + values.push(toPostgresValue(listElem)); inPatterns.push(`$${index + 1 + listIndex}`); } }); @@ -797,6 +802,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus index += 2; } + if (fieldValue.__type === 'Decimal128') { + patterns.push(`$${index}:name = $${index + 1}`); + values.push(fieldName, fieldValue.value); + index += 2; + } + Object.keys(ParseToPosgresComparator).forEach(cmp => { if (fieldValue[cmp] || fieldValue[cmp] === 0) { const pgComparator = ParseToPosgresComparator[cmp]; @@ -1430,6 +1441,9 @@ export class PostgresStorageAdapter implements StorageAdapter { case 'File': valuesArray.push(object[fieldName].name); break; + case 'Decimal128': + valuesArray.push(object[fieldName].value); + break; case 'Polygon': { const value = convertPolygonToSQL(object[fieldName].coordinates); valuesArray.push(value); @@ -1691,6 +1705,10 @@ export class PostgresStorageAdapter implements StorageAdapter { updatePatterns.push(`$${index}:name = $${index + 1}`); values.push(fieldName, toPostgresValue(fieldValue)); index += 2; + } else if (fieldValue.__type === 'Decimal128') { + updatePatterns.push(`$${index}:name = $${index + 1}`); + values.push(fieldName, fieldValue.value); + index += 2; } else if (fieldValue.__type === 'GeoPoint') { updatePatterns.push(`$${index}:name = POINT($${index + 1}, $${index + 2})`); values.push(fieldName, fieldValue.longitude, fieldValue.latitude); @@ -1971,6 +1989,12 @@ export class PostgresStorageAdapter implements StorageAdapter { name: object[fieldName], }; } + if (object[fieldName] != null && schema.fields[fieldName].type === 'Decimal128') { + object[fieldName] = { + __type: 'Decimal128', + value: String(object[fieldName]), + }; + } }); //TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field. if (object.createdAt) { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b605fba632..f08a5ee51b 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -502,6 +502,7 @@ const validNonRelationOrPointerTypes = [ 'File', 'Bytes', 'Polygon', + 'Decimal128', ]; // Returns an error suitable for throwing if the type is invalid const fieldTypeIsInvalid = ({ type, targetClass }) => { @@ -1627,6 +1628,11 @@ function getObjectType(obj): ?(SchemaField | string) { return 'Polygon'; } break; + case 'Decimal128': + if (typeof obj.value === 'string') { + return 'Decimal128'; + } + break; } throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'This is not a valid ' + obj.__type); } diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index a7dd523ba5..1cabfd1f31 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -284,6 +284,67 @@ const BYTES = new GraphQLScalarType({ }, }); +const DECIMAL128 = new GraphQLScalarType({ + name: 'Decimal128', + description: + 'The Decimal128 scalar type is used in operations and types that involve high-precision decimal numbers, such as monetary values or blockchain asset amounts.', + parseValue(value) { + if (typeof value === 'string') { + return { + __type: 'Decimal128', + value, + }; + } else if ( + typeof value === 'object' && + value.__type === 'Decimal128' && + typeof value.value === 'string' + ) { + return value; + } + + throw new TypeValidationError(value, 'Decimal128'); + }, + serialize(value) { + if (typeof value === 'string') { + return value; + } else if ( + typeof value === 'object' && + value.__type === 'Decimal128' && + typeof value.value === 'string' + ) { + return value.value; + } + + throw new TypeValidationError(value, 'Decimal128'); + }, + parseLiteral(ast) { + if (ast.kind === Kind.STRING) { + return { + __type: 'Decimal128', + value: ast.value, + }; + } else if (ast.kind === Kind.OBJECT) { + const __type = ast.fields.find(field => field.name.value === '__type'); + const value = ast.fields.find(field => field.name.value === 'value'); + if ( + __type && + __type.value && + __type.value.value === 'Decimal128' && + value && + value.value && + value.value.kind === Kind.STRING + ) { + return { + __type: __type.value.value, + value: value.value.value, + }; + } + } + + throw new TypeValidationError(ast.kind, 'Decimal128'); + }, +}); + const parseFileValue = value => { if (typeof value === 'string') { return { @@ -1097,6 +1158,25 @@ const BYTES_WHERE_INPUT = new GraphQLInputObjectType({ }, }); +const DECIMAL128_WHERE_INPUT = new GraphQLInputObjectType({ + name: 'Decimal128WhereInput', + description: + 'The Decimal128WhereInput input type is used in operations that involve filtering objects by a field of type Decimal128.', + fields: { + equalTo: equalTo(DECIMAL128), + notEqualTo: notEqualTo(DECIMAL128), + lessThan: lessThan(DECIMAL128), + lessThanOrEqualTo: lessThanOrEqualTo(DECIMAL128), + greaterThan: greaterThan(DECIMAL128), + greaterThanOrEqualTo: greaterThanOrEqualTo(DECIMAL128), + in: inOp(DECIMAL128), + notIn: notIn(DECIMAL128), + exists, + inQueryKey, + notInQueryKey, + }, +}); + const FILE_WHERE_INPUT = new GraphQLInputObjectType({ name: 'FileWhereInput', description: @@ -1224,6 +1304,7 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(OBJECT, true); parseGraphQLSchema.addGraphQLType(DATE, true); parseGraphQLSchema.addGraphQLType(BYTES, true); + parseGraphQLSchema.addGraphQLType(DECIMAL128, true); parseGraphQLSchema.addGraphQLType(FILE, true); parseGraphQLSchema.addGraphQLType(FILE_INFO, true); parseGraphQLSchema.addGraphQLType(FILE_INPUT, true); @@ -1248,6 +1329,7 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(OBJECT_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(DATE_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(BYTES_WHERE_INPUT, true); + parseGraphQLSchema.addGraphQLType(DECIMAL128_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(FILE_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_POINT_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(POLYGON_WHERE_INPUT, true); @@ -1280,6 +1362,7 @@ export { serializeDateIso, DATE, BYTES, + DECIMAL128, parseFileValue, SUBQUERY_INPUT, SELECT_INPUT, @@ -1342,6 +1425,7 @@ export { OBJECT_WHERE_INPUT, DATE_WHERE_INPUT, BYTES_WHERE_INPUT, + DECIMAL128_WHERE_INPUT, FILE_WHERE_INPUT, GEO_POINT_WHERE_INPUT, POLYGON_WHERE_INPUT, diff --git a/src/GraphQL/transformers/constraintType.js b/src/GraphQL/transformers/constraintType.js index 6da986af30..2abc9cfab9 100644 --- a/src/GraphQL/transformers/constraintType.js +++ b/src/GraphQL/transformers/constraintType.js @@ -35,6 +35,8 @@ const transformConstraintTypeToGraphQL = (parseType, targetClass, parseClassType return defaultGraphQLTypes.POLYGON_WHERE_INPUT; case 'Bytes': return defaultGraphQLTypes.BYTES_WHERE_INPUT; + case 'Decimal128': + return defaultGraphQLTypes.DECIMAL128_WHERE_INPUT; case 'ACL': return defaultGraphQLTypes.OBJECT_WHERE_INPUT; case 'Relation': diff --git a/src/GraphQL/transformers/inputType.js b/src/GraphQL/transformers/inputType.js index bba838bcd3..ff09722f8b 100644 --- a/src/GraphQL/transformers/inputType.js +++ b/src/GraphQL/transformers/inputType.js @@ -43,6 +43,8 @@ const transformInputTypeToGraphQL = (parseType, targetClass, parseClassTypes) => return defaultGraphQLTypes.POLYGON_INPUT; case 'Bytes': return defaultGraphQLTypes.BYTES; + case 'Decimal128': + return defaultGraphQLTypes.DECIMAL128; case 'ACL': return defaultGraphQLTypes.ACL_INPUT; default: diff --git a/src/GraphQL/transformers/outputType.js b/src/GraphQL/transformers/outputType.js index 81afd421d1..306ca9387b 100644 --- a/src/GraphQL/transformers/outputType.js +++ b/src/GraphQL/transformers/outputType.js @@ -43,6 +43,8 @@ const transformOutputTypeToGraphQL = (parseType, targetClass, parseClassTypes) = return defaultGraphQLTypes.POLYGON; case 'Bytes': return defaultGraphQLTypes.BYTES; + case 'Decimal128': + return defaultGraphQLTypes.DECIMAL128; case 'ACL': return new GraphQLNonNull(defaultGraphQLTypes.ACL); default: