Skip to content

Commit

Permalink
feat: implement a parser for private google sheets
Browse files Browse the repository at this point in the history
  • Loading branch information
Gumball12 committed Mar 2, 2024
1 parent 358f039 commit 0d61c13
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 68 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
coverage
private-key.json

# Logs
logs
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ This library automatically generates TypeScript types (`*.d.ts`) by parsing Goog

## 💫 Features

- [Parser presets](./src/parser) for public and private Google Sheets
- Generate types(`*.d.ts`) for Google Sheets at the desired location
- [Parser presets](./src/parser) and Generate types(`*.d.ts`) for **public and private Google Sheets**
- Customize the type and type file name

## 📦 Install
Expand Down
79 changes: 62 additions & 17 deletions src/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import { describe, it, expect } from 'vitest';
import { publicGoogleSheetsParser } from '../parser';

import { JWT } from 'google-auth-library';
import { GoogleSpreadsheet } from 'google-spreadsheet';
import PublicGoogleSheetsParser from 'public-google-sheets-parser';

import { publicGoogleSheetsParser, googleSpreadsheet } from '../parser';

const expected = {
click_conversation_data: {
conversation_id: 'string',
created_at: 'Date',
agent_type: 'string',
status: 'StatusEnum',
generate_position: "conversation' | 'playground'",
},
click_message_feedback_button: {
conversation_id: 'string',
message_id: 'string',
generate_position: '"conversation" | "playground"',
my_test: "string | 'string'",
},
};

// https://docs.google.com/spreadsheets/d/1j23zhzHcPd_LzDQ7uPrXgMJfPoZYs289boUKoKnAjUo/edit#gid=0
const SHEET_NAME = 'ParserTest';
const SPREADSHEET_ID = '1j23zhzHcPd_LzDQ7uPrXgMJfPoZYs289boUKoKnAjUo';
Expand All @@ -14,22 +34,6 @@ describe('publicGoogleSheetsParser', () => {
},
);

const expected = {
click_conversation_data: {
conversation_id: 'string',
created_at: 'Date',
agent_type: 'string',
status: 'StatusEnum',
generate_position: "conversation' | 'playground'",
},
click_message_feedback_button: {
conversation_id: 'string',
message_id: 'string',
generate_position: '"conversation" | "playground"',
my_test: "string | 'string'",
},
};

