diff --git a/CHANGELOG.md b/CHANGELOG.md index d267cb2..5c79a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,3 @@ # Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -### 1.0.1 (2024-05-20) - -### 1.0.6 (2024-05-20) - -### 1.0.5 (2024-05-19) - -### 1.0.4 (2024-05-19) - -### 1.0.3 (2024-05-18) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9c1d8eb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2024] [Teles] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 721c3b1..dcf4f91 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,471 @@ -# HolySheets +# HolySheets! -![Logo](logo.svg) +![Logo](docs/logo.svg) -HolySheets is a TypeScript library that simplifies the process of interacting with Google Sheets API. It provides a set of tools to read and write data from and to Google Sheets. +`HolySheets!` is a TypeScript library that simplifies the process of interacting with the Google Sheets API. It offers a set of tools for reading and writing data to and from Google Sheets, with a Prisma-like syntax. + +## Advantages + +- **No more memorizing range codes**: With `HolySheets!`, you don't need to remember complex range codes. The library handles all the range-related operations for you, allowing you to focus on your data. ## Features -- Easy to use API for interacting with Google Sheets. +- Easy-to-use API for interacting with Google Sheets. - Supports reading and writing data. -- Supports authentication with Google Sheets API. +- Supports authentication with the Google Sheets API. +- TypeScript support: Enhances development with static typing and intellisense. ## Installation -You can install HolySheets using npm: +You can install `HolySheets!` using npm: ```bash npm install holysheets ``` -## Usage +## Usage + +To use `HolySheets!` in your TypeScript project, you need to import it and initialize it with your Google Sheets credentials. Here's an example: + +```typescript + interface User { + name: string + email: string + age: number + } + + const hollySheets = new HolySheets({ + spreadsheetId: 'spreadsheet-id', + privateKey: credentials.private_key, // Your credentials + clientEmail: credentials.client_email // Your client email + }) + + const user = holySheets.base('Users') + + await user.findMany({ + where: { + name: { + contains: 'Joe' + } + } + }) + /// Find all users named Joe in Users sheet +``` + +:warning: Before using HolySheets, it's important to have Google credentials for your project. For more information on how to obtain these, please refer to the [Getting Credentials](docs/getting-credentials.md) guide. + + +## API documentation + +### base + +The `base` method is used to set the sheet that is going to be used. + +```typescript +const baseConfig = base('Users'); +``` + +You can also use your own type definition to have access to typescript static typing checking, for instance: + +```typescript +interface User { + name: string + email: string + age: number +} +const baseConfig = base('Users'); +/// Now you should only be able to add where clauses based on User interface keys +``` + +### findFirst + +Retrieves the first row that matches the given filter criteria. + +#### Parameters + +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. + +#### Example + +```typescript +const user = await userSheet.findFirst({ + where: { + email: 'john.doe@example.com' + } +}); +console.log(user); +// Output: { range: 'Users!A2:D2', row: 2, fields: { id: 1, name: 'John Doe', email: 'john.doe@example.com', points: 1200 } } +``` + + +### findMany + +Retrieves all rows that match the given filter criteria. + +#### Parameters + +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. + +#### Example + +```typescript +const users = await userSheet.findMany({ + where: { + points: { + greaterThan: 1000 + } + } +}); +console.log(users); +// Output: [ +// { range: 'Users!A2:D2', row: 2, fields: { id: 1, name: 'John Doe', points: 1050 } }, +// { range: 'Users!A3:D3', row: 3, fields: { id: 2, name: 'Jane Smith', points: 1100 } } +// ] +``` + +### updateFirst + +Updates the first row that matches the given filter criteria with the specified new data. + +#### Parameters + +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. +- `data`: An object specifying the new data to update the matching row with. + +#### Example + +Before update: +```typescript +// Assuming the sheet has the following data: +// | id | name | points | +// |----|---------|--------| +// | 1 | John | 950 | +// | 2 | Jane | 1050 | +// | 3 | Maria | 1100 | + +const updatedUser = await userSheet.updateFirst( + { where: { name: { contains: 'Jane' } } }, + { data: { points: 1150 } } +); +console.log(updatedUser); +// Output: { range: 'Users!A3:D3', row: 3, fields: { id: 2, name: 'Jane', points: 1150 } } +``` + +// The sheet now has the following data: +// | id | name | points | +// |----|---------|--------| +// | 1 | John | 950 | +// | 2 | Jane | 1150 | +// | 3 | Maria | 1100 | + +### updateMany +Updates all rows that match the given filter criteria with the specified new data. + +#### Parameters +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. +- `data`: An object specifying the new data to update the matching rows with. + +#### Example + +```typescript +// Assuming the sheet has the following data: +// | id | name | points | +// |----|---------|--------| +// | 1 | John | 950 | +// | 2 | Jane | 1050 | +// | 3 | Maria | 1100 | + +const updatedUsers = await userSheet.updateMany( + { where: { points: { greaterThan: 1000 } } }, + { data: { points: 1200 } } +); +console.log(updatedUsers); +// Output: [ +// { range: 'Users!A2:D2', row: 2, fields: { id: 2, name: 'Jane', points: 1200 } }, +// { range: 'Users!A3:D3', row: 3, fields: { id: 3, name: 'Maria', points: 1200 } } +// ] + +// The sheet now has the following data: +// | id | name | points | +// |----|---------|--------| +// | 1 | John | 950 | +// | 2 | Jane | 1200 | +// | 3 | Maria | 1200 | +``` + + +### clearFirst + +Clears the data in the first row that matches the given filter criteria. + +#### Parameters +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. + +#### Example + +```typescript +const clearedUser = await userSheet.clearFirst({ + where: { + name: 'John Doe' + } +}); +console.log(clearedUser); +// Output: { range: 'Users!A2:D2', row: 2, fields: { id: 1, name: '', email: '', points: '' } } +``` + +### clearMany + +Clears the data in all rows that match the given filter criteria. + +#### Parameters + +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. + +```typescript +const clearedUsers = await userSheet.clearMany({ + where: { + points: { + lessThan: 1000 + } + } +}); +console.log(clearedUsers); +// Output: [ +// { range: 'Users!A2:D2', row: 2, fields: { id: 1, name: '', points: '' } } +// ] + +// The sheet now has the following data: +// | id | name | points | +// |----|---------|--------| +// | 1 | | | +// | 2 | Jane | 1050 | +// | 3 | Maria | 1100 | +``` + +### deleteFirst + +Deletes the first row that matches the given filter criteria. + +#### Parameters + +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. + +#### Examples + +```typescript +const deletedUser = await userSheet.deleteFirst({ + where: { + email: 'john.doe@example.com' + } +}); +console.log(deletedUser); +// Output: { range: 'Users!A2:D2', row: 2, fields: { id: 1, name: 'John Doe', email: 'john.doe@example.com', points: 1200 } } +``` + +### deleteMany + +Deletes all rows that match the given filter criteria. + +#### Parameters + +- `filter`: An object specifying the filter criteria. Each key corresponds to a column name and each value specifies the condition to be met. + +#### Example + +```typescript +const deletedUsers = await userSheet.deleteMany({ + where: { + points: { + lessThan: 1000 + } + } +}); +console.log(deletedUsers); +// Output: [ +// { range: 'Users!A2:D2', row: 2, fields: { id: 1, name: 'John', points: 950 } } +// ] + +// The sheet now has the following data: +// | id | name | points | +// |----|---------|--------| +// | 2 | Jane | 1050 | +// | 3 | Maria | 1100 | +``` + +### Filter conditions + +`HolySheets!` provides a variety of filter conditions to help you query data from your Google Sheets. These filters allow you to specify criteria for selecting rows based on the values in their columns. + +#### equals + +Checks if the value in the column equals the specified value. + +```typescript +const user = await userSheet.findFirst({ + where: { + email: { + equals: 'john.doe@example.com' + } + } +}); +``` + +#### not + +Checks if the value in the column does not equal the specified value. + +```typescript +const users = await userSheet.findMany({ + where: { + email: { + not: 'john.doe@example.com' + } + } +}); +``` + +#### in + +Checks if the value in the column is included in the specified array. + +```typescript +const users = await userSheet.findMany({ + where: { + role: { + in: ['admin', 'editor'] + } + } +}); +``` + +#### notIn + +Checks if the value in the column is not included in the specified array. + +```typescript +const users = await userSheet.findMany({ + where: { + role: { + notIn: ['guest', 'banned'] + } + } +}); +``` + +#### lt + +Checks if the numeric value in the column is less than the specified value. + +```typescript +const users = await userSheet.findMany({ + where: { + age: { + lt: 30 + } + } +}); +``` + +#### lte + +Checks if the numeric value in the column is less than or equal to the specified value. + +```typescript +const users = await userSheet.findMany({ + where: { + age: { + lte: 30 + } + } +}); +``` +#### gt + +Checks if the numeric value in the column is greater than the specified value. + +```typescript +const users = await userSheet.findMany({ + where: { + points: { + gt: 1000 + } + } +}); +``` + +#### gte + +Checks if the numeric value in the column is greater than or equal to the specified value. + +```typescript +const users = await userSheet.findMany({ + where: { + points: gte(1000) + } +}); +``` + +#### contains + +Checks if the string value in the column contains the specified substring. + +```typescript +const users = await userSheet.findMany({ + where: { + name:{ + contains: 'Doe' + } + } +}); +``` + +#### search + +Performs a case-insensitive search to check if the string value in the column contains the specified substring. + +```typescript +const users = await userSheet.findMany({ + where: { + name: { + search: 'doe' + } + } +}); +``` + +#### startsWith + +Checks if the string value in the column starts with the specified substring. + +```typescript +const users = await userSheet.findMany({ + where: { + name: { + startsWith: 'John' + } + } +}); +``` + +#### endsWith + +Checks if the string value in the column ends with the specified substring. + +```typescript +const users = await userSheet.findMany({ + where: { + email: { + endsWith: '@example.com' + } + } +}); +``` -To use `HolySheets` in your TypeScript project, you need to import it and initialize it with your Google Sheets credentials. Here's an example: +## License -## Limitations +`HolySheets!` is licensed under the MIT License. For more details, see the [LICENSE](LICENSE) file in the project repository. -While HolySheets provides a convenient way to interact with Google Sheets, it may not be suitable for all projects. Specifically, it may not be the best choice for: +## Note -- Projects with large amounts of data: Google Sheets has requests limits and performance can degrade with very large sheets. If your project involves handling large datasets, a dedicated database system may be more appropriate. +While `HolySheets!` provides a simplified interface for managing Google Sheets data, it is not intended to replace a dedicated database system. Please consider the specific needs and requirements of your project when deciding whether to use `HolySheets!`. -- Projects that require advanced database features: Google Sheets is a spreadsheet tool and lacks many features of dedicated database systems. For example, it does not support table constraints like a SQL database would. If your project requires these features, consider using a dedicated database system. \ No newline at end of file diff --git a/docs/getting-credentials.md b/docs/getting-credentials.md new file mode 100644 index 0000000..205e73f --- /dev/null +++ b/docs/getting-credentials.md @@ -0,0 +1,24 @@ +# Getting Google Credentials + +Follow these steps to create a project in Google, export the JSON credentials for usage, create a spreadsheet, and add the Google project user as an editor of the spreadsheet: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and sign in with your Google account. +2. Click on the project drop-down and select "New Project". +3. Enter a name for your project and click on the "Create" button. +4. Once the project is created, click on the project drop-down again and select your newly created project. +5. In the left navigation menu, click on "APIs & Services" and then select "Credentials". +6. Click on the "Create Credentials" button and choose "Service Account". +7. Enter a name for your service account, select the role as "Project" > "Editor", and click on the "Continue" button. +8. On the next screen, click on the "Create Key" button and select "JSON" as the key type. This will download the JSON credentials file to your computer. +9. Store the downloaded JSON credentials file in a secure location. + + :warning: **Important**: Never commit your Google JSON credentials to your version control system. These are sensitive files that should be kept out of your repository to prevent unauthorized access. + +10. Now, go to [Google Sheets](https://sheets.google.com/) and sign in with your Google account. +11. Click on the "Blank" template to create a new spreadsheet. +12. Give your spreadsheet a name and click on the "Create" button. +13. In the top-right corner, click on the "Share" button. +14. In the "People" field, enter the email address of the Google project user (found in the JSON credentials file) and select the role as "Editor". +15. Click on the "Send" button to share the spreadsheet with the Google project user. + +You have now created a project in Google, exported the JSON credentials, created a spreadsheet, and added the Google project user as an editor of the spreadsheet. \ No newline at end of file diff --git a/logo.svg b/docs/logo.svg similarity index 100% rename from logo.svg rename to docs/logo.svg diff --git a/package.json b/package.json index 6831068..241ea27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "holy-sheets", - "version": "1.0.1", + "name": "holysheets", + "version": "1.0.0", "description": "A Node.js library for interacting with Google Sheets", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/sample.ts b/sample.ts index e0dc943..71f5e40 100644 --- a/sample.ts +++ b/sample.ts @@ -1,28 +1,25 @@ -import HollySheets from './src/index' -import credentials from './.credentials.json' +// import HollySheets from './src/index' +// import credentials from './.credentials.json' -const hollySheets = new HollySheets({ - spreadsheetId: '10EQjKl96GqZFrSSoJKHdJFpAwRfvCzaQY3-u85IBpEA', - privateKey: credentials.private_key, - clientEmail: credentials.client_email -}) +// const hollySheets = new HollySheets({ +// spreadsheetId: '10EQjKl96GqZFrSSoJKHdJFpAwRfvCzaQY3-u85IBpEA', +// privateKey: credentials.private_key, +// clientEmail: credentials.client_email +// }) -const main = async () => { - interface User { - name: string - email: string - age: number - } +// const main = async () => { +// interface User { +// name: string +// email: string +// age: number +// } - const user = hollySheets.base('Users') - const joses = await user.findMany({ - where: { - name: { - contains: 'Jose' - } - } - }) - console.log(JSON.stringify(joses, null, 4)) // eslint-disable-line -} +// const user = hollySheets.base('Users') +// const users = await user.updateFirst({ +// where: { name: 'John' }, +// data: { age: 25 } +// }) +// console.log(users) +// } -void main() +// void main() diff --git a/src/index.ts b/src/index.ts index f1c5da6..845f752 100644 --- a/src/index.ts +++ b/src/index.ts @@ -168,7 +168,7 @@ type SelectClause = Partial<{[column in keyof RecordType]: boolean}> export default class HolySheets = any> { public sheets: sheets_v4.Sheets public sheet: string = '' - public static spreadsheetId: string = '' + public spreadsheetId: string = '' private readonly credentials: HolySheetsCredentials /** @@ -183,7 +183,7 @@ export default class HolySheets = any> { key: credentials.privateKey, scopes: ['https://www.googleapis.com/auth/spreadsheets'] }) - HolySheets.spreadsheetId = credentials.spreadsheetId + this.spreadsheetId = credentials.spreadsheetId this.sheets = google.sheets({ version: 'v4', auth }) } @@ -214,7 +214,7 @@ export default class HolySheets = any> { const { data } = options const sheet = this.sheet const response = await this.sheets.spreadsheets.values.get({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, range: `${sheet}!${alphabet[0]}:${alphabet[alphabet.length - 1]}` }) @@ -222,14 +222,14 @@ export default class HolySheets = any> { throw new Error('No data found in the sheet.') } const lastLine = response.data.values.length - const headers = await getHeaders({ sheet: sheet, sheets: this.sheets, spreadsheetId: HolySheets.spreadsheetId}) + const headers = await getHeaders({ sheet: sheet, sheets: this.sheets, spreadsheetId: this.spreadsheetId}) const valuesFromRecords = data.map(record => decombine(record, headers)) const range = `A${lastLine + 1}:${indexToColumn(headers.length - 1)}${lastLine + valuesFromRecords.length}` await write({ tableName: sheet, range, values: valuesFromRecords, - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, sheets: this.sheets }) } @@ -246,13 +246,13 @@ export default class HolySheets = any> { public async findFirst(options: { where: WhereClause, select?: SelectClause }): Promise|undefined>{ const { where } = options const sheet = this.sheet - const headers = await getHeaders({ sheet, sheets: this.sheets, spreadsheetId: HolySheets.spreadsheetId}) + const headers = await getHeaders({ sheet, sheets: this.sheets, spreadsheetId: this.spreadsheetId}) const columns = Object.keys(where) as (keyof RecordType)[] const header = headers.find(header => header.name === columns[0]) const range = `${sheet}!${header?.column}:${header?.column}` try { const response = await this.sheets.spreadsheets.values.get({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, range }) const rowIndex = response.data.values?.findIndex(row => checkWhereFilter(where[columns[0]] as WhereCondition|string, row[0] as string)) @@ -261,7 +261,7 @@ export default class HolySheets = any> { } const rowRange = `${sheet}!A${rowIndex + 1}:${indexToColumn(headers.length - 1)}${rowIndex + 1}` const rowResponse = await this.sheets.spreadsheets.values.get({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, range: rowRange }) @@ -288,14 +288,14 @@ export default class HolySheets = any> { public async findMany(options: { where: WhereClause, select?: SelectClause }): Promise[]> { const { where, select } = options const sheet = this.sheet - const headers = await getHeaders({ sheet, sheets: this.sheets, spreadsheetId: HolySheets.spreadsheetId}) + const headers = await getHeaders({ sheet, sheets: this.sheets, spreadsheetId: this.spreadsheetId}) const columns = Object.keys(where) as (keyof RecordType)[] const header = headers.find(header => header.name === columns[0]) const range = `${sheet}!${header?.column}:${header?.column}` try { const response = await this.sheets.spreadsheets.values.get({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, range }) const rowIndexes = response.data.values?.reduce((acc: number[], row, index) => { @@ -310,7 +310,7 @@ export default class HolySheets = any> { const rowsRange = rowIndexes.map(index => `${sheet}!A${index + 1}:${indexToColumn(headers.length - 1)}${index + 1}`) const ranges = rowsRange const batchGetResponse = await this.sheets.spreadsheets.values.batchGet({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, ranges: ranges }) @@ -396,7 +396,7 @@ export default class HolySheets = any> { } const { range } = record await this.sheets.spreadsheets.values.clear({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, range }) return record @@ -418,7 +418,7 @@ export default class HolySheets = any> { } const ranges = records.map(record => record.range) await this.sheets.spreadsheets.values.batchClear({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, requestBody: { ranges } @@ -434,7 +434,7 @@ export default class HolySheets = any> { */ public async getSheetId(title: string): Promise { const response = await this.sheets.spreadsheets.get({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, includeGridData: false }) @@ -469,7 +469,7 @@ export default class HolySheets = any> { } }] await this.sheets.spreadsheets.batchUpdate({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, requestBody: { requests } @@ -506,7 +506,7 @@ export default class HolySheets = any> { )) await this.sheets.spreadsheets.batchUpdate({ - spreadsheetId: HolySheets.spreadsheetId, + spreadsheetId: this.spreadsheetId, requestBody: { requests }