feat: Init @lowdefy/connection-google-sheets package.

This commit is contained in:
Sam Tolmay 2021-11-04 18:10:07 +02:00
parent 867a6db576
commit 4ffa94f8b7
No known key found for this signature in database
GPG Key ID: D004126FCD1A6DF0
41 changed files with 5203 additions and 0 deletions

24
.pnp.cjs generated
View File

@ -86,6 +86,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"name": "@lowdefy/connection-elasticsearch",
"reference": "workspace:packages/connections/elasticsearch"
},
{
"name": "@lowdefy/connection-google-sheets",
"reference": "workspace:packages/connections/google-sheets"
},
{
"name": "@lowdefy/connection-knex",
"reference": "workspace:packages/connections/knex"
@ -189,6 +193,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@lowdefy/connection-aws", ["workspace:packages/connections/aws"]],
["@lowdefy/connection-axios-http", ["virtual:c9d7c5a0f7602869dff02ed24b6a4fe62d4c9e4a4ede33ec34082ee9e4a5dd17f3e1bb396d56863e9bea8e8476d67351fe495fe7cebce9035a9e4de117e68169#workspace:packages/connections/axios-http", "workspace:packages/connections/axios-http"]],
["@lowdefy/connection-elasticsearch", ["workspace:packages/connections/elasticsearch"]],
["@lowdefy/connection-google-sheets", ["workspace:packages/connections/google-sheets"]],
["@lowdefy/connection-knex", ["workspace:packages/connections/knex"]],
["@lowdefy/connection-mongodb", ["workspace:packages/connections/mongodb"]],
["@lowdefy/connection-sendgrid", ["workspace:packages/connections/sendgrid"]],
@ -5239,6 +5244,25 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "SOFT",
}]
]],
["@lowdefy/connection-google-sheets", [
["workspace:packages/connections/google-sheets", {
"packageLocation": "./packages/connections/google-sheets/",
"packageDependencies": [
["@lowdefy/connection-google-sheets", "workspace:packages/connections/google-sheets"],
["@babel/cli", "virtual:4a7337632ff6e9ee5a1c45a62a9ff4cc325a9367b21424babda93e269fe01b671e885bc41bdeebafb83c81f2a8eebbf0102043354a4e58905f61c8c3387cda1e#npm:7.15.7"],
["@babel/core", "npm:7.15.8"],
["@babel/preset-env", "virtual:4a7337632ff6e9ee5a1c45a62a9ff4cc325a9367b21424babda93e269fe01b671e885bc41bdeebafb83c81f2a8eebbf0102043354a4e58905f61c8c3387cda1e#npm:7.15.8"],
["@lowdefy/ajv", "workspace:packages/ajv"],
["@lowdefy/helpers", "workspace:packages/helpers"],
["babel-jest", "virtual:4a7337632ff6e9ee5a1c45a62a9ff4cc325a9367b21424babda93e269fe01b671e885bc41bdeebafb83c81f2a8eebbf0102043354a4e58905f61c8c3387cda1e#npm:27.3.1"],
["google-spreadsheet", "npm:3.1.15"],
["jest", "npm:26.6.3"],
["mingo", "npm:4.2.0"],
["moment", "npm:2.29.1"]
],
"linkType": "SOFT",
}]
]],
["@lowdefy/connection-knex", [
["workspace:packages/connections/knex", {
"packageLocation": "./packages/connections/knex/",

View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "14"
}
}
]
]
}

View File

@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['src/**/*.js'],
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/test/', '<rootDir>/src/index.js'],
coverageReporters: [['lcov', { projectRoot: '../../..' }], 'text', 'clover'],
errorOnDeprecated: true,
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/test/'],
};

View File