it('Common forms', async () => {
const parsed = await publicGoogleSheetsParser(
publicGoogleSheetsParserInstance,
Expand All @@ -54,3 +58,44 @@ describe('publicGoogleSheetsParser', () => {
expect(parsed).toEqual(expected);
});
});

const PRIVATE_SHEETS_SCOPES = [
'https://www.googleapis.com/auth/spreadsheets.readonly',
];

/**
* # How to test
* (Do not upload the private-key.json file to a public repository!)
*
* 1. Create a private-key.json file.
* This can be created using https://medium.com/@sakkeerhussainp/google-sheet-as-your-database-for-node-js-backend-a79fc5a6edd9 as a guide.
* 2. The contents of the sheet should be the same as https://docs.google.com/spreadsheets/d/1j23zhzHcPd_LzDQ7uPrXgMJfPoZYs289boUKoKnAjUo/edit#gid=0.
* If you want to use different data, edit the `expected` variable.
* 3. Change the `describe.skip` method to the `describe`. (`describe.skip(...)` -> `describe(...)`)
* 4. Run the test with the `pnpm test` command.
*/
describe.skip('GoogleSpreadsheet', () => {
it('Common forms', async () => {
// Do not upload the private-key.json file to a public repository when testing!
// @ts-expect-error private-key
const privateKey = await import('./private-key.json');

const jwt = new JWT({
email: privateKey.client_email,
key: privateKey.private_key,
scopes: PRIVATE_SHEETS_SCOPES,
});

const doc = new GoogleSpreadsheet(SPREADSHEET_ID, jwt);
await doc.loadInfo();

const sheetInstance = doc.sheetsByIndex[0];

const parsed = await googleSpreadsheet(sheetInstance, {
path: ['Key', 'Property'],
typeName: 'Type',
})();

expect(parsed).toEqual(expected);
});
});
59 changes: 56 additions & 3 deletions src/parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ import { publicGoogleSheetsParser } from 'google-spreadsheet-dts/parser';

import PublicGoogleSheetsParser from 'public-google-sheets-parser';

// Define parser
const parser = publicGoogleSheetsParser(
new PublicGoogleSheetsParser(/* ... */),
{
publicGoogleSheetsParserInstance,
path: ['Key', 'Property'],
typeName: 'Type',
},
);

// Generate d.ts file
generateDts({
name: 'GoogleSheets',
directory: resolve(__dirname, '../src'),
Expand Down Expand Up @@ -73,9 +74,61 @@ If `path` is `['Key', 'Property']` and `typeName` is `Type`, the return will loo
}
```

## google-spreadsheet-parser
## google-spreadsheet (node-google-spreadsheet)

WIP
A parser that parses **private** Google Sheets. This parser uses [google-spreadsheet](https://github.com/theoephraim/node-google-spreadsheet).

### Usage

```ts
import { generateDts } from 'google-spreadsheet-dts';
import { googleSpreadsheet } from 'google-spreadsheet-dts/parser';

import { JWT } from 'google-auth-library';
import { GoogleSpreadsheet } from 'google-spreadsheet';

// Load private Google Sheets data
const jwt = new JWT(/* ... */);
const doc = new GoogleSpreadsheet(/* ... */);
await doc.loadInfo();

const sheet = doc.sheetsByIndex[0];

// Define parser
const parser = googleSpreadsheet(sheet, {
path: ['Key', 'Property'],
typeName: 'Type',
});

// Generate d.ts file
generateDts({
name: 'GoogleSheets',
directory: resolve(__dirname, '../src'),
parser,
});
```

Note that you need to pass the [sheet instance](https://theoephraim.github.io/node-google-spreadsheet/#/classes/google-spreadsheet-worksheet).

### API

#### `googleSpreadsheet`

```ts
function googleSpreadsheet(
sheetInstance: GoogleSpreadsheetWorksheet,
params: {
path: string[];
typeName: string;
},
): Parser;
```

- `sheetInstance`: An instance of [`GoogleSpreadsheetWorksheet`](https://theoephraim.github.io/node-google-spreadsheet/#/classes/google-spreadsheet-worksheet)
- `path`: List of column names where object property names exists
- `typeName`: Column name where the type name exists

See [publicGoogleSheetsParser API](#public-google-sheets-parser) for a detailed description of the `path` and `typeName` behavior.

## Writing a custom parser

Expand Down
40 changes: 27 additions & 13 deletions src/parser/googleSpreadSheet.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import { JWT } from 'google-auth-library';
import { GoogleSpreadsheet } from 'google-spreadsheet';
import { type GoogleSpreadsheetWorksheet } from 'google-spreadsheet';
import { filledDataToObject } from './utils/filledDataToObject';
import { FilledData } from './types/data';

export const googleSpreadsheetParser = async (
spreadsheetId: string,
serviceAccountAuth: JWT,
) => {
const doc = new GoogleSpreadsheet(spreadsheetId, serviceAccountAuth);
await doc.loadInfo();
type GoogleSpreadsheetParams = {
path: string[];
typeName: string;
};

const sheet = doc.sheetsByIndex[0];
await sheet.loadHeaderRow();
export const googleSpreadsheet =
(
sheetInstance: GoogleSpreadsheetWorksheet,
{ path, typeName }: GoogleSpreadsheetParams,
) =>
async (): Promise<object> => {
const rows = await sheetInstance.getRows();
const filledData = rows.reduce<FilledData>((filledData, row, index) => {
const prevData = filledData[index - 1];
const data = [...path, typeName].reduce<FilledData[number]>((acc, p) => {
const item = row.get(p) || prevData[p];
acc[p] = item;
return acc;
}, {});

const rows = await sheet.getRows();
console.log(rows, sheet.headerValues);
};
filledData.push(data);
return filledData;
}, []);

return filledDataToObject(filledData, path, typeName);
};
1 change: 1 addition & 0 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { publicGoogleSheetsParser } from './publicGoogleSheetsParser';
export { googleSpreadsheet } from './googleSpreadsheet';

Check failure on line 2 in src/parser/index.ts

View workflow job for this annotation

GitHub Actions / ci

Cannot find module './googleSpreadsheet' or its corresponding type declarations.
48 changes: 15 additions & 33 deletions src/parser/publicGoogleSheetsParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// https://github.com/fureweb-com/public-google-sheets-parser

import type PublicGoogleSheetsParser from 'public-google-sheets-parser';
import { FilledData } from './types/data';
import { filledDataToObject } from './utils/filledDataToObject';

type PublicGoogleSheetsParserParams = {
path: string[];
Expand All @@ -14,42 +16,22 @@ export const publicGoogleSheetsParser =
{ path, typeName }: PublicGoogleSheetsParserParams,
) =>
async (): Promise<object> => {
const data = await publicGoogleSheetsParserInstance.parse();

const filledData = data.map((item, index, data) => {
if (index === 0) {
return item;
}

for (const p of path) {
if (!item[p]) {
item[p] = data[index - 1][p];
const parsedData = await publicGoogleSheetsParserInstance.parse();
const filledData = parsedData.map<FilledData[number]>(
(item, index, data) => {
if (index === 0) {
return item;
}
}

return item;
});

const result = filledData.reduce((acc, item) => {
let current = acc;

for (let i = 0; i < path.length - 1; i++) {
const p = path[i];
const name = item[p];

if (!current[name]) {
current[name] = {};
for (const p of path) {
if (!item[p]) {
item[p] = data[index - 1][p];
}
}

current = current[name];
}

const last = path[path.length - 1];
const name = item[last];
current[name] = item[typeName];

return acc;
}, {} as unknown);
return item;
},
);

return result;
return filledDataToObject(filledData, path, typeName);
};
1 change: 1 addition & 0 deletions src/parser/types/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type FilledData = Record<string, string>[];
31 changes: 31 additions & 0 deletions src/parser/utils/filledDataToObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FilledData } from '../types/data';

type Result = {
[key: string]: string | Result;
};

export const filledDataToObject = (
filledData: FilledData,
path: string[],
typeName: string,
): Result =>
filledData.reduce<Result>((filledData, item) => {
let current = filledData;
for (let i = 0; i < path.length - 1; i++) {
const p = path[i];
const name = item[p];

if (!current[name]) {
current[name] = {};
}

// @ts-expect-error TS7053
current = current[name];
}

const last = path[path.length - 1];
const name = item[last];
current[name] = item[typeName];

return filledData;
}, {});

0 comments on commit 0d61c13

Please sign in to comment.