diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheet.js b/packages/graphql/src/connections/GoogleSheet/GoogleSheet.js index e4867d666..429f7d28b 100644 --- a/packages/graphql/src/connections/GoogleSheet/GoogleSheet.js +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheet.js @@ -17,6 +17,8 @@ import schema from './GoogleSheetSchema.json'; import GoogleSheetAppendMany from './GoogleSheetAppendMany/GoogleSheetAppendMany'; import GoogleSheetAppendOne from './GoogleSheetAppendOne/GoogleSheetAppendOne'; +import GoogleSheetDeleteOne from './GoogleSheetDeleteOne/GoogleSheetDeleteOne'; +import GoogleSheetDeleteMany from './GoogleSheetDeleteMany/GoogleSheetDeleteMany'; import GoogleSheetGetMany from './GoogleSheetGetMany/GoogleSheetGetMany'; import GoogleSheetGetOne from './GoogleSheetGetOne/GoogleSheetGetOne'; @@ -25,6 +27,8 @@ export default { requests: { GoogleSheetAppendMany, GoogleSheetAppendOne, + GoogleSheetDeleteOne, + GoogleSheetDeleteMany, GoogleSheetGetMany, GoogleSheetGetOne, }, diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheet.test.js b/packages/graphql/src/connections/GoogleSheet/GoogleSheet.test.js index 5bcec6adf..c3fe5cc66 100644 --- a/packages/graphql/src/connections/GoogleSheet/GoogleSheet.test.js +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheet.test.js @@ -21,7 +21,10 @@ import { ConfigurationError } from '../../context/errors'; const { schema } = GoogleSheet; test('All requests are present', () => { + expect(GoogleSheet.requests.GoogleSheetAppendOne).toBeDefined(); expect(GoogleSheet.requests.GoogleSheetAppendMany).toBeDefined(); + expect(GoogleSheet.requests.GoogleSheetDeleteOne).toBeDefined(); + expect(GoogleSheet.requests.GoogleSheetDeleteMany).toBeDefined(); expect(GoogleSheet.requests.GoogleSheetGetMany).toBeDefined(); expect(GoogleSheet.requests.GoogleSheetGetOne).toBeDefined(); }); diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteMany.js b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteMany.js new file mode 100644 index 000000000..98ecbc22d --- /dev/null +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteMany.js @@ -0,0 +1,41 @@ +/* + Copyright 2020 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import schema from './GoogleSheetDeleteManySchema.json'; +import getSheet from '../getSheet'; +import mingoFilter from '../../../utils/mingoFilter'; + +async function googleSheetDeleteMany({ request, connection }) { + const { filter, options = {} } = request; + const { limit, skip } = options; + const sheet = await getSheet({ connection }); + let rows = await sheet.getRows({ limit, offset: skip }); + rows = mingoFilter({ input: rows, filter }); + if (rows.length === 0) { + return { + deletedCount: 0, + }; + } + const promises = rows.map(async (row) => { + await row.delete(); + }); + await Promise.all(promises); + return { + deletedCount: rows.length, + }; +} + +export default { resolver: googleSheetDeleteMany, schema, checkRead: false, checkWrite: true }; diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteMany.test.js b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteMany.test.js new file mode 100644 index 000000000..ce3683e84 --- /dev/null +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteMany.test.js @@ -0,0 +1,189 @@ +/* + Copyright 2020 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import GoogleSheetDeleteMany from './GoogleSheetDeleteMany'; +import { ConfigurationError } from '../../../context/errors'; +import testSchema from '../../../utils/testSchema'; + +const mockGetRows = jest.fn(); +const mockDelete = jest.fn(); +jest.mock('../getSheet', () => () => ({ + getRows: mockGetRows, +})); + +const mockGetRowsDefaultImp = ({ limit, offset }) => { + const rows = [ + { + _rowNumber: 2, + _rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'], + id: '1', + name: 'John', + age: '34', + birth_date: '2020/04/26', + married: 'TRUE', + _sheet: {}, + delete: mockDelete, + }, + { + _rowNumber: 3, + _rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'], + id: '2', + name: 'Steve', + age: '43', + birth_date: '2020/04/27', + married: 'FALSE', + _sheet: {}, + delete: mockDelete, + }, + { + _rowNumber: 4, + _rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'], + id: '3', + name: 'Tim', + age: '34', + birth_date: '2020/04/28', + married: 'FALSE', + _sheet: {}, + delete: mockDelete, + }, + { + _rowNumber: 5, + _rawData: ['4', 'Craig', '21', '2020-04-25T22:00:00.000Z', 'TRUE'], + id: '4', + name: 'Craig', + age: '120', + birth_date: '2020-04-25T22:00:00.000Z', + married: 'TRUE', + _sheet: {}, + delete: mockDelete, + }, + ]; + return Promise.resolve(rows.slice(offset).slice(undefined, limit)); +}; + +const { resolver, schema, checkRead, checkWrite } = GoogleSheetDeleteMany; + +beforeEach(() => { + mockGetRows.mockReset(); + mockDelete.mockReset(); +}); + +test('googleSheetDeleteMany, match one', async () => { + mockGetRows.mockImplementation(mockGetRowsDefaultImp); + const res = await resolver({ + request: { + filter: { id: '1' }, + }, + connection: {}, + }); + expect(res).toEqual({ + deletedCount: 1, + }); + expect(mockDelete).toHaveBeenCalledTimes(1); +}); + +test('googleSheetDeleteMany, match nothing', async () => { + mockGetRows.mockImplementation(mockGetRowsDefaultImp); + const res = await resolver({ + request: { + filter: { id: 'does_not_exist' }, + }, + connection: {}, + }); + expect(res).toEqual({ + deletedCount: 0, + }); + expect(mockDelete).toHaveBeenCalledTimes(0); +}); + +test('googleSheetDeleteMany, match more than one', async () => { + mockGetRows.mockImplementation(mockGetRowsDefaultImp); + const res = await resolver({ + request: { + filter: { _rowNumber: { $gt: 3 } }, + }, + connection: {}, + }); + expect(res).toEqual({ + deletedCount: 2, + }); + expect(mockDelete).toHaveBeenCalledTimes(2); +}); + +test('valid request schema', () => { + const request = { + filter: { id: '1' }, + }; + expect(testSchema({ schema, object: request })).toBe(true); +}); + +test('request properties is not an object', () => { + const request = 'request'; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteMany request properties should be an object.' + ); +}); + +test('filter is not an object', () => { + const request = { + filter: true, + }; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteMany request property "filter" should be an object.' + ); +}); + +test('filter is missing', () => { + const request = {}; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteMany request should have required property "filter".' + ); +}); + +test('limit is not a number', () => { + const request = { + options: { + limit: true, + }, + }; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteMany request property "options.limit" should be a number.' + ); +}); + +test('skip is not a number', () => { + const request = { + options: { + skip: true, + }, + }; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteMany request property "options.skip" should be a number.' + ); +}); + +test('checkRead should be false', async () => { + expect(checkRead).toBe(false); +}); + +test('checkWrite should be true', async () => { + expect(checkWrite).toBe(true); +}); diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteManySchema.json b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteManySchema.json new file mode 100644 index 000000000..cc0395dc5 --- /dev/null +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteMany/GoogleSheetDeleteManySchema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Lowdefy Request Schema - GoogleSheetDeleteMany", + "type": "object", + "required": ["filter"], + "properties": { + "filter": { + "type": "object", + "description": "A MongoDB query expression to filter the data. All rows matched by the filter will be deleted", + "errorMessage": { + "type": "GoogleSheetDeleteMany request property \"filter\" should be an object." + } + }, + "options": { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "The maximum number of rows to fetch.", + "errorMessage": { + "type": "GoogleSheetDeleteMany request property \"options.limit\" should be a number." + } + }, + "skip": { + "type": "number", + "description": "The number of rows to skip from the top of the sheet.", + "errorMessage": { + "type": "GoogleSheetDeleteMany request property \"options.skip\" should be a number." + } + } + } + } + }, + "errorMessage": { + "type": "GoogleSheetDeleteMany request properties should be an object.", + "required": { + "filter": "GoogleSheetDeleteMany request should have required property \"filter\"." + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOne.js b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOne.js new file mode 100644 index 000000000..3b1cbb72b --- /dev/null +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOne.js @@ -0,0 +1,41 @@ +/* + Copyright 2020 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import schema from './GoogleSheetDeleteOneSchema.json'; +import cleanRows from '../cleanRows'; +import getSheet from '../getSheet'; +import mingoFilter from '../../../utils/mingoFilter'; + +async function googleSheetDeleteOne({ request, connection }) { + const { filter, options = {} } = request; + const { limit, skip } = options; + const sheet = await getSheet({ connection }); + let rows = await sheet.getRows({ limit, offset: skip }); + rows = mingoFilter({ input: rows, filter }); + if (rows.length === 0) { + return { + deletedCount: 0, + }; + } + const row = rows[0]; + await row.delete(); + return { + deletedCount: 1, + row: cleanRows(row), + }; +} + +export default { resolver: googleSheetDeleteOne, schema, checkRead: false, checkWrite: true }; diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOne.test.js b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOne.test.js new file mode 100644 index 000000000..899d1bf96 --- /dev/null +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOne.test.js @@ -0,0 +1,204 @@ +/* + Copyright 2020 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import GoogleSheetDeleteOne from './GoogleSheetDeleteOne'; +import { ConfigurationError } from '../../../context/errors'; +import testSchema from '../../../utils/testSchema'; + +const mockGetRows = jest.fn(); +const mockDelete = jest.fn(); +jest.mock('../getSheet', () => () => ({ + getRows: mockGetRows, +})); + +const mockGetRowsDefaultImp = ({ limit, offset }) => { + const rows = [ + { + _rowNumber: 2, + _rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'], + id: '1', + name: 'John', + age: '34', + birth_date: '2020/04/26', + married: 'TRUE', + _sheet: {}, + delete: mockDelete, + }, + { + _rowNumber: 3, + _rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'], + id: '2', + name: 'Steve', + age: '43', + birth_date: '2020/04/27', + married: 'FALSE', + _sheet: {}, + delete: mockDelete, + }, + { + _rowNumber: 4, + _rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'], + id: '3', + name: 'Tim', + age: '34', + birth_date: '2020/04/28', + married: 'FALSE', + _sheet: {}, + delete: mockDelete, + }, + { + _rowNumber: 5, + _rawData: ['4', 'Craig', '21', '2020-04-25T22:00:00.000Z', 'TRUE'], + id: '4', + name: 'Craig', + age: '120', + birth_date: '2020-04-25T22:00:00.000Z', + married: 'TRUE', + _sheet: {}, + delete: mockDelete, + }, + ]; + return Promise.resolve(rows.slice(offset).slice(undefined, limit)); +}; + +const { resolver, schema, checkRead, checkWrite } = GoogleSheetDeleteOne; + +test('googleSheetDeleteMany, match one', async () => { + mockGetRows.mockImplementation(mockGetRowsDefaultImp); + const res = await resolver({ + request: { + filter: { id: '1' }, + }, + connection: {}, + }); + expect(res).toEqual({ + deletedCount: 1, + row: { + _rowNumber: 2, + _rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'], + id: '1', + name: 'John', + age: '34', + birth_date: '2020/04/26', + married: 'TRUE', + delete: mockDelete, + }, + }); + expect(mockDelete).toHaveBeenCalledTimes(1); +}); + +test('googleSheetDeleteMany, match nothing', async () => { + mockGetRows.mockImplementation(mockGetRowsDefaultImp); + const res = await resolver({ + request: { + filter: { id: 'does_not_exist' }, + }, + connection: {}, + }); + expect(res).toEqual({ + deletedCount: 0, + }); + expect(mockDelete).toHaveBeenCalledTimes(0); +}); + +test('googleSheetDeleteMany, match more than one', async () => { + mockGetRows.mockImplementation(mockGetRowsDefaultImp); + const res = await resolver({ + request: { + filter: { _rowNumber: { $gt: 3 } }, + }, + connection: {}, + }); + expect(res).toEqual({ + deletedCount: 1, + row: { + _rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'], + _rowNumber: 4, + age: '34', + birth_date: '2020/04/28', + delete: mockDelete, + id: '3', + married: 'FALSE', + name: 'Tim', + }, + }); + expect(mockDelete).toHaveBeenCalledTimes(1); +}); + +test('valid request schema', () => { + const request = { + filter: { id: '1' }, + }; + expect(testSchema({ schema, object: request })).toBe(true); +}); + +test('request properties is not an object', () => { + const request = 'request'; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteOne request properties should be an object.' + ); +}); + +test('filter is not an object', () => { + const request = { + filter: true, + }; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteOne request property "filter" should be an object.' + ); +}); + +test('filter is missing', () => { + const request = {}; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteOne request should have required property "filter".' + ); +}); + +test('limit is not a number', () => { + const request = { + options: { + limit: true, + }, + }; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteOne request property "options.limit" should be a number.' + ); +}); + +test('skip is not a number', () => { + const request = { + options: { + skip: true, + }, + }; + expect(() => testSchema({ schema, object: request })).toThrow(ConfigurationError); + expect(() => testSchema({ schema, object: request })).toThrow( + 'GoogleSheetDeleteOne request property "options.skip" should be a number.' + ); +}); + +test('checkRead should be false', async () => { + expect(checkRead).toBe(false); +}); + +test('checkWrite should be true', async () => { + expect(checkWrite).toBe(true); +}); diff --git a/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOneSchema.json b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOneSchema.json new file mode 100644 index 000000000..2b28e5ff0 --- /dev/null +++ b/packages/graphql/src/connections/GoogleSheet/GoogleSheetDeleteOne/GoogleSheetDeleteOneSchema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Lowdefy Request Schema - GoogleSheetDeleteOne", + "type": "object", + "required": ["filter"], + "properties": { + "filter": { + "type": "object", + "description": "A MongoDB query expression to filter the data. The first row matched by the filter will be deleted", + "errorMessage": { + "type": "GoogleSheetDeleteOne request property \"filter\" should be an object." + } + }, + "options": { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "The maximum number of rows to fetch.", + "errorMessage": { + "type": "GoogleSheetDeleteOne request property \"options.limit\" should be a number." + } + }, + "skip": { + "type": "number", + "description": "The number of rows to skip from the top of the sheet.", + "errorMessage": { + "type": "GoogleSheetDeleteOne request property \"options.skip\" should be a number." + } + } + } + } + }, + "errorMessage": { + "type": "GoogleSheetDeleteOne request properties should be an object.", + "required": { + "filter": "GoogleSheetDeleteOne request should have required property \"filter\"." + } + } +} \ No newline at end of file