@ -0,0 +1,59 @@
{
"name": "@lowdefy/connection-google-sheets",
"version": "3.22.0",
"licence": "Apache-2.0",
"description": "",
"homepage": "https://lowdefy.com",
"keywords": [
"lowdefy",
"lowdefy connection"
],
"bugs": {
"url": "https://github.com/lowdefy/lowdefy/issues"
},
"contributors": [
{
"name": "Sam Tolmay",
"url": "https://github.com/SamTolmay"
},
{
"name": "Gerrie van Wyk",
"url": "https://github.com/Gervwyk"
}
],
"repository": {
"type": "git",
"url": "https://github.com/lowdefy/lowdefy.git"
},
"main": "dist/index.js",
"files": [
"dist/*"
],
"scripts": {
"babel": "babel src --out-dir dist",
"build": "rm -rf dist && yarn babel",
"clean": "rm -rf dist",
"prepare": "yarn build",
"test": "jest --coverage"
},
"dependencies": {
"@lowdefy/helpers": "3.22.0",
"google-spreadsheet": "3.1.15",
"mingo": "4.2.0",
"moment": "2.29.1"
},
"devDependencies": {
"@babel/cli": "7.15.7",
"@babel/core": "7.15.8",
"@babel/preset-env": "7.15.8",
"@lowdefy/ajv": "3.22.0",
"babel-jest": "27.3.1",
"jest": "26.6.3"
},
"peerDependencies": {
"@lowdefy/api": "3.22.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,37 @@
/*
Copyright 2020-2021 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 './GoogleSheetSchema.json';
import GoogleSheetAppendMany from './GoogleSheetAppendMany/GoogleSheetAppendMany';
import GoogleSheetAppendOne from './GoogleSheetAppendOne/GoogleSheetAppendOne';
import GoogleSheetDeleteOne from './GoogleSheetDeleteOne/GoogleSheetDeleteOne';
import GoogleSheetGetMany from './GoogleSheetGetMany/GoogleSheetGetMany';
import GoogleSheetGetOne from './GoogleSheetGetOne/GoogleSheetGetOne';
import GoogleSheetUpdateOne from './GoogleSheetUpdateOne/GoogleSheetUpdateOne';
import GoogleSheetUpdateMany from './GoogleSheetUpdateMany/GoogleSheetUpdateMany';
export default {
schema,
requests: {
GoogleSheetAppendMany,
GoogleSheetAppendOne,
GoogleSheetDeleteOne,
GoogleSheetGetMany,
GoogleSheetGetOne,
GoogleSheetUpdateOne,
GoogleSheetUpdateMany,
},
};

View File

@ -0,0 +1,166 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheet from './GoogleSheet';
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.GoogleSheetGetMany).toBeDefined();
expect(GoogleSheet.requests.GoogleSheetGetOne).toBeDefined();
expect(GoogleSheet.requests.GoogleSheetUpdateOne).toBeDefined();
expect(GoogleSheet.requests.GoogleSheetUpdateMany).toBeDefined();
});
test('valid connection schema', () => {
const connection = {
apiKey: 'apiKey',
sheetIndex: 0,
spreadsheetId: 'spreadsheetId',
};
expect(validate({ schema, data: connection })).toEqual({ valid: true });
});
test('valid connection schema, all properties', () => {
const connection = {
apiKey: 'apiKey',
client_email: 'client_email',
private_key: 'private_key',
sheetId: 'sheetId',
sheetIndex: 0,
spreadsheetId: 'spreadsheetId',
};
expect(validate({ schema, data: connection })).toEqual({ valid: true });
});
test('connection properties is not an object', () => {
const connection = 'connection';
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection properties should be an object.'
);
});
test('spreadsheetId missing', () => {
const connection = {};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection should have required property "spreadsheetId".'
);
});
test('spreadsheetId is not a string', () => {
const connection = {
spreadsheetId: true,
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "spreadsheetId" should be a string.'
);
});
test('apiKey is not a string', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
apiKey: true,
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "apiKey" should be a string.'
);
});
test('client_email is not a string', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
client_email: true,
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "client_email" should be a string.'
);
});
test('private_key is not a string', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
private_key: true,
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "private_key" should be a string.'
);
});
test('sheetId is not a string', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
sheetId: true,
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "sheetId" should be a string.'
);
});
test('sheetIndex is not a number', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
sheetIndex: '',
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "sheetIndex" should be a number.'
);
});
test('columnTypes is not an object', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
columnTypes: '',
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "columnTypes" should be an object.'
);
});
test('columnTypes type is invalid', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
columnTypes: {
column: 'invalid',
},
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "/columnTypes/column" should be one of "string", "number", "boolean", "date", or "json".'
);
});
test('read is not a boolean', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
read: 'read',
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "read" should be a boolean.'
);
});
test('write is not a boolean', () => {
const connection = {
spreadsheetId: 'spreadsheetId',
write: 'write',
};
expect(() => validate({ schema, data: connection })).toThrow(
'GoogleSheet connection property "write" should be a boolean.'
);
});

View File

@ -0,0 +1,31 @@
/*
Copyright 2020-2021 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 './GoogleSheetAppendManySchema.json';
import getSheet from '../getSheet';
import { transformWrite } from '../transformTypes';
async function googleSheetAppendMany({ request, connection }) {
const { rows, options = {} } = request;
const { raw } = options;
const sheet = await getSheet({ connection });
await sheet.addRows(transformWrite({ input: rows, types: connection.columnTypes }), { raw });
return {
insertedCount: rows.length,
};
}
export default { resolver: googleSheetAppendMany, schema, checkRead: false, checkWrite: true };

View File

@ -0,0 +1,273 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetAppendMany from './GoogleSheetAppendMany';
const mockAddRows = jest.fn();
jest.mock('../getSheet', () => () => ({
addRows: mockAddRows,
}));
const { resolver, schema, checkRead, checkWrite } = GoogleSheetAppendMany;
const mockAddRowsDefaultImp = (rows) => rows.map((row) => ({ ...row, _sheet: {} }));
test('googleSheetAppendMany, one row', async () => {
mockAddRows.mockImplementation(mockAddRowsDefaultImp);
const res = await resolver({
request: {
rows: [{ id: '1', name: 'John', age: '34', birth_date: '2020/04/26', married: 'TRUE' }],
},
connection: {},
});
expect(res).toEqual({ insertedCount: 1 });
expect(mockAddRows.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Array [
Object {
"age": "34",
"birth_date": "2020/04/26",
"id": "1",
"married": "TRUE",
"name": "John",
},
],
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetAppendMany, two rows', async () => {
mockAddRows.mockImplementation(mockAddRowsDefaultImp);
const res = await resolver({
request: {
rows: [
{ id: '1', name: 'John', age: '34', birth_date: '2020/04/26', married: 'TRUE' },
{ id: '2', name: 'Peter', age: '34', birth_date: '2020/04/26', married: 'TRUE' },
],
},
connection: {},
});
expect(res).toEqual({ insertedCount: 2 });
expect(mockAddRows.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Array [
Object {
"age": "34",
"birth_date": "2020/04/26",
"id": "1",
"married": "TRUE",
"name": "John",
},
Object {
"age": "34",
"birth_date": "2020/04/26",
"id": "2",
"married": "TRUE",
"name": "Peter",
},
],
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetAppendMany, rows empty array', async () => {
mockAddRows.mockImplementation(mockAddRowsDefaultImp);
const res = await resolver({
request: {
rows: [],
},
connection: {},
});
expect(res).toEqual({ insertedCount: 0 });
expect(mockAddRows.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Array [],
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetAppendMany, transform types', async () => {
mockAddRows.mockImplementation(mockAddRowsDefaultImp);
const res = await resolver({
request: {
rows: [
{
id: '1',
name: 'John',
age: 34,
birth_date: new Date('2020-04-26T00:00:00.000Z'),
married: true,
},
],
},
connection: {
columnTypes: {
name: 'string',
age: 'number',
married: 'boolean',
birth_date: 'date',
},
},
});
expect(res).toEqual({ insertedCount: 1 });
expect(mockAddRows.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Array [
Object {
"age": "34",
"birth_date": "2020-04-26T00:00:00.000Z",
"id": "1",
"married": "TRUE",
"name": "John",
},
],
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetAppendMany, one row, raw true', async () => {
mockAddRows.mockImplementation(mockAddRowsDefaultImp);
const res = await resolver({
request: {
rows: [{ id: '1', name: 'John', age: '34', birth_date: '2020/04/26', married: 'TRUE' }],
options: {
raw: true,
},
},
connection: {},
});
expect(res).toEqual({ insertedCount: 1 });
expect(mockAddRows.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Array [
Object {
"age": "34",
"birth_date": "2020/04/26",
"id": "1",
"married": "TRUE",
"name": "John",
},
],
Object {
"raw": true,
},
],
]
`);
});
test('valid request schema', () => {
const request = {
rows: [
{
name: 'name',
},
],
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request schema, all options', () => {
const request = {
rows: [
{
name: 'name',
},
],
options: {
raw: true,
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendMany request properties should be an object.'
);
});
test('rows is not an array', () => {
const request = {
rows: true,
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendMany request property "rows" should be an array.'
);
});
test('rows is not an array of objects', () => {
const request = {
rows: [1, 2, 3],
};
// Gives an error message for each item in array
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendMany request property "rows" should be an array of objects.; GoogleSheetAppendMany request property "rows" should be an array of objects.'
);
});
test('rows is missing', () => {
const request = {};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendMany request should have required property "rows".'
);
});
test('raw is not a boolean', () => {
const request = {
rows: [
{
name: 'name',
},
],
options: {
raw: 'raw',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendMany request property "options.raw" should be a boolean.'
);
});
test('checkRead should be false', async () => {
expect(checkRead).toBe(false);
});
test('checkWrite should be true', async () => {
expect(checkWrite).toBe(true);
});

View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - GoogleSheetAppendMany",
"type": "object",
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"description": "The rows to insert into the sheet. An an array of objects where keys are the column names and values are the values to insert.",
"errorMessage": {
"type": "GoogleSheetAppendMany request property \"rows\" should be an array."
},
"items": {
"type": "object",
"description": "The row to insert into the sheet. An object where keys are the column names and values are the values to insert.",
"errorMessage": {
"type": "GoogleSheetAppendMany request property \"rows\" should be an array of objects."
}
}
},
"options": {
"type": "object",
"properties": {
"raw": {
"type": "boolean",
"description": "Store raw values instead of converting as if typed into the sheets UI.",
"errorMessage": {
"type": "GoogleSheetAppendMany request property \"options.raw\" should be a boolean."
}
}
}
}
},
"errorMessage": {
"type": "GoogleSheetAppendMany request properties should be an object.",
"required": {
"rows": "GoogleSheetAppendMany request should have required property \"rows\"."
}
}
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2020-2021 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 './GoogleSheetAppendOneSchema.json';
import getSheet from '../getSheet';
import cleanRows from '../cleanRows';
import { transformWrite } from '../transformTypes';
async function googleSheetAppendOne({ request, connection }) {
const { row, options = {} } = request;
const { raw } = options;
const sheet = await getSheet({ connection });
const insertedRow = await sheet.addRow(
transformWrite({ input: row, types: connection.columnTypes }),
{ raw }
);
return {
insertedCount: 1,
row: cleanRows(insertedRow),
};
}
export default { resolver: googleSheetAppendOne, schema, checkRead: false, checkWrite: true };

View File

@ -0,0 +1,216 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetAppendOne from './GoogleSheetAppendOne';
const mockAddRow = jest.fn();
jest.mock('../getSheet', () => () => ({
addRow: mockAddRow,
}));
const { resolver, schema, checkRead, checkWrite } = GoogleSheetAppendOne;
const mockAddRowDefaultImp = (row) => ({ ...row, _sheet: {} });
test('googleSheetAppendOne', async () => {
mockAddRow.mockImplementation(mockAddRowDefaultImp);
const res = await resolver({
request: {
row: { id: '1', name: 'John', age: '34', birth_date: '2020/04/26', married: 'TRUE' },
},
connection: {},
});
expect(res).toEqual({
insertedCount: 1,
row: {
id: '1',
name: 'John',
age: '34',
birth_date: '2020/04/26',
married: 'TRUE',
},
});
expect(mockAddRow.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"age": "34",
"birth_date": "2020/04/26",
"id": "1",
"married": "TRUE",
"name": "John",
},
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetAppendOne, transform types', async () => {
mockAddRow.mockImplementation(mockAddRowDefaultImp);
const res = await resolver({
request: {
row: {
id: '1',
name: 'John',
age: 34,
birth_date: new Date('2020-04-26T00:00:00.000Z'),
married: true,
},
},
connection: {
columnTypes: {
name: 'string',
age: 'number',
married: 'boolean',
birth_date: 'date',
},
},
});
expect(res).toEqual({
insertedCount: 1,
row: {
id: '1',
name: 'John',
age: '34',
birth_date: '2020-04-26T00:00:00.000Z',
married: 'TRUE',
},
});
expect(mockAddRow.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"age": "34",
"birth_date": "2020-04-26T00:00:00.000Z",
"id": "1",
"married": "TRUE",
"name": "John",
},
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetAppendOne, raw true', async () => {
mockAddRow.mockImplementation(mockAddRowDefaultImp);
const res = await resolver({
request: {
row: { id: '1', name: 'John', age: '34', birth_date: '2020/04/26', married: 'TRUE' },
options: {
raw: true,
},
},
connection: {},
});
expect(res).toEqual({
insertedCount: 1,
row: {
id: '1',
name: 'John',
age: '34',
birth_date: '2020/04/26',
married: 'TRUE',
},
});
expect(mockAddRow.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"age": "34",
"birth_date": "2020/04/26",
"id": "1",
"married": "TRUE",
"name": "John",
},
Object {
"raw": true,
},
],
]
`);
});
test('valid request schema', () => {
const request = {
row: {
name: 'name',
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request schema, all options', () => {
const request = {
row: {
name: 'name',
},
options: {
raw: true,
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendOne request properties should be an object.'
);
});
test('row is not an object', () => {
const request = {
row: true,
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendOne request property "row" should be an object.'
);
});
test('row is missing', () => {
const request = {};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendOne request should have required property "row".'
);
});
test('raw is not a boolean', () => {
const request = {
row: {
name: 'name',
},
options: {
raw: 'raw',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetAppendOne request property "options.raw" should be a boolean.'
);
});
test('checkRead should be false', async () => {
expect(checkRead).toBe(false);
});
test('checkWrite should be true', async () => {
expect(checkWrite).toBe(true);
});

View File

@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - GoogleSheetAppendOne",
"type": "object",
"required": ["row"],
"properties": {
"row": {
"type": "object",
"description": "The row to insert into the sheet. An object where keys are the column names and values are the values to insert.",
"errorMessage": {
"type": "GoogleSheetAppendOne request property \"row\" should be an object."
}
},
"options": {
"type": "object",
"properties": {
"raw": {
"type": "boolean",
"description": "Store raw values instead of converting as if typed into the sheets UI.",
"errorMessage": {
"type": "GoogleSheetAppendOne request property \"options.raw\" should be a boolean."
}
}
}
}
},
"errorMessage": {
"type": "GoogleSheetAppendOne request properties should be an object.",
"required": {
"row": "GoogleSheetAppendOne request should have required property \"row\"."
}
}
}

View File

@ -0,0 +1,43 @@
/*
Copyright 2020-2021 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 { transformRead } from '../transformTypes';
import mingoFilter from '../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 = transformRead({ input: rows, types: connection.columnTypes });
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 };

View File

@ -0,0 +1,198 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetDeleteOne from './GoogleSheetDeleteOne';
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(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetDeleteOne request properties should be an object.'
);
});
test('filter is not an object', () => {
const request = {
filter: true,
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetDeleteOne request property "filter" should be an object.'
);
});
test('filter is missing', () => {
const request = {};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetDeleteOne request should have required property "filter".'
);
});
test('limit is not a number', () => {
const request = {
options: {
limit: true,
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetDeleteOne request property "options.limit" should be a number.'
);
});
test('skip is not a number', () => {
const request = {
options: {
skip: true,
},
};
expect(() => validate({ schema, data: 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);
});

View File

@ -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\"."
}
}
}

View File

@ -0,0 +1,40 @@
/*
Copyright 2020-2021 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 './GoogleSheetGetManySchema.json';
import cleanRows from '../cleanRows';
import getSheet from '../getSheet';
import { transformRead } from '../transformTypes';
import mingoAggregation from '../mingoAggregation';
import mingoFilter from '../mingoFilter';
async function googleSheetGetMany({ request, connection }) {
const { filter, pipeline, options = {} } = request;
const { limit, skip } = options;
const sheet = await getSheet({ connection });
let rows = await sheet.getRows({ limit, offset: skip });
rows = cleanRows(rows);
rows = transformRead({ input: rows, types: connection.columnTypes });
if (filter) {
rows = mingoFilter({ input: rows, filter });
}
if (pipeline) {
rows = mingoAggregation({ input: rows, pipeline });
}
return rows;
}
export default { resolver: googleSheetGetMany, schema, checkRead: true, checkWrite: false };

View File

@ -0,0 +1,371 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetGetMany from './GoogleSheetGetMany';
const mockGetRows = jest.fn();
jest.mock('../getSheet', () => () => ({
getRows: mockGetRows,
}));
const { resolver, schema, checkRead, checkWrite } = GoogleSheetGetMany;
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: {},
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: '43',
birth_date: '2020/04/27',
married: 'FALSE',
_sheet: {},
},
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
_sheet: {},
},
{
_rowNumber: 5,
_rawData: ['4', 'Craig', '21', '2020-04-26T00:00:00.000Z', 'TRUE'],
id: '4',
name: 'Craig',
age: '120',
birth_date: '2020-04-26T00:00:00.000Z',
married: 'TRUE',
_sheet: {},
},
];
return Promise.resolve(rows.slice(offset).slice(undefined, limit));
};
test('googleSheetGetMany, all rows', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: {}, connection: {} });
expect(res).toEqual([
{
_rowNumber: 2,
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
id: '1',
name: 'John',
age: '34',
birth_date: '2020/04/26',
married: 'TRUE',
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: '43',
birth_date: '2020/04/27',
married: 'FALSE',
},
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
},
{
_rowNumber: 5,
_rawData: ['4', 'Craig', '21', '2020-04-26T00:00:00.000Z', 'TRUE'],
id: '4',
name: 'Craig',
age: '120',
birth_date: '2020-04-26T00:00:00.000Z',
married: 'TRUE',
},
]);
});
test('googleSheetGetMany, empty rows returned', async () => {
mockGetRows.mockImplementation(() => []);
const res = await resolver({ request: {}, connection: {} });
expect(res).toEqual([]);
});
test('googleSheetGetMany, limit', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { options: { limit: 2 } }, connection: {} });
expect(res).toEqual([
{
_rowNumber: 2,
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
id: '1',
name: 'John',
age: '34',
birth_date: '2020/04/26',
married: 'TRUE',
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: '43',
birth_date: '2020/04/27',
married: 'FALSE',
},
]);
});
test('googleSheetGetMany, skip', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { options: { skip: 2 } }, connection: {} });
expect(res).toEqual([
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
},
{
_rowNumber: 5,
_rawData: ['4', 'Craig', '21', '2020-04-26T00:00:00.000Z', 'TRUE'],
id: '4',
name: 'Craig',
age: '120',
birth_date: '2020-04-26T00:00:00.000Z',
married: 'TRUE',
},
]);
});
test('googleSheetGetMany, skip and limit', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { options: { skip: 2, limit: 1 } }, connection: {} });
expect(res).toEqual([
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
},
]);
});
test('googleSheetGetMany, filter', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { filter: { name: 'Tim' } }, connection: {} });
expect(res).toEqual([
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
},
]);
});
test('googleSheetGetMany, filter filters all', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { filter: { name: 'Nobody' } }, connection: {} });
expect(res).toEqual([]);
});
test('googleSheetGetMany, filter _rowNumber', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { filter: { _rowNumber: { $gt: 3 } } }, connection: {} });
expect(res).toEqual([
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
},
{
_rowNumber: 5,
_rawData: ['4', 'Craig', '21', '2020-04-26T00:00:00.000Z', 'TRUE'],
id: '4',
name: 'Craig',
age: '120',
birth_date: '2020-04-26T00:00:00.000Z',
married: 'TRUE',
},
]);
});
test('googleSheetGetMany, pipeline count', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: { pipeline: [{ $group: { _id: 0, count: { $sum: 1 } } }] },
connection: {},
});
expect(res).toEqual([
{
_id: 0,
count: 4,
},
]);
});
test('googleSheetGetMany, columnTypes', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {},
connection: {
columnTypes: {
name: 'string',
age: 'number',
married: 'boolean',
birth_date: 'date',
},
},
});
expect(res).toEqual([
{
_rowNumber: 2,
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
id: '1',
name: 'John',
age: 34,
birth_date: new Date('2020-04-26T00:00:00.000Z'),
married: true,
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: 43,
birth_date: new Date('2020-04-27T00:00:00.000Z'),
married: false,
},
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: 34,
birth_date: new Date('2020-04-28T00:00:00.000Z'),
married: false,
},
{
_rowNumber: 5,
_rawData: ['4', 'Craig', '21', '2020-04-26T00:00:00.000Z', 'TRUE'],
id: '4',
name: 'Craig',
age: 120,
birth_date: new Date('2020-04-26T00:00:00.000Z'),
married: true,
},
]);
});
test('valid request schema', () => {
const request = {};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request schema, all properties', () => {
const request = {
filter: { id: 1 },
pipeline: [{ $addFields: { a: 1 } }],
options: {
limit: 100,
skip: 300,
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetMany request properties should be an object.'
);
});
test('limit is not a number', () => {
const request = {
options: {
limit: true,
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetMany request property "options.limit" should be a number.'
);
});
test('skip is not a number', () => {
const request = {
options: {
skip: true,
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetMany request property "options.skip" should be a number.'
);
});
test('filter is not an object', () => {
const request = {
filter: true,
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetMany request property "filter" should be an object.'
);
});
test('pipeline is not an array', () => {
const request = {
pipeline: true,
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetMany request property "pipeline" should be an array.'
);
});
test('checkRead should be true', async () => {
expect(checkRead).toBe(true);
});
test('checkWrite should be false', async () => {
expect(checkWrite).toBe(false);
});

View File

@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - GoogleSheetGetMany",
"type": "object",
"properties": {
"filter": {
"type": "object",
"description": "A MongoDB query expression to filter the data.",
"errorMessage": {
"type": "GoogleSheetGetMany request property \"filter\" should be an object."
}
},
"pipeline": {
"type": "array",
"description": "A MongoDB aggregation pipeline to transform the data.",
"errorMessage": {
"type": "GoogleSheetGetMany request property \"pipeline\" should be an array."
}
},
"options": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "The maximum number of rows to fetch.",
"errorMessage": {
"type": "GoogleSheetGetMany 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": "GoogleSheetGetMany request property \"options.skip\" should be a number."
}
}
}
}
},
"errorMessage": {
"type": "GoogleSheetGetMany request properties should be an object."
}
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2020-2021 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 './GoogleSheetGetOneSchema.json';
import cleanRows from '../cleanRows';
import getSheet from '../getSheet';
import { transformRead } from '../transformTypes';
import mingoFilter from '../mingoFilter';
async function googleSheetGetOne({ request, connection }) {
const { filter, options = {} } = request;
const { limit, skip } = options;
const sheet = await getSheet({ connection });
let rows = await sheet.getRows({ limit, offset: skip });
rows = cleanRows(rows);
rows = transformRead({ input: rows, types: connection.columnTypes });
if (filter) {
rows = mingoFilter({ input: rows, filter });
}
return rows[0] || null;
}
export default { resolver: googleSheetGetOne, schema, checkRead: true, checkWrite: false };

View File

@ -0,0 +1,277 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetGetOne from './GoogleSheetGetOne';
const mockGetRows = jest.fn();
jest.mock('../getSheet', () => () => ({
getRows: mockGetRows,
}));
const { resolver, schema, checkRead, checkWrite } = GoogleSheetGetOne;
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: {},
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: '43',
birth_date: '2020/04/27',
married: 'FALSE',
_sheet: {},
},
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
_sheet: {},
},
{
_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: {},
},
];
return Promise.resolve(rows.slice(offset).slice(undefined, limit));
};
test('googleSheetGetOne, first row is returned', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: {}, connection: {} });
expect(res).toEqual({
_rowNumber: 2,
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
id: '1',
name: 'John',
age: '34',
birth_date: '2020/04/26',
married: 'TRUE',
});
});
test('googleSheetGetOne, empty rows returned', async () => {
mockGetRows.mockImplementation(() => []);
const res = await resolver({ request: {}, connection: {} });
expect(res).toEqual(null);
});
test('googleSheetGetOne, limit', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { options: { limit: 2 } }, connection: {} });
expect(res).toEqual({
_rowNumber: 2,
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
id: '1',
name: 'John',
age: '34',
birth_date: '2020/04/26',
married: 'TRUE',
});
});
test('googleSheetGetOne, skip', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { options: { skip: 2 } }, connection: {} });
expect(res).toEqual({
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
});
});
test('googleSheetGetOne, skip and limit', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { options: { skip: 2, limit: 1 } }, connection: {} });
expect(res).toEqual({
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
});
});
test('googleSheetGetOne, filter', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { filter: { name: 'Tim' } }, connection: {} });
expect(res).toEqual({
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
});
});
test('googleSheetGetOne, limit before filter', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: { filter: { name: 'Tim' }, options: { limit: 2 } },
connection: {},
});
expect(res).toEqual(null);
});
test('googleSheetGetOne, skip before filter', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: { filter: { married: 'TRUE' }, options: { skip: 2 } },
connection: {},
});
expect(res).toEqual({
_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',
});
});
test('googleSheetGetOne, filter filters all', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { filter: { name: 'Nobody' } }, connection: {} });
expect(res).toEqual(null);
});
test('googleSheetGetOne, filter _rowNumber', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({ request: { filter: { _rowNumber: { $gt: 3 } } }, connection: {} });
expect(res).toEqual({
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
});
});
test('googleSheetGetOne, columnTypes', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {},
connection: {
columnTypes: {
name: 'string',
age: 'number',
married: 'boolean',
birth_date: 'date',
},
},
});
expect(res).toEqual({
_rowNumber: 2,
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
id: '1',
name: 'John',
age: 34,
birth_date: new Date('2020-04-26T00:00:00.000Z'),
married: true,
});
});
test('valid request schema', () => {
const request = {};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request schema, all properties', () => {
const request = {
options: {
limit: 100,
skip: 300,
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetOne request properties should be an object.'
);
});
test('limit is not a number', () => {
const request = {
options: {
limit: true,
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetOne request property "options.limit" should be a number.'
);
});
test('skip is not a number', () => {
const request = {
options: {
skip: true,
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetOne request property "options.skip" should be a number.'
);
});
test('filter is not an object', () => {
const request = {
filter: true,
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetGetOne request property "filter" should be an object.'
);
});
test('checkRead should be true', async () => {
expect(checkRead).toBe(true);
});
test('checkWrite should be false', async () => {
expect(checkWrite).toBe(false);
});

View File

@ -0,0 +1,36 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - GoogleSheetGetOne",
"type": "object",
"properties": {
"filter": {
"type": "object",
"description": "A MongoDB query expression to filter the data.",
"errorMessage": {
"type": "GoogleSheetGetOne request property \"filter\" should be an object."
}
},
"options": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "The maximum number of rows to fetch.",
"errorMessage": {
"type": "GoogleSheetGetOne 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": "GoogleSheetGetOne request property \"options.skip\" should be a number."
}
}
}
}
},
"errorMessage": {
"type": "GoogleSheetGetOne request properties should be an object."
}
}

View File

@ -0,0 +1,88 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Connection Schema - GoogleSheet",
"type": "object",
"required": ["spreadsheetId"],
"properties": {
"apiKey": {
"type": "string",
"description": "API key for your google project.",
"errorMessage": {
"type": "GoogleSheet connection property \"apiKey\" should be a string."
}
},
"client_email": {
"type": "string",
"description": "The email of your service account.",
"errorMessage": {
"type": "GoogleSheet connection property \"client_email\" should be a string."
}
},
"private_key": {
"type": "string",
"description": "The private key for your service account.",
"errorMessage": {
"type": "GoogleSheet connection property \"private_key\" should be a string."
}
},
"sheetId": {
"type": "string",
"description": "The ID of the worksheet. Can be found in the URL as the \"gid\" parameter. One of \"sheetId\" or \"sheetIndex\" is required.",
"errorMessage": {
"type": "GoogleSheet connection property \"sheetId\" should be a string."
}
},
"sheetIndex": {
"type": "number",
"description": "The position of the worksheet as they appear in the Google sheets UI. Starts from 0. One of \"sheetId\" or \"sheetIndex\" is required.",
"errorMessage": {
"type": "GoogleSheet connection property \"sheetIndex\" should be a number."
}
},
"spreadsheetId": {
"type": "string",
"description": "document ID from the URL of the spreadsheet.",
"errorMessage": {
"type": "GoogleSheet connection property \"spreadsheetId\" should be a string."
}
},
"columnTypes": {
"type": "object",
"description": "Define types for columns in the spreadsheet.",
"patternProperties": {
"^.*$": {
"type": "string",
"enum": ["string", "number", "boolean", "date", "json"],
"errorMessage": {
"enum": "GoogleSheet connection property \"{{ dataPath }}\" should be one of \"string\", \"number\", \"boolean\", \"date\", or \"json\"."
}
}
},
"errorMessage": {
"type": "GoogleSheet connection property \"columnTypes\" should be an object."
}
},
"read": {
"type": "boolean",
"default": true,
"description": "Allow reads from the spreadsheet.",
"errorMessage": {
"type": "GoogleSheet connection property \"read\" should be a boolean."
}
},
"write": {
"type": "boolean",
"default": false,
"description": "Allow writes to the spreadsheet.",
"errorMessage": {
"type": "GoogleSheet connection property \"write\" should be a boolean."
}
}
},
"errorMessage": {
"type": "GoogleSheet connection properties should be an object.",
"required": {
"spreadsheetId": "GoogleSheet connection should have required property \"spreadsheetId\"."
}
}
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2020-2021 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 './GoogleSheetUpdateManySchema.json';
import getSheet from '../getSheet';
import { transformRead, transformWrite } from '../transformTypes';
import mingoFilter from '../mingoFilter';
async function googleSheetUpdateMany({ request, connection }) {
const { filter, update, options = {} } = request;
const { limit, skip, raw } = options;
const sheet = await getSheet({ connection });
let rows = await sheet.getRows({ limit, offset: skip });
rows = transformRead({ input: rows, types: connection.columnTypes });
rows = mingoFilter({ input: rows, filter });
const transformedUpdate = transformWrite({ input: update, types: connection.columnTypes });
if (rows.length === 0) {
return {
modifiedCount: 0,
};
}
const promises = rows.map(async (row) => {
Object.assign(row, transformedUpdate);
await row.save({ raw });
});
await Promise.all(promises);
return {
modifiedCount: rows.length,
};
}
export default { resolver: googleSheetUpdateMany, schema, checkRead: false, checkWrite: true };

View File

@ -0,0 +1,258 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetUpdateMany from './GoogleSheetUpdateMany';
const mockGetRows = jest.fn();
const mockSave = jest.fn();
jest.mock('../getSheet', () => () => ({
getRows: mockGetRows,
}));
const { resolver, schema, checkRead, checkWrite } = GoogleSheetUpdateMany;
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: {},
save: mockSave,
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: '43',
birth_date: '2020/04/27',
married: 'FALSE',
_sheet: {},
save: mockSave,
},
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
_sheet: {},
save: mockSave,
},
{
_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: {},
save: mockSave,
},
];
return Promise.resolve(rows.slice(offset).slice(undefined, limit));
};
test('googleSheetUpdateMany, match one', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { id: '1' },
update: {
name: 'New',
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 1,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetUpdateMany, match one, raw true', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { id: '1' },
update: {
name: 'New',
},
options: {
raw: true,
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 1,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"raw": true,
},
],
]
`);
});
test('googleSheetUpdateMany, match nothing', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { id: 'does_not_exist' },
update: {
name: 'New',
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 0,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`Array []`);
});
test('googleSheetUpdateMany, match more than one', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { _rowNumber: { $gt: 3 } },
update: {
name: 'New',
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 2,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"raw": undefined,
},
],
Array [
Object {
"raw": undefined,
},
],
]
`);
});
test('valid request schema', () => {
const request = {
filter: { id: '1' },
update: {
name: 'New',
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateMany request properties should be an object.'
);
});
test('filter is not an object', () => {
const request = {
filter: 'filter',
update: {
name: 'New',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateMany request property "filter" should be an object.'
);
});
test('update is not an object', () => {
const request = {
filter: { id: '1' },
update: 'update',
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateMany request property "update" should be an object.'
);
});
test('filter is missing', () => {
const request = {
update: {
name: 'New',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateMany request should have required property "filter".'
);
});
test('update is missing', () => {
const request = {
filter: { id: '1' },
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateMany request should have required property "update".'
);
});
test('options.raw is not a boolean', () => {
const request = {
filter: 'filter',
update: {
name: 'New',
},
options: {
raw: 'raw',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateMany request property "options.raw" should be a boolean.'
);
});
test('checkRead should be false', async () => {
expect(checkRead).toBe(false);
});
test('checkWrite should be true', async () => {
expect(checkWrite).toBe(true);
});

View File

@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - GoogleSheetUpdateMany",
"type": "object",
"required": ["update", "filter"],
"properties": {
"filter": {
"type": "object",
"description": "A MongoDB query expression to filter the data. All rows matched by the filter will be updated.",
"errorMessage": {
"type": "GoogleSheetUpdateMany request property \"filter\" should be an object."
}
},
"update": {
"type": "object",
"description": "The update to apply to the row. An object where keys are the column names and values are the updated values.",
"errorMessage": {
"type": "GoogleSheetUpdateMany request property \"update\" should be an object."
}
},
"options": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "The maximum number of rows to fetch.",
"errorMessage": {
"type": "GoogleSheetUpdateMany request property \"options.limit\" should be a number."
}
},
"raw": {
"type": "boolean",
"description": "Store raw values instead of converting as if typed into the sheets UI.",
"errorMessage": {
"type": "GoogleSheetUpdateMany request property \"options.raw\" should be a boolean."
}
},
"skip": {
"type": "number",
"description": "The number of rows to skip from the top of the sheet.",
"errorMessage": {
"type": "GoogleSheetUpdateMany request property \"options.skip\" should be a number."
}
}
}
}
},
"errorMessage": {
"type": "GoogleSheetUpdateMany request properties should be an object.",
"required": {
"filter": "GoogleSheetUpdateMany request should have required property \"filter\".",
"update": "GoogleSheetUpdateMany request should have required property \"update\"."
}
}
}

View File

@ -0,0 +1,55 @@
/*
Copyright 2020-2021 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 './GoogleSheetUpdateOneSchema.json';
import cleanRows from '../cleanRows';
import getSheet from '../getSheet';
import { transformRead, transformWrite } from '../transformTypes';
import mingoFilter from '../mingoFilter';
async function googleSheetUpdateOne({ request, connection }) {
const { filter, update, options = {} } = request;
const { limit, skip, upsert, raw } = options;
const sheet = await getSheet({ connection });
let rows = await sheet.getRows({ limit, offset: skip });
rows = transformRead({ input: rows, types: connection.columnTypes });
rows = mingoFilter({ input: rows, filter });
const transformedUpdate = transformWrite({ input: update, types: connection.columnTypes });
if (rows.length === 0) {
if (upsert) {
const insertedRow = await sheet.addRow(transformedUpdate, { raw });
return {
modifiedCount: 1,
upserted: true,
row: cleanRows(insertedRow),
};
}
return {
modifiedCount: 0,
upserted: false,
};
}
const row = rows[0];
Object.assign(row, transformedUpdate);
await row.save({ raw });
return {
modifiedCount: 1,
upserted: false,
row: cleanRows(row),
};
}
export default { resolver: googleSheetUpdateOne, schema, checkRead: false, checkWrite: true };

View File

@ -0,0 +1,327 @@
/*
Copyright 2020-2021 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 { validate } from '@lowdefy/ajv';
import GoogleSheetUpdateOne from './GoogleSheetUpdateOne';
const mockGetRows = jest.fn();
const mockAddRow = jest.fn();
const mockSave = jest.fn();
jest.mock('../getSheet', () => () => ({
addRow: mockAddRow,
getRows: mockGetRows,
}));
const { resolver, schema, checkRead, checkWrite } = GoogleSheetUpdateOne;
const mockAddRowDefaultImp = (row) => ({ ...row, _sheet: {} });
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: {},
save: mockSave,
},
{
_rowNumber: 3,
_rawData: ['2', 'Steven', '43', '2020/04/27', 'FALSE'],
id: '2',
name: 'Steve',
age: '43',
birth_date: '2020/04/27',
married: 'FALSE',
_sheet: {},
save: mockSave,
},
{
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'Tim',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
_sheet: {},
save: mockSave,
},
{
_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: {},
save: mockSave,
},
];
return Promise.resolve(rows.slice(offset).slice(undefined, limit));
};
test('googleSheetUpdateOne, match one', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { id: '1' },
update: {
name: 'New',
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 1,
row: {
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
_rowNumber: 2,
age: '34',
birth_date: '2020/04/26',
id: '1',
married: 'TRUE',
name: 'New',
save: mockSave,
},
upserted: false,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetUpdateOne, match one, raw true', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { id: '1' },
update: {
name: 'New',
},
options: {
raw: true,
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 1,
row: {
_rawData: ['1', 'John', '34', '2020/04/26', 'TRUE'],
_rowNumber: 2,
age: '34',
birth_date: '2020/04/26',
id: '1',
married: 'TRUE',
name: 'New',
save: mockSave,
},
upserted: false,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"raw": true,
},
],
]
`);
});
test('googleSheetUpdateOne, match nothing', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { id: 'does_not_exist' },
update: {
name: 'New',
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 0,
upserted: false,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`Array []`);
});
test('googleSheetUpdateOne, match nothing, upsert true', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
mockAddRow.mockImplementation(mockAddRowDefaultImp);
const res = await resolver({
request: {
filter: { id: 'does_not_exist' },
update: {
name: 'New',
},
options: {
upsert: true,
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 1,
row: {
name: 'New',
},
upserted: true,
});
expect(mockAddRow.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"name": "New",
},
Object {
"raw": undefined,
},
],
]
`);
});
test('googleSheetUpdateOne, match more than one', async () => {
mockGetRows.mockImplementation(mockGetRowsDefaultImp);
const res = await resolver({
request: {
filter: { _rowNumber: { $gt: 3 } },
update: {
name: 'New',
},
},
connection: {},
});
expect(res).toEqual({
modifiedCount: 1,
row: {
_rowNumber: 4,
_rawData: ['3', 'Tim', '34', '2020/04/28', 'FALSE'],
id: '3',
name: 'New',
age: '34',
birth_date: '2020/04/28',
married: 'FALSE',
save: mockSave,
},
upserted: false,
});
expect(mockSave.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"raw": undefined,
},
],
]
`);
});
test('valid request schema', () => {
const request = {
filter: { id: '1' },
update: {
name: 'New',
},
};
expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('request properties is not an object', () => {
const request = 'request';
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateOne request properties should be an object.'
);
});
test('filter is not an object', () => {
const request = {
filter: 'filter',
update: {
name: 'New',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateOne request property "filter" should be an object.'
);
});
test('update is not an object', () => {
const request = {
filter: { id: '1' },
update: 'update',
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateOne request property "update" should be an object.'
);
});
test('filter is missing', () => {
const request = {
update: {
name: 'New',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateOne request should have required property "filter".'
);
});
test('update is missing', () => {
const request = {
filter: { id: '1' },
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateOne request should have required property "update".'
);
});
test('options.raw is not a boolean', () => {
const request = {
filter: 'filter',
update: {
name: 'New',
},
options: {
raw: 'raw',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'GoogleSheetUpdateOne request property "options.raw" should be a boolean.'
);
});
test('checkRead should be false', async () => {
expect(checkRead).toBe(false);
});
test('checkWrite should be true', async () => {
expect(checkWrite).toBe(true);
});

View File

@ -0,0 +1,62 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - GoogleSheetUpdateOne",
"type": "object",
"required": ["update", "filter"],
"properties": {
"filter": {
"type": "object",
"description": "A MongoDB query expression to filter the data. The first row matched by the filter will be updated.",
"errorMessage": {
"type": "GoogleSheetUpdateOne request property \"filter\" should be an object."
}
},
"update": {
"type": "object",
"description": "The update to apply to the row. An object where keys are the column names and values are the updated values.",
"errorMessage": {
"type": "GoogleSheetUpdateOne request property \"update\" should be an object."
}
},
"options": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "The maximum number of rows to fetch.",
"errorMessage": {
"type": "GoogleSheetUpdateOne request property \"options.limit\" should be a number."
}
},
"raw": {
"type": "boolean",
"description": "Store raw values instead of converting as if typed into the sheets UI.",
"errorMessage": {
"type": "GoogleSheetUpdateOne request property \"options.raw\" should be a boolean."
}
},
"skip": {
"type": "number",
"description": "The number of rows to skip from the top of the sheet.",
"errorMessage": {
"type": "GoogleSheetUpdateOne request property \"options.skip\" should be a number."
}
},
"upsert": {
"type": "boolean",
"description": "Insert the row if no rows are matched by the filter.",
"errorMessage": {
"type": "GoogleSheetUpdateOne request property \"options.upsert\" should be a boolean."
}
}
}
}
},
"errorMessage": {
"type": "GoogleSheetUpdateOne request properties should be an object.",
"required": {
"filter": "GoogleSheetUpdateOne request should have required property \"filter\".",
"update": "GoogleSheetUpdateOne request should have required property \"update\"."
}
}
}

View File

@ -0,0 +1,35 @@
/*
Copyright 2020-2021 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 { type } from '@lowdefy/helpers';
function cleanRow(row) {
// eslint-disable-next-line no-unused-vars
const { _sheet, ...rest } = row;
return { ...rest };
}
function cleanRows(input) {
if (type.isObject(input)) {
return cleanRow(input);
}
if (type.isArray(input)) {
return input.map((row) => cleanRow(row));
}
throw new Error(`cleanRows received invalid input type ${type.typeOf(input)}.`);
}
export default cleanRows;

View File

@ -0,0 +1,66 @@
/*
Copyright 2020-2021 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 cleanRows from './cleanRows';
test('cleanRows removes objects with key _sheet from an array of rows', () => {
expect(
cleanRows([
{
id: 1,
value: 'a',
_sheet: {
string: 'string',
},
},
{
id: 2,
value: 'b',
_sheet: {
string: 'string',
},
},
])
).toEqual([
{
id: 1,
value: 'a',
},
{
id: 2,
value: 'b',
},
]);
});
test('cleanRows removes objects with key _sheet from a row', () => {
expect(
cleanRows({
id: 1,
value: 'a',
_sheet: {
string: 'string',
},
})
).toEqual({
id: 1,
value: 'a',
});
});
test('cleanRows invalid input', () => {
expect(() => cleanRows(1)).toThrow('cleanRows received invalid input type number.');
});

View File

@ -0,0 +1,57 @@
/*
Copyright 2020-2021 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 { GoogleSpreadsheet } from 'google-spreadsheet';
async function authenticate({ doc, apiKey, client_email, private_key }) {
if (apiKey) {
doc.useApiKey(apiKey);
} else {
await doc.useServiceAccountAuth({
client_email: client_email,
private_key: private_key,
});
}
}
function getSheetFromDoc({ doc, sheetId, sheetIndex }) {
let sheet;
if (sheetId) {
sheet = doc.sheetsById[sheetId];
if (!sheet) {
throw new Error(`Could not find sheet with sheetId "${sheetId}"`);
}
} else {
sheet = doc.sheetsByIndex[sheetIndex];
if (!sheet) {
throw new Error(`Could not find sheet with sheetIndex ${sheetIndex}`);
}
}
return sheet;
}
async function getSheet({ connection }) {
const { apiKey, client_email, private_key, sheetId, sheetIndex, spreadsheetId } = connection;
const doc = new GoogleSpreadsheet(spreadsheetId);
await authenticate({ doc, apiKey, client_email, private_key });
await doc.loadInfo();
return getSheetFromDoc({ doc, sheetId, sheetIndex });
}
export default getSheet;

View File

@ -0,0 +1,239 @@
/*
Copyright 2020-2021 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.
*/
// eslint-disable-next-line no-unused-vars
import { GoogleSpreadsheet } from 'google-spreadsheet';
import getSheet from './getSheet';
// Not testing if spreadsheetId is given to GoogleSpreadsheet class in
// const doc = new GoogleSpreadsheet(spreadsheetId);
const mockSheetsById = {};
const mockSheetsByIndex = {};
const mockUseApiKey = jest.fn();
const mockUseServiceAccountAuth = jest.fn();
const mockLoadInfo = jest.fn();
jest.mock('google-spreadsheet', () => {
function GoogleSpreadsheet() {
return {
sheetsById: mockSheetsById,
sheetsByIndex: mockSheetsByIndex,
useApiKey: mockUseApiKey,
useServiceAccountAuth: mockUseServiceAccountAuth,
loadInfo: mockLoadInfo,
};
}
return {
GoogleSpreadsheet,
};
});
async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const useApiKeyDefaultImp = (apiKey) => {
if (apiKey !== 'valid') {
throw new Error('Test Api Key Auth Error.');
}
};
const useServiceAccountAuthDefaultImp = async ({ client_email, private_key }) => {
await wait(3);
if (client_email !== 'client_email' || private_key !== 'private_key') {
throw new Error('Test Service Account Auth Error.');
}
};
const loadInfoDefaultImp = async () => {
await wait(3);
mockSheetsById.sheetId1 = { id: 'sheetId1' };
mockSheetsByIndex[0] = { index: 0 };
};
beforeEach(() => {
jest.resetAllMocks();
Object.keys(mockSheetsById).forEach((key) => {
delete mockSheetsById[key];
});
Object.keys(mockSheetsByIndex).forEach((key) => {
delete mockSheetsByIndex[key];
});
});
test('getSheet with apiKey, sheetId', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
const sheet = await getSheet({
connection: {
apiKey: 'valid',
spreadsheetId: 'spreadsheetId',
sheetId: 'sheetId1',
},
});
expect(mockUseApiKey.mock.calls).toEqual([['valid']]);
expect(mockUseServiceAccountAuth.mock.calls).toEqual([]);
expect(mockLoadInfo.mock.calls).toEqual([[]]);
expect(sheet).toEqual({ id: 'sheetId1' });
});
test('getSheet with service account, sheetId', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
const sheet = await getSheet({
connection: {
client_email: 'client_email',
private_key: 'private_key',
spreadsheetId: 'spreadsheetId',
sheetId: 'sheetId1',
},
});
expect(mockUseApiKey.mock.calls).toEqual([]);
expect(mockUseServiceAccountAuth.mock.calls).toEqual([
[
{
client_email: 'client_email',
private_key: 'private_key',
},
],
]);
expect(mockLoadInfo.mock.calls).toEqual([[]]);
expect(sheet).toEqual({ id: 'sheetId1' });
});
test('getSheet with service account, sheetIndex', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
const sheet = await getSheet({
connection: {
client_email: 'client_email',
private_key: 'private_key',
spreadsheetId: 'spreadsheetId',
sheetIndex: 0,
},
});
expect(mockUseApiKey.mock.calls).toEqual([]);
expect(mockUseServiceAccountAuth.mock.calls).toEqual([
[
{
client_email: 'client_email',
private_key: 'private_key',
},
],
]);
expect(mockLoadInfo.mock.calls).toEqual([[]]);
expect(sheet).toEqual({ index: 0 });
});
test('getSheet with invalid apiKey', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
await expect(() =>
getSheet({
connection: {
apiKey: 'invalid',
spreadsheetId: 'spreadsheetId',
sheetId: 'sheetId1',
},
})
).rejects.toThrow('Test Api Key Auth Error.');
expect(mockUseApiKey.mock.calls).toEqual([['invalid']]);
});
test('getSheet with invalid client_id', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
await expect(() =>
getSheet({
connection: {
client_email: 'invalid_client_email',
private_key: 'private_key',
spreadsheetId: 'spreadsheetId',
sheetId: 'sheetId1',
},
})
).rejects.toThrow('Test Service Account Auth Error.');
expect(mockUseServiceAccountAuth.mock.calls).toEqual([
[
{
client_email: 'invalid_client_email',
private_key: 'private_key',
},
],
]);
});
test('getSheet with invalid private_key', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
await expect(() =>
getSheet({
connection: {
client_email: 'client_email',
private_key: 'invalid_private_key',
spreadsheetId: 'spreadsheetId',
sheetId: 'sheetId1',
},
})
).rejects.toThrow('Test Service Account Auth Error.');
expect(mockUseServiceAccountAuth.mock.calls).toEqual([
[
{
client_email: 'client_email',
private_key: 'invalid_private_key',
},
],
]);
});
test('getSheet with sheetId, sheet does not exist', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
await expect(
getSheet({
connection: {
apiKey: 'valid',
spreadsheetId: 'spreadsheetId',
sheetId: 'sheetId2',
},
})
).rejects.toThrow('Could not find sheet with sheetId "sheetId2"');
});
test('getSheet with sheetIndex, sheet does not exist', async () => {
mockUseApiKey.mockImplementation(useApiKeyDefaultImp);
mockUseServiceAccountAuth.mockImplementation(useServiceAccountAuthDefaultImp);
mockLoadInfo.mockImplementation(loadInfoDefaultImp);
await expect(
getSheet({
connection: {
apiKey: 'valid',
spreadsheetId: 'spreadsheetId',
sheetIndex: 1,
},
})
).rejects.toThrow('Could not find sheet with sheetIndex 1');
});

