Skip to content

Commit

Permalink
CreateAll method (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
joseffffff committed May 23, 2024
1 parent 9dfca02 commit ec95151
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 57 deletions.
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,33 @@ await orm.create({
- **Parameters**:
- `entity`: The entity object to persist as a new row in the sheet.
- **Remarks**:
- This method appends a new row at the end of the specified sheet in the associated spreadsheet.
- It retrieves the headers of the sheet to ensure proper alignment of data.
- The entity object is converted into an array of cell values according to the sheet headers.
- It internally retrieves the headers of the sheet to ensure proper alignment of data.
- Quota retries are automatically handled to manage API rate limits.

### `createAll(entities: T[])`

Creates many new rows in the specified sheet with the provided entities.

```typescript
// Adds 2 new rows to sheet
await orm.createAll([
{
id: '1111-2222-3333-4444',
dateCreated: new Date(),
name: 'John Doe',
},
{
id: '5555-6666-7777-8888',
dateCreated: new Date(),
name: 'John Doe 2',
},
]);
```

- **Parameters**:
- `entities`: The entity array to persist as new rows in the sheet.
- **Remarks**:
- It internally retrieves the headers of the sheet to ensure proper alignment of data.
- Quota retries are automatically handled to manage API rate limits.

### `delete(entity: T)`
Expand Down
99 changes: 48 additions & 51 deletions src/GoogleSpreadsheetsOrm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,14 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
* @param entity - The entity object to persist as a new row in the sheet.
*
* @remarks
* This method appends a new row to the end of the specified sheet in the associated spreadsheet.
* This method appends a new row at the end of the specified sheet in the associated spreadsheet.
* It retrieves the headers of the sheet to ensure proper alignment of data.
* Quota retries are automatically handled to manage API rate limits.
*
* @returns A Promise that resolves when the row creation process is completed successfully.
*/
public async create(entity: T): Promise<void> {
const headers: string[] = await this.sheetHeaders();
const toSave: ParsedSpreadsheetCellValue[] = this.toSheetArrayFromHeaders(entity, headers);

await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
sheetsClient.spreadsheets.values.append({
spreadsheetId: this.options.spreadsheetId,
range: this.options.sheet,
insertDataOption: 'INSERT_ROWS',
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [toSave],
},
}),
);
return this.createAll([entity]);
}

/**
Expand Down Expand Up @@ -114,35 +101,45 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
);
}

// public async createAll(entities: T[]): Promise<boolean> {
// if (entities.length === 0) {
// return true;
// }
//
// if (entities.some(entity => !entity.id)) {
// throw new GoogleSpreadsheetOrmError('Cannot persist entities that have no id.');
// }
//
// const { headers } = await this.findTableData();
//
// const entitiesDatabaseArrays: ParsedSpreadsheetCellValue[][] = entities.map(entity =>
// this.toSheetArrayFromHeaders(entity, headers),
// );
//
// await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
// sheetsClient.spreadsheets.values.append({
// spreadsheetId: this.spreadsheetId,
// range: this.sheet,
// insertDataOption: 'INSERT_ROWS',
// valueInputOption: 'USER_ENTERED',
// requestBody: {
// values: entitiesDatabaseArrays,
// },
// }),
// );
//
// return true;
// }
/**
* Creates a new row in the specified sheet for each entity provided in the *entities* array.
*
* @param entities - An array of entities objects to persist as a new row in the sheet.
*
* @remarks
* This method appends a new row for each entity provided at the end of the specified sheet in the associated spreadsheet.
* It retrieves the headers of the sheet to ensure proper alignment of data.
* Quota retries are automatically handled to manage API rate limits.
*
* @returns A Promise that resolves when the row creation process is completed successfully.
*/
public async createAll(entities: T[]): Promise<void> {
if (entities.length === 0) {
return;
}

if (entities.some(entity => !entity.id)) {
throw new GoogleSpreadsheetOrmError('Cannot persist entities that have no id.');
}

const headers = await this.sheetHeaders();

const entitiesDatabaseArrays: ParsedSpreadsheetCellValue[][] = entities.map(entity =>
this.toSheetArrayFromHeaders(entity, headers),
);

await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
sheetsClient.spreadsheets.values.append({
spreadsheetId: this.options.spreadsheetId,
range: this.options.sheet,
insertDataOption: 'INSERT_ROWS',
valueInputOption: 'USER_ENTERED',
requestBody: {
values: entitiesDatabaseArrays,
},
}),
);
}

// public updateAll(entities: T[]): boolean {
// if (entities.length === 0) {
Expand Down Expand Up @@ -184,7 +181,7 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {

return sheetDetails;
}
//

