Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"packages/br-cal",
"packages/br-script",
"packages/discover-dl",
"packages/lm-sync"
"packages/lm-sync",
"packages/makesheet"
],
"devDependencies": {
"@endo/eslint-plugin": "^0.5.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/makesheet/.claspignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git/**
node_modules/**
37 changes: 37 additions & 0 deletions packages/makesheet/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"extends": [
"airbnb-base"
],
"env": {
"es6": true
},
"rules": {
"quotes": [
"error",
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"comma-dangle": ["error", "always-multiline"],
"no-console": "off",
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-undef": "off",
"no-underscore-dangle": [
"error",
{
"allowAfterThis": false,
"allowAfterSuper": false,
"enforceInMethodNames": false,
"allow": ["_testDoGet", "_testCreateJsonResponse"]
}
]
}
}
1 change: 1 addition & 0 deletions packages/makesheet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock.json
49 changes: 49 additions & 0 deletions packages/makesheet/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Contributing to makesheet

## Code Style

This project follows Airbnb JavaScript style guidelines. Use ESLint to check and fix code style:

```bash
yarn lint
yarn lint-fix
```

## Object Capability (OCap) Discipline

This project follows object-capability discipline to enable secure composition and testing. This means avoiding ambient authority where possible.

### Ambient Authority Pattern

When functions need to access global APIs (like `DriveApp.getFileById`), use dependency injection to allow callers to provide these capabilities:

```js
function doGet(e, io = {}) {
const {
getFileById = DriveApp.getFileById,
} = io;
// ... use getFileById instead of DriveApp.getFileById directly
}
```

This pattern provides several benefits:

1. **Testability**: Callers can inject mock implementations for testing
2. **Attenuation**: Callers can provide restricted versions of capabilities
3. **Composability**: Functions become more modular and reusable
4. **Security**: Reduces ambient authority and enables principle of least privilege

### References

For more details on object-capability discipline, see the [Agoric OCap Discipline wiki](https://github.com/Agoric/agoric-sdk/wiki/OCap-Discipline).

## Testing

Since this is Google Apps Script code, we use test functions that can be executed in the Google Apps Script debugger:

1. Create test functions prefixed with underscore (e.g., `_testDoGet`)
2. Use `console.log` to output test results
3. Run tests by selecting the test function in the GAS editor and clicking run
4. View results in the console output

This approach allows testing without external frameworks while maintaining the ability to test core functionality.
91 changes: 91 additions & 0 deletions packages/makesheet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# makesheet

Google Sheets timestamp service for Make-based workflows.

## Overview

This package provides a Google Apps Script web app that returns the modification timestamp of a Google Sheet. This is useful for Make-based build systems that need to track when a sheet has been modified to trigger downstream processing.

## Setup

1. Install clasp globally (required for deployment, though you could add it as a devDependency if preferred):
```bash
npm install -g @google/clasp
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any particular reason to do it globally rather than devDependencies?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added note that clasp can be installed as devDependency if preferred, though global installation is common for deployment tools. Commit ee3ee2e

```

2. Login to Google Apps Script:
```bash
yarn login
```

3. Create a new Google Apps Script project and note the script ID.
[See the Google Apps Script documentation](https://developers.google.com/apps-script/guides/projects) for detailed instructions on creating projects.

4. Update `.clasp.json` with your script ID:
```json
{
"scriptId": "your-script-id-here",
"rootDir": "."
}
```

5. Push the code to Google Apps Script:
```bash
yarn push
```

6. Deploy as a web app and note the deployment ID:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cite (link) docs for how to deploy it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added links to Google Apps Script documentation and clasp deployment documentation. Commit ee3ee2e

```bash
yarn deploy
```
[See the clasp deployment documentation](https://developers.google.com/apps-script/guides/clasp#deployments) for more details on deployment options and web app configuration.

## Usage

Once deployed, you can query the modification time of a Google Sheet using:

```bash
curl -s "https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec?sheetId=YOUR_SHEET_ID"
```

This returns a JSON response:
```json
{
"modifiedTime": "2023-12-01T15:30:45.123Z"
}
```

Or an error response:
```json
{
"error": "sheetId parameter required"
}
```

## Integration with Make

Use the provided `example-usage.mk` file as a reference for integrating with Make-based workflows.

## Scripts

- `yarn push` - Push code to Google Apps Script (uploads your local files to the remote project)
- `yarn deploy` - Push and deploy as web app (requires DEPLOYMENT_ID env var) (creates a new web app version that can be accessed via URL)
- `yarn curl` - Test the deployed web app (requires DEPLOYMENT_ID and SHEET_ID env vars)
- `yarn login` - Login to Google Apps Script
- `yarn lint` - Run ESLint to check code style (Airbnb style)
- `yarn lint-fix` - Run ESLint and automatically fix issues

## Testing

The code includes test functions that can be used with the Google Apps Script debugger:

- `_testDoGet()` - Tests the main `doGet` function with various input scenarios
- `_testCreateJsonResponse()` - Tests the JSON response creation function

To run tests in the Google Apps Script editor:
1. Open your script in the Google Apps Script editor
2. Select one of the test functions from the function dropdown
3. Click the run button to execute the test
4. View the output in the console

This approach follows the pattern suggested for testing Google Apps Script functions without requiring external test frameworks.
11 changes: 11 additions & 0 deletions packages/makesheet/appsscript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"webapp": {
"access": "ANYONE_ANONYMOUS",
"executeAs": "USER_DEPLOYING"
},
"timeZone": "America/Chicago",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
46 changes: 46 additions & 0 deletions packages/makesheet/example-usage.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Example usage of makesheet package in Makefile workflows
#
# Set these environment variables:
# DEPLOYMENT_ID = your Google Apps Script deployment ID
# SHEET_ID = the Google Sheet ID you want to track

# Target file to store the sheet's timestamp
SHEET_TIMESTAMP_FILE = sheet.timestamp

# URL for the deployed makesheet web app
SHEET_INFO_URL = https://script.google.com/macros/s/$(DEPLOYMENT_ID)/exec

# Create a timestamp file based on the sheet's last modification time
$(SHEET_TIMESTAMP_FILE):
curl -s "$(SHEET_INFO_URL)?sheetId=$(SHEET_ID)" | \
jq -r '.modifiedTime' | \
xargs -I {} touch -d {} $@

# Example target that depends on the sheet timestamp
data-processing: $(SHEET_TIMESTAMP_FILE)
@echo "Processing data from sheet (last modified: $$(cat $(SHEET_TIMESTAMP_FILE)))"
# Your data processing commands here

# Clean up timestamp file
clean:
rm -f $(SHEET_TIMESTAMP_FILE)

# Check if sheet has been modified since last processing
check-sheet-status: $(SHEET_TIMESTAMP_FILE)
@if [ -f $(SHEET_TIMESTAMP_FILE) ]; then \
echo "Sheet timestamp file exists: $$(ls -la $(SHEET_TIMESTAMP_FILE))"; \
else \
echo "Sheet timestamp file does not exist, will be created"; \
fi

# Alternative approach: always check and update if sheet is newer
update-if-newer:
@CURRENT_TIME=$$(curl -s "$(SHEET_INFO_URL)?sheetId=$(SHEET_ID)" | jq -r '.modifiedTime'); \
if [ ! -f $(SHEET_TIMESTAMP_FILE) ] || [ "$$(date -r $(SHEET_TIMESTAMP_FILE) -u +%Y-%m-%dT%H:%M:%S.%3NZ)" != "$$CURRENT_TIME" ]; then \
echo "Sheet has been modified, updating timestamp"; \
echo "$$CURRENT_TIME" | xargs -I {} touch -d {} $(SHEET_TIMESTAMP_FILE); \
else \
echo "Sheet has not been modified since last check"; \
fi

.PHONY: data-processing clean check-sheet-status update-if-newer
23 changes: 23 additions & 0 deletions packages/makesheet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "makesheet",
"version": "0.1.0",
"description": "Google Sheets timestamp service for Make-based workflows",
"author": "Dan Connolly (with GitHub Copilot)",
"license": "MIT",
"scripts": {
"push": "clasp push",
"deploy": "clasp push; clasp deploy --deploymentId $DEPLOYMENT_ID",
"curl": "curl -L https://script.google.com/macros/s/$DEPLOYMENT_ID/exec?sheetId=$SHEET_ID",
"login": "clasp login",
"lint": "eslint --config .eslintrc.json --no-eslintrc *.js",
"lint-fix": "eslint --config .eslintrc.json --no-eslintrc --fix *.js"
},
"devDependencies": {
"@google/clasp": "^2.3.0",
"@types/google-apps-script": "^1.0.17",
"typescript": "^4.0.5",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.22.1"
}
}
77 changes: 77 additions & 0 deletions packages/makesheet/sheetInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Creates a JSON response for the web app
* @param {Object} data - The data to include in the JSON response
* @returns {GoogleAppsScript.Content.TextOutput} The JSON response
*/
function createJsonResponse(data) {
return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(
ContentService.MimeType.JSON,
);
}

/**
* Main web app handler that returns sheet modification timestamps
* @param {GoogleAppsScript.Events.DoGet} e - The GET request event
* @param {Object} io - Dependency injection object for testing
* @param {Function} io.getFileById - Function to get file by ID (defaults to DriveApp.getFileById)
* @returns {GoogleAppsScript.Content.TextOutput} JSON response with timestamp or error
*/
function doGet(e, io = {}) {
const {
getFileById = DriveApp.getFileById,
} = io;

const { sheetId } = e.parameter;
if (!sheetId) {
return createJsonResponse({ error: 'sheetId parameter required' });
}

try {
const file = getFileById(sheetId);
const modifiedTime = file.getLastUpdated().toISOString();
return createJsonResponse({ modifiedTime });
} catch (error) {
return createJsonResponse({ error: error.toString() });
}
}

// Test functions for use with Google Apps Script debugger
/**
* Test function for doGet with various scenarios
*/
function _testDoGet() {
console.log('Testing doGet with missing sheetId...');
const resultMissing = doGet({ parameter: {} });
console.log('Result:', resultMissing.getContent());

console.log('Testing doGet with invalid sheetId...');
const resultInvalid = doGet({ parameter: { sheetId: 'invalid-id' } });
console.log('Result:', resultInvalid.getContent());

console.log('Testing doGet with mock getFileById...');
const mockFile = {
getLastUpdated: () => new Date('2023-12-01T15:30:45.123Z'),
};
const mockIo = {
getFileById: () => mockFile,
};
const resultMock = doGet({ parameter: { sheetId: 'test-id' } }, mockIo);
console.log('Result:', resultMock.getContent());

// Note: To test with a valid sheet ID, replace 'your-test-sheet-id'
// with an actual Google Sheets ID you have access to
console.log('Testing doGet with valid sheetId (uncomment to test)...');
// const resultValid = doGet({ parameter: { sheetId: 'your-test-sheet-id' } });
// console.log('Result:', resultValid.getContent());
}

/**
* Test function for createJsonResponse
*/
function _testCreateJsonResponse() {
console.log('Testing createJsonResponse...');
const testData = { test: 'value', timestamp: new Date().toISOString() };
const response = createJsonResponse(testData);
console.log('Response content:', response.getContent());
console.log('Response MIME type:', response.getMimeType());
}