View File

@ -0,0 +1,43 @@
/*
Copyright 2020-2021 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 { type } from '@lowdefy/helpers';
import mingo from 'mingo';
import { useOperators, OperatorType } from 'mingo/core';
import * as accumulatorOperators from 'mingo/operators/accumulator';
import * as expressionOperators from 'mingo/operators/expression';
import * as pipelineOperators from 'mingo/operators/pipeline';
import * as queryOperators from 'mingo/operators/query';
import * as projectionOperators from 'mingo/operators/projection';
useOperators(OperatorType.ACCUMULATOR, accumulatorOperators);
useOperators(OperatorType.EXPRESSION, expressionOperators);
useOperators(OperatorType.PIPELINE, pipelineOperators);
useOperators(OperatorType.PROJECTION, queryOperators);
useOperators(OperatorType.QUERY, projectionOperators);
function mingoAggregation({ input = [], pipeline = [] }) {
if (!type.isArray(input)) {
throw new Error('Mingo aggregation error. Argument "input" should be an array.');
}
if (!type.isArray(pipeline)) {
throw new Error('Mingo aggregation error. Argument "pipeline" should be an array.');
}
const aggregator = new mingo.Aggregator(pipeline);
return aggregator.run(input);
}
export default mingoAggregation;

View File

@ -0,0 +1,165 @@
/*
Copyright 2020-2021 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 mingoAggregation from './mingoAggregation';
test('mingoAggregation sort', () => {
const pipeline = [
{
$sort: {
id: 1,
},
},
];
const input = [
{
id: 2,
},
{
id: 1,
},
];
const res = mingoAggregation({ input, pipeline });
expect(res).toEqual([
{
id: 1,
},
{
id: 2,
},
]);
});
test('mingoAggregation group', () => {
const pipeline = [
{
$group: {
_id: 0,
count: { $sum: 1 },
},
},
];
const input = [
{
id: 2,
},
{
id: 1,
},
];
const res = mingoAggregation({ input, pipeline });
expect(res).toEqual([
{
_id: 0,
count: 2,
},
]);
});
test('mingoAggregation empty pipeline', () => {
const pipeline = [];
const input = [
{
id: 2,
},
{
id: 1,
},
];
const res = mingoAggregation({ input, pipeline });
expect(res).toEqual([
{
id: 2,
},
{
id: 1,
},
]);
});
test('mingoAggregation undefined pipeline', () => {
const input = [
{
id: 2,
},
{
id: 1,
},
];
const res = mingoAggregation({ input });
expect(res).toEqual([
{
id: 2,
},
{
id: 1,
},
]);
});
test('mingoAggregation empty input', () => {
const pipeline = [
{
$sort: {
id: 1,
},
},
];
const input = [];
const res = mingoAggregation({ input, pipeline });
expect(res).toEqual([]);
});
test('mingoAggregation undefined input', () => {
const pipeline = [
{
$sort: {
id: 1,
},
},
];
const res = mingoAggregation({ pipeline });
expect(res).toEqual([]);
});
test('mingoAggregation pipeline is not an array', () => {
const pipeline = 'pipeline';
const input = [
{
id: 2,
},
{
id: 1,
},
];
expect(() => mingoAggregation({ input, pipeline })).toThrow(
'Mingo aggregation error. Argument "pipeline" should be an array.'
);
});
test('mingoAggregation input is not an array', () => {
const pipeline = [
{
$sort: {
id: 1,
},
},
];
const input = 'input';
expect(() => mingoAggregation({ input, pipeline })).toThrow(
'Mingo aggregation error. Argument "input" should be an array.'
);
});

View File

@ -0,0 +1,31 @@
/*
Copyright 2020-2021 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 { type } from '@lowdefy/helpers';
import mingoAggregation from './mingoAggregation';
function mingoFilter({ input = [], filter = {} }) {
if (!type.isObject(filter)) {
throw new Error('Mingo filter error. Argument "filter" should be an object.');
}
if (!type.isArray(input)) {
throw new Error('Mingo filter error. Argument "input" should be an array.');
}
const pipeline = [{ $match: filter }];
return mingoAggregation({ input, pipeline });
}
export default mingoFilter;

View File

@ -0,0 +1,207 @@
/*
Copyright 2020-2021 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 mingoFilter from './mingoFilter';
test('mingoFilter equals shorthand', () => {
const filter = { id: 1 };
const input = [
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
];
const res = mingoFilter({ input, filter });
expect(res).toEqual([
{
id: 1,
x: 'x',
},
]);
});
test('mingoFilter greater than', () => {
const filter = { id: { $gt: 1 } };
const input = [
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
];
const res = mingoFilter({ input, filter });
expect(res).toEqual([
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
]);
});
test('mingoFilter $in', () => {
const filter = { id: { $in: [1, 3] } };
const input = [
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
];
const res = mingoFilter({ input, filter });
expect(res).toEqual([
{
id: 1,
x: 'x',
},
{
id: 3,
x: 'x',
},
]);
});
test('mingoFilter filter empty object', () => {
const filter = {};
const input = [
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
];
const res = mingoFilter({ input, filter });
expect(res).toEqual([
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
]);
});
test('mingoFilter filter undefined', () => {
const input = [
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
];
const res = mingoFilter({ input });
expect(res).toEqual([
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
]);
});
test('mingoFilter input empty array', () => {
const filter = { id: 1 };
const input = [];
const res = mingoFilter({ input, filter });
expect(res).toEqual([]);
});
test('mingoFilter input undefined', () => {
const filter = { id: 1 };
const res = mingoFilter({ filter });
expect(res).toEqual([]);
});
test('mingoFilter filter not an object', () => {
const filter = 'filter';
const input = [
{
id: 1,
x: 'x',
},
{
id: 2,
x: 'x',
},
{
id: 3,
x: 'x',
},
];
expect(() => mingoFilter({ input, filter })).toThrow(
'Mingo filter error. Argument "filter" should be an object.'
);
});
test('mingoFilter input not an array', () => {
const filter = { id: 1 };
const input = 'input';
expect(() => mingoFilter({ input, filter })).toThrow(
'Mingo filter error. Argument "input" should be an array.'
);
});

View File

@ -0,0 +1,677 @@
/*
Copyright 2020-2021 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 { transformRead } from './transformTypes';
test('transformRead invalid input', () => {
expect(() => transformRead({ input: 1 })).toThrow(
'transformRead received invalid input type number.'
);
});
test('transformRead works on an object', () => {
expect(
transformRead({
input: {
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: '1',
booleanTransform: 'TRUE',
dateTransform: '2020/01/26',
jsonTransform: '{"key":"value"}',
},
types: {
stringTransform: 'string',
numberTransform: 'number',
booleanTransform: 'boolean',
dateTransform: 'date',
jsonTransform: 'json',
},
})
).toEqual({
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: 1,
booleanTransform: true,
dateTransform: new Date('2020-01-26T00:00:00.000Z'),
jsonTransform: { key: 'value' },
});
});
test('transformRead works on an object, no types provided', () => {
expect(
transformRead({
input: {
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
},
})
).toEqual({
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
});
});
test('transformRead works on an array', () => {
expect(
transformRead({
input: [
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: '1',
booleanTransform: 'TRUE',
dateTransform: '2020/01/26',
jsonTransform: '{"key":"value"}',
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
stringTransform: 'string2',
numberTransform: '2',
booleanTransform: 'FALSE',
dateTransform: '2020/01/27',
jsonTransform: '{"key":"value2"}',
},
],
types: {
stringTransform: 'string',
numberTransform: 'number',
booleanTransform: 'boolean',
dateTransform: 'date',
jsonTransform: 'json',
},
})
).toEqual([
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: 1,
booleanTransform: true,
dateTransform: new Date('2020-01-26T00:00:00.000Z'),
jsonTransform: { key: 'value' },
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
stringTransform: 'string2',
numberTransform: 2,
booleanTransform: false,
dateTransform: new Date('2020-01-27T00:00:00.000Z'),
jsonTransform: { key: 'value2' },
},
]);
});
test('transformRead works on an array, no types provided', () => {
expect(
transformRead({
input: [
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
},
],
})
).toEqual([
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
},
]);
});
test('transformRead numbers', () => {
expect(
transformRead({
input: [
{
numberTransform: '1',
original: '1',
},
{
numberTransform: '2.0',
original: '2.0',
},
{
numberTransform: '3.141592',
original: '3.141592',
},
{
numberTransform: '4.2asdfg',
original: '4.2asdfg',
},
{
numberTransform: NaN,
original: NaN,
},
{
numberTransform: null,
original: null,
},
{
numberTransform: 'Hello',
original: 'Hello',
},
{
numberTransform: {},
original: {},
},
{
numberTransform: [],
original: [],
},
],
types: {
numberTransform: 'number',
},
})
).toEqual([
{
numberTransform: 1,
original: '1',
},
{
numberTransform: 2,
original: '2.0',
},
{
numberTransform: 3.141592,
original: '3.141592',
},
{
numberTransform: null,
original: '4.2asdfg',
},
{
numberTransform: null,
original: NaN,
},
{
numberTransform: 0,
original: null,
},
{
numberTransform: null,
original: 'Hello',
},
{
numberTransform: null,
original: {},
},
{
numberTransform: 0,
original: [],
},
]);
});
test('transformRead booleans', () => {
expect(
transformRead({
input: [
{
booleanTransform: 'TRUE',
original: 'TRUE',
},
{
booleanTransform: 'FALSE',
original: 'FALSE',
},
{
booleanTransform: 'False',
original: 'False',
},
{
booleanTransform: 'True',
original: 'True',
},
{
booleanTransform: 'true',
original: 'true',
},
{
booleanTransform: 'false',
original: 'false',
},
{
booleanTransform: true,
original: true,
},
{
booleanTransform: false,
original: false,
},
{
booleanTransform: 1,
original: 1,
},
{
booleanTransform: 0,
original: 0,
},
{
booleanTransform: 42,
original: 42,
},
{
booleanTransform: 'Hello',
original: 'Hello',
},
{
booleanTransform: null,
original: null,
},
{
booleanTransform: {},
original: {},
},
{
booleanTransform: [],
original: [],
},
],
types: {
booleanTransform: 'boolean',
},
})
).toEqual([
{
booleanTransform: true,
original: 'TRUE',
},
{
booleanTransform: false,
original: 'FALSE',
},
{
booleanTransform: false,
original: 'False',
},
{
booleanTransform: false,
original: 'True',
},
{
booleanTransform: false,
original: 'true',
},
{
booleanTransform: false,
original: 'false',
},
{
booleanTransform: false,
original: true,
},
{
booleanTransform: false,
original: false,
},
{
booleanTransform: false,
original: 1,
},
{
booleanTransform: false,
original: 0,
},
{
booleanTransform: false,
original: 42,
},
{
booleanTransform: false,
original: 'Hello',
},
{
booleanTransform: false,
original: null,
},
{
booleanTransform: false,
original: {},
},
{
booleanTransform: false,
original: [],
},
]);
});
test('transformRead dates', () => {
expect(
transformRead({
input: [
{
dateTransform: '2020/01/27',
original: '2020/01/27',
},
{
dateTransform: '2020-01-27T00:00:00.000Z',
original: '2020-01-27T00:00:00.000Z',
},
{
dateTransform: 1,
original: 1,
},
{
dateTransform: '1',
original: '1',
},
{
dateTransform: '2.0',
original: '2.0',
},
{
dateTransform: '3.141592',
original: '3.141592',
},
{
dateTransform: NaN,
original: NaN,
},
{
dateTransform: null,
original: null,
},
{
dateTransform: 'Hello',
original: 'Hello',
},
],
types: {
dateTransform: 'date',
},
})
).toEqual([
{
dateTransform: new Date('2020-01-27T00:00:00.000Z'),
original: '2020/01/27',
},
{
dateTransform: new Date('2020-01-27T00:00:00.000Z'),
original: '2020-01-27T00:00:00.000Z',
},
{
dateTransform: new Date('1970-01-01T00:00:00.001Z'),
original: 1,
},
{
dateTransform: new Date('2001-01-01T00:00:00.000Z'), // This is weird
original: '1',
},
{
dateTransform: null,
original: '2.0',
},
{
dateTransform: null,
original: '3.141592',
},
{
dateTransform: null,
original: NaN,
},
{
dateTransform: null,
original: null,
},
{
dateTransform: null,
original: 'Hello',
},
]);
});
test('transformRead json', () => {
expect(
transformRead({
input: [
{
jsonTransform: '1',
original: '1',
},
{
jsonTransform: '2.0',
original: '2.0',
},
{
jsonTransform: '3.141592',
original: '3.141592',
},
{
jsonTransform: 'Hello',
original: 'Hello',
},
{
jsonTransform: '"Hello"',
original: '"Hello"',
},
{
jsonTransform: 'null',
original: 'null',
},
{
jsonTransform: 'true',
original: 'true',
},
{
jsonTransform: 'false',
original: 'false',
},
{
jsonTransform: '{"key":"value"}',
original: '{"key":"value"}',
},
{
jsonTransform: '[1,2,3]',
original: '[1,2,3]',
},
{
jsonTransform: '{}',
original: '{}',
},
{
jsonTransform: '[]',
original: '[]',
},
{
jsonTransform: '[1,2,3',
original: '[1,2,3',
},
{
jsonTransform: '"Hell',
original: '"Hell',
},
{
jsonTransform: 'undefined',
original: 'undefined',
},
{
jsonTransform: NaN,
original: NaN,
},
{
jsonTransform: null,
original: null,
},
{
jsonTransform: true,
original: true,
},
{
jsonTransform: false,
original: false,
},
{
jsonTransform: 42,
original: 42,
},
{
jsonTransform: {},
original: {},
},
{
jsonTransform: [],
original: [],
},
],
types: {
jsonTransform: 'json',
},
})
).toEqual([
{
jsonTransform: 1,
original: '1',
},
{
jsonTransform: 2,
original: '2.0',
},
{
jsonTransform: 3.141592,
original: '3.141592',
},
{
jsonTransform: null,
original: 'Hello',
},
{
jsonTransform: 'Hello',
original: '"Hello"',
},
{
jsonTransform: null,
original: 'null',
},
{
jsonTransform: true,
original: 'true',
},
{
jsonTransform: false,
original: 'false',
},
{
jsonTransform: {
key: 'value',
},
original: '{"key":"value"}',
},
{
jsonTransform: [1, 2, 3],
original: '[1,2,3]',
},
{
jsonTransform: {},
original: '{}',
},
{
jsonTransform: [],
original: '[]',
},
{
jsonTransform: null,
original: '[1,2,3',
},
{
jsonTransform: null,
original: '"Hell',
},
{
jsonTransform: null,
original: 'undefined',
},
{
jsonTransform: null,
original: NaN,
},
{
jsonTransform: null,
original: null,
},
{
jsonTransform: true,
original: true,
},
{
jsonTransform: false,
original: false,
},
{
jsonTransform: 42,
original: 42,
},
{
jsonTransform: null,
original: {},
},
{
jsonTransform: null,
original: [],
},
]);
});

View File

@ -0,0 +1,91 @@
/*
Copyright 2020-2021 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 { type } from '@lowdefy/helpers';
import moment from 'moment';
const readTransformers = {
string: (value) => value,
number: (value) => {
const number = Number(value);
if (isNaN(number)) return null;
return number;
},
boolean: (value) => value === 'TRUE',
date: (value) => {
const date = moment.utc(value);
if (!date.isValid()) return null;
return date.toDate();
},
json: (value) => {
try {
return JSON.parse(value);
} catch (_) {
return null;
}
},
};
const writeTransformers = {
string: (value) => value,
number: (value) => (type.isNumber(value) ? value.toString() : value),
boolean: (value) => {
if (value === true) return 'TRUE';
if (value === false) return 'FALSE';
return value;
},
date: (value) => (type.isDate(value) ? value.toISOString() : value),
json: (value) => {
try {
return JSON.stringify(value);
} catch (_) {
return value;
}
},
};
const transformObject =
({ transformers, types }) =>
(object) => {
Object.keys(object).forEach((key) => {
if (types[key]) {
object[key] = transformers[types[key]](object[key]);
}
});
return object;
};
function transformRead({ input, types = {} }) {
if (type.isObject(input)) {
return transformObject({ transformers: readTransformers, types })(input);
}
if (type.isArray(input)) {
return input.map((obj) => transformObject({ transformers: readTransformers, types })(obj));
}
throw new Error(`transformRead received invalid input type ${type.typeOf(input)}.`);
}
function transformWrite({ input, types = {} }) {
if (type.isObject(input)) {
return transformObject({ transformers: writeTransformers, types })(input);
}
if (type.isArray(input)) {
return input.map((obj) => transformObject({ transformers: writeTransformers, types })(obj));
}
throw new Error(`transformWrite received invalid input type ${type.typeOf(input)}.`);
}
export { transformRead, transformWrite };

View File

@ -0,0 +1,654 @@
/*
Copyright 2020-2021 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 { transformWrite } from './transformTypes';
test('transformWrite invalid input', () => {
expect(() => transformWrite({ input: 1 })).toThrow(
'transformWrite received invalid input type number.'
);
});
test('transformWrite works on an object', () => {
expect(
transformWrite({
input: {
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: 1,
booleanTransform: true,
dateTransform: new Date('2020-01-26T00:00:00.000Z'),
jsonTransform: { key: 'value' },
},
types: {
stringTransform: 'string',
numberTransform: 'number',
booleanTransform: 'boolean',
dateTransform: 'date',
jsonTransform: 'json',
},
})
).toEqual({
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: '1',
booleanTransform: 'TRUE',
dateTransform: '2020-01-26T00:00:00.000Z',
jsonTransform: '{"key":"value"}',
});
});
test('transformWrite works on an object, no types provided', () => {
expect(
transformWrite({
input: {
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
},
})
).toEqual({
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
});
});
test('transformWrite works on an array', () => {
expect(
transformWrite({
input: [
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: 1,
booleanTransform: true,
dateTransform: new Date('2020-01-26T00:00:00.000Z'),
jsonTransform: { key: 'value' },
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
stringTransform: 'string2',
numberTransform: 2,
booleanTransform: false,
dateTransform: new Date('2020-01-27T00:00:00.000Z'),
jsonTransform: { key: 'value2' },
},
],
types: {
stringTransform: 'string',
numberTransform: 'number',
booleanTransform: 'boolean',
dateTransform: 'date',
jsonTransform: 'json',
},
})
).toEqual([
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
stringTransform: 'string',
numberTransform: '1',
booleanTransform: 'TRUE',
dateTransform: '2020-01-26T00:00:00.000Z',
jsonTransform: '{"key":"value"}',
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
stringTransform: 'string2',
numberTransform: '2',
booleanTransform: 'FALSE',
dateTransform: '2020-01-27T00:00:00.000Z',
jsonTransform: '{"key":"value2"}',
},
]);
});
test('transformWrite works on an array, no types provided', () => {
expect(
transformWrite({
input: [
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
},
],
})
).toEqual([
{
string: 'string',
number: '1',
boolean: 'TRUE',
date: '2020/01/26',
json: '{"key":"value"}',
},
{
string: 'string2',
number: '2',
boolean: 'FALSE',
date: '2020/01/27',
json: '{"key":"value2"}',
},
]);
});
test('transformWrite numbers', () => {
expect(
transformWrite({
input: [
{
numberTransform: 1,
original: 1,
},
{
numberTransform: 2.0,
original: 2.0,
},
{
numberTransform: 3.141592,
original: 3.141592,
},
{
numberTransform: '1',
original: '1',
},
{
numberTransform: '2.0',
original: '2.0',
},
{
numberTransform: '3.141592',
original: '3.141592',
},
{
numberTransform: '4.2asdfg',
original: '4.2asdfg',
},
{
numberTransform: NaN,
original: NaN,
},
{
numberTransform: null,
original: null,
},
{
numberTransform: 'Hello',
original: 'Hello',
},
{
numberTransform: {},
original: {},
},
{
numberTransform: [],
original: [],
},
],
types: {
numberTransform: 'number',
},
})
).toEqual([
{
numberTransform: '1',
original: 1,
},
{
numberTransform: '2',
original: 2.0,
},
{
numberTransform: '3.141592',
original: 3.141592,
},
{
numberTransform: '1',
original: '1',
},
{
numberTransform: '2.0',
original: '2.0',
},
{
numberTransform: '3.141592',
original: '3.141592',
},
{
numberTransform: '4.2asdfg',
original: '4.2asdfg',
},
{
numberTransform: NaN,
original: NaN,
},
{
numberTransform: null,
original: null,
},
{
numberTransform: 'Hello',
original: 'Hello',
},
{
numberTransform: {},
original: {},
},
{
numberTransform: [],
original: [],
},
]);
});
test('transformWrite booleans', () => {
expect(
transformWrite({
input: [
{
booleanTransform: true,
original: true,
},
{
booleanTransform: false,
original: false,
},
{
booleanTransform: 'TRUE',
original: 'TRUE',
},
{
booleanTransform: 'FALSE',
original: 'FALSE',
},
{
booleanTransform: 'False',
original: 'False',
},
{
booleanTransform: 'True',
original: 'True',
},
{
booleanTransform: 'true',
original: 'true',
},
{
booleanTransform: 'false',
original: 'false',
},
{
booleanTransform: 1,
original: 1,
},
{
booleanTransform: 0,
original: 0,
},
{
booleanTransform: 42,
original: 42,
},
{
booleanTransform: 'Hello',
original: 'Hello',
},
{
booleanTransform: null,
original: null,
},
{
booleanTransform: {},
original: {},
},
{
booleanTransform: [],
original: [],
},
],
types: {
booleanTransform: 'boolean',
},
})
).toEqual([
{
booleanTransform: 'TRUE',
original: true,
},
{
booleanTransform: 'FALSE',
original: false,
},
{
booleanTransform: 'TRUE',
original: 'TRUE',
},
{
booleanTransform: 'FALSE',
original: 'FALSE',
},
{
booleanTransform: 'False',
original: 'False',
},
{
booleanTransform: 'True',
original: 'True',
},
{
booleanTransform: 'true',
original: 'true',
},
{
booleanTransform: 'false',
original: 'false',
},
{
booleanTransform: 1,
original: 1,
},
{
booleanTransform: 0,
original: 0,
},
{
booleanTransform: 42,
original: 42,
},
{
booleanTransform: 'Hello',
original: 'Hello',
},
{
booleanTransform: null,
original: null,
},
{
booleanTransform: {},
original: {},
},
{
booleanTransform: [],
original: [],
},
]);
});
test('transformWrite dates', () => {
expect(
transformWrite({
input: [
{
dateTransform: new Date('2020-01-27T00:00:00.000Z'),
original: new Date('2020-01-27T00:00:00.000Z'),
},
{
dateTransform: '2020/01/27',
original: '2020/01/27',
},
{
dateTransform: '2020-01-27T00:00:00.000Z',
original: '2020-01-27T00:00:00.000Z',
},
{
dateTransform: 1,
original: 1,
},
{
dateTransform: '1',
original: '1',
},
{
dateTransform: '2.0',
original: '2.0',
},
{
dateTransform: '3.141592',
original: '3.141592',
},
{
dateTransform: NaN,
original: NaN,
},
{
dateTransform: null,
original: null,
},
{
dateTransform: 'Hello',
original: 'Hello',
},
],
types: {
dateTransform: 'date',
},
})
).toEqual([
{
dateTransform: '2020-01-27T00:00:00.000Z',
original: new Date('2020-01-27T00:00:00.000Z'),
},
{
dateTransform: '2020/01/27',
original: '2020/01/27',
},
{
dateTransform: '2020-01-27T00:00:00.000Z',
original: '2020-01-27T00:00:00.000Z',
},
{
dateTransform: 1,
original: 1,
},
{
dateTransform: '1',
original: '1',
},
{
dateTransform: '2.0',
original: '2.0',
},
{
dateTransform: '3.141592',
original: '3.141592',
},
{
dateTransform: NaN,
original: NaN,
},
{
dateTransform: null,
original: null,
},
{
dateTransform: 'Hello',
original: 'Hello',
},
]);
});
test('transformWrite json', () => {
const circular = {};
circular.reference = circular;
expect(
transformWrite({
input: [
{
jsonTransform: 1,
original: 1,
},
{
jsonTransform: 2,
original: 2.0,
},
{
jsonTransform: 3.141592,
original: 3.141592,
},
{
jsonTransform: 'Hello',
original: 'Hello',
},
{
jsonTransform: null,
original: null,
},
{
jsonTransform: true,
original: true,
},
{
jsonTransform: false,
original: false,
},
{
jsonTransform: {
key: 'value',
},
original: {
key: 'value',
},
},
{
jsonTransform: [1, 2, 3],
original: [1, 2, 3],
},
{
jsonTransform: {},
original: {},
},
{
jsonTransform: [],
original: [],
},
{
jsonTransform: undefined,
original: undefined,
},
{
jsonTransform: NaN,
original: NaN,
},
{
// JSON stringify throws on circular references
jsonTransform: circular,
original: circular,
},
],
types: {
jsonTransform: 'json',
},
})
).toEqual([
{
jsonTransform: '1',
original: 1,
},
{
jsonTransform: '2',
original: 2.0,
},
{
jsonTransform: '3.141592',
original: 3.141592,
},
{
jsonTransform: '"Hello"',
original: 'Hello',
},
{
jsonTransform: 'null',
original: null,
},
{
jsonTransform: 'true',
original: true,
},
{
jsonTransform: 'false',
original: false,
},
{
jsonTransform: '{"key":"value"}',
original: {
key: 'value',
},
},
{
jsonTransform: '[1,2,3]',
original: [1, 2, 3],
},
{
jsonTransform: '{}',
original: {},
},
{
jsonTransform: '[]',
original: [],
},
{
jsonTransform: undefined,
original: undefined,
},
{
jsonTransform: 'null',
original: NaN,
},
{
jsonTransform: circular,
original: circular,
},
]);
});

View File

@ -0,0 +1,7 @@
import GoogleSheet from './GoogleSheet/GoogleSheet';
export const connections = {
GoogleSheet,
};
export default { connections };

View File

@ -3602,6 +3602,25 @@ __metadata:
languageName: unknown
linkType: soft
"@lowdefy/connection-google-sheets@workspace:packages/connections/google-sheets":
version: 0.0.0-use.local
resolution: "@lowdefy/connection-google-sheets@workspace:packages/connections/google-sheets"
dependencies:
"@babel/cli": 7.15.7
"@babel/core": 7.15.8
"@babel/preset-env": 7.15.8
"@lowdefy/ajv": 3.22.0
"@lowdefy/helpers": 3.22.0
babel-jest: 27.3.1
google-spreadsheet: 3.1.15
jest: 26.6.3
mingo: 4.2.0
moment: 2.29.1
peerDependencies:
"@lowdefy/api": 3.22.0
languageName: unknown
linkType: soft
"@lowdefy/connection-knex@workspace:packages/connections/knex":
version: 0.0.0-use.local
resolution: "@lowdefy/connection-knex@workspace:packages/connections/knex"