This commit is contained in:
SheetJS 2023-11-30 02:10:37 -05:00
parent f14874c930
commit 4770277d0b
20 changed files with 247 additions and 100 deletions

View File

@ -65,7 +65,7 @@ Snyk security tooling may report errors involving "Prototype Pollution":
Prototype Pollution [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-XLSX-5457926]
```
As noted in the [Snyk report](https://web.archive.org/web/20230920204324/https://security.snyk.io/vuln/SNYK-JS-XLSX-5457926):
As noted in the [Snyk report](https://web.archive.org/web/20231129100639/https://security.snyk.io/vuln/SNYK-JS-XLSX-5457926):
> The issue is resolved in version 0.19.3

View File

@ -55,7 +55,7 @@ Snyk security tooling may report errors involving "Prototype Pollution":
Prototype Pollution [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-XLSX-5457926]
```
As noted in the [Snyk report](https://web.archive.org/web/20230920204324/https://security.snyk.io/vuln/SNYK-JS-XLSX-5457926):
As noted in the [Snyk report](https://web.archive.org/web/20231129100639/https://security.snyk.io/vuln/SNYK-JS-XLSX-5457926):
> The issue is resolved in version 0.19.3

View File

@ -446,7 +446,7 @@ This browser demo was tested in the following environments:
| Browser | Date |
|:------------|:-----------|
| Chrome 119 | 2023-11-04 |
| Chrome 119 | 2023-11-30 |
Some lesser-used browsers do not support File System Access API:

View File

@ -144,13 +144,13 @@ This workbook is typically exported to the filesystem with `writeFile`[^6].
## Live Demo
:::note
:::note Tested Deployments
This browser demo was tested in the following environments:
| Browser | Date |
|:------------|:-----------|
| Chrome 117 | 2023-10-13 |
| Chrome 119 | 2023-11-30 |
Some lesser-used browsers do not support WebSQL:

View File

@ -14,13 +14,13 @@ This demo covers two common use patterns:
- "Row Objects" shows a simple convention for loading and storing row objects
- "Simple Strings" discusses how to persist and recover a raw Storage
:::note
:::note Tested Deployments
Each browser demo was tested in the following environments:
| Browser | Date |
|:------------|:-----------|
| Chrome 116 | 2023-09-17 |
| Chrome 119 | 2023-11-30 |
| Safari 16.6 | 2023-09-17 |
:::

View File

@ -16,13 +16,13 @@ XLS (both '97-2004 and '95), and SpreadsheetML 2003.
Not all Clipboard APIs offer access to all clipboard types.
:::note
:::note Tested Deployments
Each browser demo was tested in the following environments:
| Browser | Date |
|:------------|:-----------|
| Chrome 116 | 2023-09-01 |
| Chrome 119 | 2023-11-30 |
| Safari 16.6 | 2023-09-01 |
| Brave 1.57 | 2023-09-01 |

View File

@ -37,13 +37,14 @@ production sites.
### localForage
:::note
:::note Tested Deployments
This demo was last tested in the following environments:
| Browser | Date | `localForage` |
|:------------|:-----------|:--------------|
| Chrome 117 | 2023-10-18 | 1.10.0 |
| Chrome 119 | 2023-11-30 | 1.10.0 |
| Safari 17.0 | 2023-11-30 | 1.10.0 |
:::
@ -108,13 +109,13 @@ function SheetJSLocalForage() {
### DexieJS
:::note
:::note Tested Deployments
This demo was last tested in the following environments:
| Browser | Date | DexieJS |
|:------------|:-----------|:--------|
| Chrome 117 | 2023-10-18 | 3.2.4 |
| Chrome 119 | 2023-11-30 | 3.2.4 |
:::

View File

@ -1,4 +1,4 @@
{
"label": "Local Data",
"position": 8
"position": 27
}

View File

@ -27,7 +27,7 @@ may require some adjustments. The official documentation should be consulted.
:::
:::note
:::note Tested Deployments
This demo was last tested on 2023 September 30 using Lightning API version `58.0`.
@ -71,6 +71,7 @@ It is recommended to load the library in a callback. For example, the following
```js
import { LightningElement, api } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
// highlight-next-line
import sheetjs from '@salesforce/resourceUrl/sheetjs';
export default class SheetComponent extends LightningElement {
@ -96,21 +97,17 @@ export default class SheetComponent extends LightningElement {
### Exporting Data from SF List
:::note pass
There are many different data types and APIs. This demo uses the deprecated
`getListUi` function to pull account data.
:::
Using the LWC Wire Service, components receive data in separate events. Exports
are typically generated in a separate event handler. Component state is normally
used to handle the timing mismatch.
Using the LWC Wire Service, components receive data in separate events.[^3]
Event handlers typically store the updated data in component state, ensuring the
data is available when a spreadsheet export is requested.
#### Getting Account Data
The main method to obtain data is `getListUi` and the key for account data is
`ACCOUNT_OBJECT`:
This demo uses the deprecated `getListUi` function[^4] to pull account data.
`getListUi` requires the name of the LWC object (`objectApiName` property) and
name of the LWC list view (`listViewApiName` property)
The following snippet receives data from the "All Accounts" list view:
```js
import { LightningElement, wire } from 'lwc';
@ -231,10 +228,10 @@ export default class SheetComponent extends LightningElement {
#### Exporting Data
This is readily exported to a spreadsheet in a callback function. Starting from
the array of arrays, the SheetJS `aoa_to_sheet` method[^3] generates a SheetJS
sheet object[^4]. A workbook object[^5] is created with `book_new`[^6] and the
sheet is added with `book_append_sheet`[^7]. Finally, the SheetJS `writeFile`
method creates a XLSX file and initiates a download[^8].
the array of arrays, the SheetJS `aoa_to_sheet` method[^5] generates a SheetJS
sheet object[^6]. A workbook object[^7] is created with `book_new`[^8] and the
sheet is added with `book_append_sheet`[^9]. Finally, the SheetJS `writeFile`
method creates a XLSX file and initiates a download[^10].
```js
@api async download() {
@ -708,9 +705,11 @@ cell styling, automatic column width calculations, and frozen rows.
[^1]: It is strongly recommended to review the [detailed introduction in the Salesforce documentation](https://developer.salesforce.com/docs/platform/lwc/guide/get-started-introduction.html)
[^2]: The `XLSX` variable is the main global for the SheetJS library. It exposes methods as described in ["API Reference"](/docs/api/)
[^3]: See [`aoa_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
[^4]: See ["Sheet Objects"](/docs/csf/sheet)
[^5]: See ["Workbook Object"](/docs/csf/book)
[^6]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
[^7]: See [`book_append_sheet` in "Utilities"](/docs/api/utilities/wb)
[^8]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^3]: See ["Understand the Wire Service"](https://developer.salesforce.com/docs/platform/lwc/guide/data-wire-service-about.html) in the Salesforce LWC documentation.
[^4]: See [`getListUI`](https://developer.salesforce.com/docs/platform/lwc/guide/reference-get-list-ui.html) in the Salesforce LWC documentation.
[^5]: See [`aoa_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
[^6]: See ["Sheet Objects"](/docs/csf/sheet)
[^7]: See ["Workbook Object"](/docs/csf/book)
[^8]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
[^9]: See [`book_append_sheet` in "Utilities"](/docs/api/utilities/wb)
[^10]: See [`writeFile` in "Writing Files"](/docs/api/write-options)

View File

@ -18,7 +18,7 @@ data from spreadsheets.
This demo explores the SuiteScript scripting features in NetSuite. We'll explore
how to use SheetJS in SuiteScripts for reading and writing files in NetSuite.
:::note
:::note Tested Deployments
This demo was verified by NetSuite consultants in the following deployments:
@ -40,8 +40,8 @@ to the file cabinet and referenced in the `define` call in SuiteScripts.
:::info pass
SheetJS scripts have been tested against the Rhino engine[^3] and work in both
SuiteScript 2.0 and SuiteScript 2.1.
SheetJS scripts have been tested against the Rhino JavaScript engine[^3] and
work in both SuiteScript 2.0 and SuiteScript 2.1 deployments.
:::

View File

@ -7,7 +7,7 @@ pagination_next: demos/extensions/index
import current from '/version.js';
import CodeBlock from '@theme/CodeBlock';
[Amazon Web Services](https://aws.amazon.com/) (AWS) is a Cloud Services
[Amazon Web Services](https://aws.amazon.com/) (AWS) is a cloud services
platform which includes traditional virtual machine support, "Serverless
Functions" and cloud storage.
@ -31,7 +31,7 @@ will be available in the future.
:::
:::note
:::note Tested Deployments
This demo was last tested on 2023 October 01.
@ -39,7 +39,7 @@ This demo was last tested on 2023 October 01.
## Lambda Functions
AWS offers the NodeJS runtime for JavaScript serverless function.[^1]
AWS offers NodeJS runtimes for running JavaScript serverless functions.[^1]
The [SheetJS NodeJS module](/docs/getting-started/installation/nodejs) can be
required in Lambda functions. When deploying, the entire `node_modules` folder

View File

@ -9,7 +9,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
[Azure Cloud Services](https://azure.microsoft.com/) is a Cloud Services
[Azure Cloud Services](https://azure.microsoft.com/) is a cloud services
platform which includes traditional virtual machine support, "Serverless
Functions" and cloud storage.
@ -33,7 +33,7 @@ will be available in the future.
:::
:::note
:::note Tested Deployments
This demo was last tested on 2023 October 06.
@ -123,13 +123,39 @@ option was not required.
### Reading Data
Using `@azure/functions`, the handler callback receives a `Request` object. With
standard JS operations, the file can be pulled into an `ArrayBuffer` object.
Using `@azure/functions`, the handler callback receives a `Request` object.
Uploaded files can be pulled into `ArrayBuffer` objects.
<details><summary><b>Code Snippet</b> (click to show)</summary>
This function returns a promise that resolves to an `ArrayBuffer` object:
```js
const { Blob } = require('buffer');
async function get_file_from_request(request, form_field_name) {
/* parse the request body */
const formData = await request.formData();
/* pull the specified field */
const file = formData.get(form_field_name);
/* if a file was submitted, `file` will be a Blob */
if(!(file instanceof Blob)) throw new Error(`File is missing!`);
/* pull data into an ArrayBuffer object */
const ab = await file.arrayBuffer();
return ab;
}
```
</details>
The SheetJS `read` method[^2] can read the `ArrayBuffer` objects and generate
SheetJS workbook objects[^3] which can be processed with other API functions.
For example, a handler can use `sheet_to_csv`[^4] to generate CSV text:
For example, a handler can use `sheet_to_csv`[^4] to generate CSV text from
user-submitted spreadsheets:
```js
const { Blob } = require('buffer');
@ -143,6 +169,7 @@ app.http('SheetJSAzure', {
const formData = await req.formData();
const f = formData.get("upload");
/* if a file was submitted, `f` will be a Blob object */
if(!(f instanceof Blob)) return { status: 400, body: "Must submit a file" };
/* parse file */

View File

@ -60,17 +60,17 @@ As a project from the company, the entire lifecycle uses GitHub offerings:
- GitHub offers free hosting for Git repositories
- GitHub Actions[^1] infrastructure runs tasks at regular intervals
- `githubocto/flat`[^2] Action to help fetch data and automate post-processing
- `flat-postprocessing`[^3] Post-processing helper functions and examples
- "Flat Viewer"[^4]: Web viewer for structured CSV and JSON data on GitHub
- `githubocto/flat`[^2] library helps fetch data and automate post-processing
- `flat-postprocessing`[^3] library provides post-processing helper functions
- "Flat Viewer"[^4] displays structured CSV and JSON data from Git repositories
:::caution pass
A GitHub account is required. When the demo was tested, free GitHub accounts had
no Actions usage limits for public repositories.
A GitHub account is required. When the demo was last tested, "GitHub Free"
accounts had no Actions usage limits for public repositories[^5].
Using private GitHub repositories is not recommended because the Flat Viewer
cannot access private repositories.
Private GitHub repositories can be used for processing data, but the Flat Viewer
will not be able to display private data.
:::
@ -143,12 +143,12 @@ for more details.
The first argument to the post-processing script is the filename.
The SheetJS `readFile` method[^5] will read the file and generate a SheetJS
workbook object[^6]. After extracting the first worksheet, `sheet_to_csv`[^7]
The SheetJS `readFile` method[^6] will read the file and generate a SheetJS
workbook object[^7]. After extracting the first worksheet, `sheet_to_csv`[^8]
generates a CSV string.
After generating a CSV string, the string should be written to the filesystem
using `Deno.writeFileSync`[^8]. By convention, the CSV should preserve the file
using `Deno.writeFileSync`[^9]. By convention, the CSV should preserve the file
name stem and replace the extension with `.csv`:
<CodeBlock title="postprocess.ts" language="ts">{`\
@ -179,7 +179,7 @@ Deno.writeFileSync(out_file, new TextEncoder().encode(csv));`}
## Complete Example
:::note
:::note Tested Deployments
This was last tested by SheetJS users on 2023 September 24 using the GitHub UI.
@ -321,7 +321,8 @@ jobs:
[^2]: See [`githubocto/flat`](https://github.com/githubocto/flat) repo on GitHub.
[^3]: See [`githubocto/flat-postprocessing`](https://github.com/githubocto/flat-postprocessing) repo on GitHub.
[^4]: The hosted version is available at <https://flatgithub.com/>
[^5]: See [`readFile` in "Reading Files"](/docs/api/parse-options)
[^6]: See ["Workbook Object"](/docs/csf/book)
[^7]: See [`sheet_to_csv` in "CSV and Text"](/docs/api/utilities/csv#delimiter-separated-output)
[^8]: See [`Deno.writeFileSync`](https://deno.land/api?s=Deno.writeFileSync) in the Deno Runtime APIs documentation.
[^5]: See ["About billing for GitHub Actions"](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions) in the GitHub documentation.
[^6]: See [`readFile` in "Reading Files"](/docs/api/parse-options)
[^7]: See ["Workbook Object"](/docs/csf/book)
[^8]: See [`sheet_to_csv` in "CSV and Text"](/docs/api/utilities/csv#delimiter-separated-output)
[^9]: See [`Deno.writeFileSync`](https://deno.land/api?s=Deno.writeFileSync) in the Deno Runtime APIs documentation.

View File

@ -16,8 +16,8 @@ data from spreadsheets.
This demo covers integration details. We'll explore how to load and use SheetJS
scripts in Deno Deploy functions.
The ["Demo"](#demo) section includes build a sample service that converts XLSX
and other types of spreadsheets to HTML tables and CSV rows.
The ["Demo"](#demo) section builds a sample service that converts XLSX and other
types of spreadsheets to HTML tables and CSV rows.
:::caution pass
@ -25,7 +25,7 @@ When the demo was last tested, Deno Deploy required a GitHub account.
:::
:::note
:::note Tested Deployments
This demo was last tested by SheetJS users on 2023 October 18.

View File

@ -54,8 +54,8 @@ referenced APIs will be available in the future.
This demo uses the following NodeJS modules:
- `google-auth-library`[^1] to authenticate with Google APIs
- `node-google-spreadsheet`[^2] to interact with Google Sheets v4 API
- `google-auth-library`[^1] simplifies authentication with Google APIs
- `node-google-spreadsheet`[^2] interacts with Google Sheets v4 API
:::info Initial Setup
@ -70,7 +70,10 @@ It is strongly recommended to use a service account for Google API operations.
The ["Service Account Setup" section](#service-account-setup) covers how to
create a service account and generate a JSON key file.
```js title="Authenticate using a JSON key file"
The generated JSON key file includes `client_email` and `private_key` fields.
These fields can be used in JWT authentication:
```js title="JWT Authentication using a JSON key file"
import { JWT } from 'google-auth-library'
import { GoogleSpreadsheet } from 'google-spreadsheet';
@ -331,7 +334,7 @@ At this point `wb` is a SheetJS workbook object[^7].
## Complete Example
:::note
:::note Tested Deployments
This demo was last tested on 2023 September 17 using `google-auth-library` for
authentication (`v8.9.0`) and `google-spreadsheet` for API access (`v4.1.0`).

View File

@ -24,44 +24,85 @@ libraries and appended to a dataset in Airtable
## NodeJS Integration
Airtable recommends Personal Access Tokens for interacting with their API. When
fetching data from the API, the result will include an array of row objects that
can be converted to a worksheet with `XLSX.utils.json_to_sheet`. The API methods
to write data will accept row objects generated by `XLSX.utils.sheet_to_json`.
### Installation
The main module is `airtable` and can be installed with `npm`:
The [SheetJS NodeJS module](/docs/getting-started/installation/nodejs) can be
required in NodeJS scripts that interact with Airtable.
The Airtable connector module is `airtable` and can be installed with `npm`:
<CodeBlock language="bash">{`\
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz airtable`}
</CodeBlock>
To obtain a reference to a table, code needs a [PAT](#personal-access-token),
the name of the workspace (typically starting with `app`), and the name of the
desired table (the Excel import typically supports the worksheet name):
### Authentication
Airtable recommends Personal Access Tokens ("PAT") for interacting with the API.
:::note pass
The ["Personal Access Token"](#personal-access-token) section walks through the
process of creating a PAT.
:::
The connector constructor accepts an options argument. The PAT should be passed
using the `apiKey` property:
```js
const Airtable = require("airtable"), XLSX = require("xlsx");
/* query all records in a table */
const conn = new Airtable({apiKey: "PAT...", /* and other options ... */});
const table = conn.base("app...").table("tablename...");
const Airtable = require("airtable");
const apiKey = "PAT..."; // personal access token
const conn = new Airtable({apiKey, /* see docs for other options ... */});
```
The `base` method opens a specified workspace. The internal workspace name is
the first fragment in the Airtable URL, typically starts with "app":
```js
const base = conn.base("app...");
```
The `table` method of the workspace object opens a specified table:
```js
const table = base.table("tablename...");
```
### Exporting Data
When querying data, a result set will be a simple array of Record objects. The
`fields` property is a simple JS object compatible with `json_to_sheet`:
`fields` property of each record object is a simple JS object. Mapping over the
result set and picking the `fields` field yields a standard array of objects:
```js
/** Create SheetJS worksheet from Airtable table */
async function airtable_to_worksheet(table) {
/** Create array of objects from Airtable table */
async function airtable_to_aoo(table) {
/* get all rows */
const result = await table.select().all();
/* pull raw objects from the result */
// highlight-next-line
const aoo = result.map(r => r.fields);
return aoo;
}
```
The SheetJS `json_to_sheet` utility function[^1] can generate a worksheet object
from the array of objects:
```js
const XLSX = require("xlsx");
/** Create SheetJS worksheet from Airtable table */
async function airtable_to_worksheet(table) {
/* get all rows */
const result = await table.select().all();
/* pull raw objects from the result */
const aoo = result.map(r => r.fields);
/* create a worksheet */
// highlight-next-line
const worksheet = XLSX.utils.json_to_sheet(aoo);
return worksheet;
}
@ -74,17 +115,43 @@ for sorting by fields.
:::
### Importing Data
When inserting records, each object should be wrapped in a parent object with a
single `fields` property:
The worksheet object must be added to a new workbook object using the `book_new`
and `book_append_sheet` helper functions[^2]:
```js
/** Append records from a SheetJS worksheet to Airtable table */
async function airtable_load_worksheet(table, worksheet) {
/* suppose the field names */
const aoo = XLSX.utils.sheet_to_json(worksheet);
/** Create SheetJS workbook from Airtable table */
async function airtable_to_workbook(table) {
/* generate worksheet */
const ws = await airtable_to_worksheet(table);
/* create a new workbook */
const wb = XLSX.utils.book_new();
/* add worksheet to workbook */
XLSX.utils.book_append_sheet(wb, ws, "ExportedData");
return wb;
}
```
Local files can be created using the SheetJS `writeFile` method[^3]:
```js
(async() => {
/* generate SheetJS workbook */
const wb = await airtable_to_workbook(table);
/* write to XLSX */
XLSX.writeFile(wb, "SheetJSAirtableExport.xlsx");
})();
```
### Importing Data
The Airtable table `create` method expects an array of record objects. The
`fields` property of each object is expected to contain the raw record data.
Mapping over a standard array of objects can create Airtable-friendly data:
```js
/** Append records from an array of data objects to Airtable table */
async function airtable_load_aoo(table, aoo) {
/* reshape to be compatible with Airtable API */
// highlight-next-line
const airtable_rows = aoo.map(fields => ({ fields }));
@ -94,9 +161,50 @@ async function airtable_load_worksheet(table, worksheet) {
}
```
Starting from a SheetJS worksheet object[^4], the `sheet_to_json` method[^5] can
generate normal arrays of objects:
```js
const XLSX = require("xlsx");
/** Append records from a SheetJS worksheet to Airtable table */
async function airtable_load_worksheet(table, worksheet) {
/* generate normal array of objects */
// highlight-next-line
const aoo = XLSX.utils.sheet_to_json(worksheet);
/* upload data */
return await airtable_load_aoo(table, aoo);
}
```
A SheetJS worksheet object can be extracted from a workbook object[^6]:
```js
/** Append records from the first worksheet of a workbook to Airtable table */
async function airtable_load_workbook(table, workbook) {
/* pull first worksheet from workbook object */
// highlight-next-line
const first_sheet_name = workbook.SheetNames[0];
const ws = workbook.Sheets[first_sheet_name];
/* upload data */
return await airtable_load_worksheet(table, ws);
}
```
Local files can be read using the SheetJS `readFile` method[^7]:
```js
const wb = XLSX.readFile("SheetJSAirtableTest.xlsb");
(async() => {
await airtable_load_workbook(table, wb);
});
```
## Complete Example
:::note
:::note Tested Deployments
This demo was last tested on 2023 September 03. At the time, free accounts
included limited API access.
@ -240,4 +348,12 @@ node SheetJSAirtableWrite.js
Open Airtable and verify the new row was added:
![Final Result in Airtable](pathname:///airtable/post.png)
![Final Result in Airtable](pathname:///airtable/post.png)
[^1]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^2]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^3]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^4]: See ["Sheet Objects"](/docs/csf/sheet) for more details/
[^5]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^6]: See ["Workbook Object"](/docs/csf/book)
[^7]: See [`readFile` in "Reading Files"](/docs/api/parse-options)

View File

@ -6,7 +6,7 @@ pagination_next: demos/extensions/index
---
<head>
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="4ysmnhy8wtm6k3w"></script>
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="6msofx0wc1zd7da"></script>
</head>
[Dropbox](https://www.dropbox.com/) is a file hosting service that offers APIs
@ -34,7 +34,7 @@ their Dropbox account. This demo will generate a XLS workbook using SheetJS.
The Dropbox API script is loaded in this page with
```html
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="4ysmnhy8wtm6k3w"></script>
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="6msofx0wc1zd7da"></script>
```
The `data-app-key` used in this demo is a "Development" key associated with the
@ -262,9 +262,9 @@ function SheetJSEnregistrez() {
## Dropbox App
:::note
:::note Tested Deployments
This demo was last tested on 2023 August 26.
This demo was last tested on 2023 November 30.
:::

View File

@ -1,4 +1,4 @@
{
"label": "Cloud Platforms",
"position": 9
"position": 30
}