// public deleteAll(entities: T[]): boolean {
// if (entities.length === 0) {
// return true;
Expand All @@ -205,14 +202,14 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
// }
//
private rowNumber(data: ParsedSpreadsheetCellValue[][], entity: T): number {
for (let i = 0; i < data.length; i++) {
if (data[i][0] === entity.id) {
// +1 because no headers in array and +1 because row positions starts at 1
return i + 2;
}
const index = data.findIndex(row => row[0] === entity.id);

if (index === -1) {
throw new GoogleSpreadsheetOrmError(`Provided entity is not part of '${this.options.sheet}' sheet.`);
}

throw new GoogleSpreadsheetOrmError('Not found');
// +1 because no headers in array and +1 because row numbers starts at 1
return index + 2;
}

private toSheetArrayFromHeaders(entity: T, tableHeaders: string[]): ParsedSpreadsheetCellValue[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GoogleSpreadsheetOrmError } from '../errors/GoogleSpreadsheetOrmError';
import { GoogleSpreadsheetOrmError } from './GoogleSpreadsheetOrmError';

export class SerializationError extends GoogleSpreadsheetOrmError {
constructor(message: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/serialization/BooleanSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Serializer } from './Serializer';
import { SerializationError } from './SerializationError';
import { SerializationError } from '../errors/SerializationError';

export class BooleanSerializer implements Serializer<boolean> {
public fromSpreadsheetValue(value: string): boolean | undefined {
Expand Down
2 changes: 1 addition & 1 deletion src/serialization/NumberSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Serializer } from './Serializer';
import { SerializationError } from './SerializationError';
import { SerializationError } from '../errors/SerializationError';

export class NumberSerializer implements Serializer<number> {
public fromSpreadsheetValue(value: string | undefined): number | undefined {
Expand Down
126 changes: 126 additions & 0 deletions tests/GoogleSpreadsheetsOrm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Resource$Spreadsheets = sheets_v4.Resource$Spreadsheets;
import Schema$Spreadsheet = sheets_v4.Schema$Spreadsheet;

import Params$Resource$Spreadsheets$Values$Append = sheets_v4.Params$Resource$Spreadsheets$Values$Append;
import { GoogleSpreadsheetOrmError } from '../src/errors/GoogleSpreadsheetOrmError';

const SPREADSHEET_ID = 'spreadsheetId';
const SHEET = 'test_entities';
Expand Down Expand Up @@ -164,6 +165,98 @@ describe(GoogleSpreadsheetsOrm.name, () => {
} as Params$Resource$Spreadsheets$Values$Append);
});

test('createAll method should insert a new row per each entity provided', async () => {
// Configure table headers, so that save method can correctly match headers positions.
const rawValues = [['id', 'createdAt', 'name', 'jsonField', 'current', 'year']];
mockValuesResponse(rawValues);

const entities: TestEntity[] = [
{
id: 'ae222b54-182f-4958-b77f-26a3a04dff35',
createdAt: new Date('2023-12-29 17:47:04'),
name: 'John Doe',
jsonField: {
a: 'b',
c: [1, 2, 3],
},
current: undefined,
year: 2023,
},
{
id: 'ae222b54-182f-4958-b77f-26a3a04dff36',
createdAt: new Date('2024-12-31 17:47:04'),
name: 'John Doe 2',
jsonField: [1, 2, 3],
current: false,
year: 2000,
},
];

await sut.createAll(entities);

expect(getValuesUsedSheetClient()?.spreadsheets.values.append).toHaveBeenCalledWith({
spreadsheetId: SPREADSHEET_ID,
range: SHEET,
insertDataOption: 'INSERT_ROWS',
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [
[
'ae222b54-182f-4958-b77f-26a3a04dff35', // id
'29/12/2023 17:47:04', // createdAt
'John Doe', // name
// language=json
'{"a":"b","c":[1,2,3]}', // jsonField
'', // current
'2023', // year
],
[
'ae222b54-182f-4958-b77f-26a3a04dff36', // id
'31/12/2024 17:47:04', // createdAt
'John Doe 2', // name
// language=json
'[1,2,3]', // jsonField
'false', // current
'2000', // year
],
],
},
} as Params$Resource$Spreadsheets$Values$Append);
});

test('createAll method should not persist anything if an empty array is passed', async () => {
await sut.createAll([]);
// @ts-ignore
expect(sheetClients.every(client => client.spreadsheets.values.append.mock.calls.length === 0)).toBeTruthy();
});

test('createAll method should fail if some passed entity has undefined id', async () => {
await expect(
sut.createAll([
{
// @ts-ignore
id: undefined,
createdAt: new Date('2023-12-29 17:47:04'),
name: 'John Doe',
jsonField: {
a: 'b',
c: [1, 2, 3],
},
current: undefined,
year: 2023,
},
{
id: 'ae222b54-182f-4958-b77f-26a3a04dff36',
createdAt: new Date('2024-12-31 17:47:04'),
name: 'John Doe 2',
jsonField: [1, 2, 3],
current: false,
year: 2000,
},
]),
).rejects.toStrictEqual(new GoogleSpreadsheetOrmError('Cannot persist entities that have no id.'));
});

test('delete method should correctly delete the row with that id', async () => {
mockValuesResponse([
['id', 'createdAt', 'name', 'jsonField', 'current', 'year'],
Expand Down Expand Up @@ -233,6 +326,39 @@ describe(GoogleSpreadsheetsOrm.name, () => {
});
});

test('delete method should fail if provided entity is not part of the sheet', async () => {
mockValuesResponse([['id', 'createdAt', 'name', 'jsonField', 'current', 'year']]);

mockSpreadsheetDetailsResponse({
data: {
sheets: [
{
properties: {
title: SHEET,
sheetId: 1234,
},
},
],
},
} as never);

const entity: TestEntity = {
id: 'ae222b54-182f-4958-b77f-26a3a04dff35',
createdAt: new Date('2023-12-29 17:47:04'),
name: 'John Doe',
jsonField: {
a: 'b',
c: [1, 2, 3],
},
current: true,
year: 2023,
};

await expect(sut.delete(entity)).rejects.toStrictEqual(
new GoogleSpreadsheetOrmError(`Provided entity is not part of '${SHEET}' sheet.`),
);
});

function mockValuesResponse(rawValues: string[][]): void {
sheetClients
.map(s => s.spreadsheets.values as MockProxy<sheets_v4.Resource$Spreadsheets$Values>)
Expand Down

0 comments on commit ec95151

Please sign in to comment.