@flink-app/google-sheets-plugin 2.0.0-alpha.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/GoogleSheetsClient.d.ts +2 -0
- package/dist/GoogleSheetsClient.js +115 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +52 -0
- package/dist/tools/GoogleSheetsTool.d.ts +8 -0
- package/dist/tools/GoogleSheetsTool.js +82 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/package.json +36 -0
- package/src/GoogleSheetsClient.ts +150 -0
- package/src/index.ts +49 -0
- package/src/tools/GoogleSheetsTool.ts +101 -0
- package/src/types.ts +80 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @flink-app/google-sheets-plugin
|
|
2
|
+
|
|
3
|
+
## 2.0.0-alpha.60
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 831892b: feat: add @flink-app/google-sheets-plugin
|
|
8
|
+
|
|
9
|
+
New plugin for reading and writing Google Sheets from a Flink app using service account authentication.
|
|
10
|
+
|
|
11
|
+
- `googleSheetsPlugin()` factory adds `ctx.plugins.googleSheets` to the app context
|
|
12
|
+
- Scoped sheet API via `.sheet(title)` and `.sheetByIndex(n)` with `getRows`, `appendRow`, `updateRow`, `deleteRow`
|
|
13
|
+
- `getInfo()` for spreadsheet metadata
|
|
14
|
+
- Static credentials via `credentials` option or dynamic loading via `loadCredentials` callback
|
|
15
|
+
- Bundled `GoogleSheetsTool` for use with Flink AI agents
|
|
16
|
+
- Supports raw PEM private keys and base64-encoded service account JSON
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- @flink-app/flink@2.0.0-alpha.60
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Frost Experience AB https://www.frost.se
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @flink-app/google-sheets-plugin
|
|
2
|
+
|
|
3
|
+
Read and write Google Sheets from a Flink app using service account authentication.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
1. Create a service account in [Google Cloud Console](https://console.cloud.google.com/) → IAM & Admin → Service Accounts
|
|
8
|
+
2. Enable the **Google Sheets API** for your project
|
|
9
|
+
3. Share your spreadsheet with the service account email (Editor access)
|
|
10
|
+
|
|
11
|
+
## Register the plugin
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { googleSheetsPlugin, GoogleSheetsPluginCtx } from "@flink-app/google-sheets-plugin";
|
|
15
|
+
|
|
16
|
+
interface AppCtx extends FlinkContext<GoogleSheetsPluginCtx> {
|
|
17
|
+
repos: {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
new FlinkApp<AppCtx>({
|
|
21
|
+
plugins: [
|
|
22
|
+
googleSheetsPlugin({
|
|
23
|
+
spreadsheetId: process.env.GOOGLE_SPREADSHEET_ID!,
|
|
24
|
+
credentials: {
|
|
25
|
+
clientEmail: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL!,
|
|
26
|
+
privateKey: process.env.GOOGLE_PRIVATE_KEY!,
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The private key accepts a raw PEM string (with `\n`) or a base64-encoded service account JSON blob.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Access the API via `ctx.plugins.googleSheets`. Use `.sheet("Title")` or `.sheetByIndex(n)` to scope operations to a specific sheet.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// Read all rows
|
|
41
|
+
const rows = await ctx.plugins.googleSheets.sheet("Tasks").getRows();
|
|
42
|
+
// [{ rowIndex: 0, data: { title: "Fix bug", status: "pending" } }, ...]
|
|
43
|
+
|
|
44
|
+
// Append a row
|
|
45
|
+
const row = await ctx.plugins.googleSheets.sheet("Tasks").appendRow({
|
|
46
|
+
title: "New task",
|
|
47
|
+
status: "pending",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Update a row
|
|
51
|
+
await ctx.plugins.googleSheets.sheet("Tasks").updateRow(row.rowIndex, {
|
|
52
|
+
status: "completed",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Delete a row
|
|
56
|
+
await ctx.plugins.googleSheets.sheet("Tasks").deleteRow(row.rowIndex);
|
|
57
|
+
|
|
58
|
+
// Spreadsheet metadata
|
|
59
|
+
const info = await ctx.plugins.googleSheets.getInfo();
|
|
60
|
+
// { title, spreadsheetId, sheetCount }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Rows are plain objects: `{ rowIndex: number, data: Record<string, string> }` where keys are column headers.
|
|
64
|
+
|
|
65
|
+
## Dynamic credentials
|
|
66
|
+
|
|
67
|
+
Load credentials from the database at startup instead of env vars:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
googleSheetsPlugin({
|
|
71
|
+
loadCredentials: async (ctx) => {
|
|
72
|
+
const config = await ctx.repos.configRepo.getOne({ key: "google" });
|
|
73
|
+
return {
|
|
74
|
+
credentials: {
|
|
75
|
+
clientEmail: config.serviceAccountEmail,
|
|
76
|
+
privateKey: config.privateKey,
|
|
77
|
+
},
|
|
78
|
+
spreadsheetId: config.spreadsheetId,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## AI tool
|
|
85
|
+
|
|
86
|
+
A generic `GoogleSheetsTool` is included for use with Flink agents. It supports `getRows`, `appendRow`, `updateRow`, `deleteRow`, and `getInfo` operations.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import GoogleSheetsTool, { Tool } from "@flink-app/google-sheets-plugin/tools/GoogleSheetsTool";
|
|
90
|
+
|
|
91
|
+
class MyAgent extends FlinkAgent<AppCtx> {
|
|
92
|
+
tools = [{ ...Tool, handler: GoogleSheetsTool }];
|
|
93
|
+
}
|
|
94
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGoogleSheetsApi = createGoogleSheetsApi;
|
|
4
|
+
const google_spreadsheet_1 = require("google-spreadsheet");
|
|
5
|
+
const google_auth_library_1 = require("google-auth-library");
|
|
6
|
+
function parsePrivateKey(raw) {
|
|
7
|
+
// Handle escaped newlines
|
|
8
|
+
let key = raw.replace(/\\n/g, "\n");
|
|
9
|
+
// Handle base64-encoded service account JSON
|
|
10
|
+
if (!key.includes("BEGIN PRIVATE KEY")) {
|
|
11
|
+
try {
|
|
12
|
+
const decoded = Buffer.from(key, "base64").toString("utf-8");
|
|
13
|
+
const parsed = JSON.parse(decoded);
|
|
14
|
+
if (parsed.private_key) {
|
|
15
|
+
key = parsed.private_key;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (_a) {
|
|
19
|
+
// Not base64-encoded JSON, use as-is
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return key;
|
|
23
|
+
}
|
|
24
|
+
function createScopedSheetApi(doc, resolve) {
|
|
25
|
+
return {
|
|
26
|
+
async getRows() {
|
|
27
|
+
const sheet = await resolve();
|
|
28
|
+
const rows = await sheet.getRows();
|
|
29
|
+
return rows.map((row, index) => ({
|
|
30
|
+
rowIndex: index,
|
|
31
|
+
data: Object.fromEntries(sheet.headerValues.map((h) => { var _a; return [h, (_a = row.get(h)) !== null && _a !== void 0 ? _a : ""]; })),
|
|
32
|
+
}));
|
|
33
|
+
},
|
|
34
|
+
async appendRow(data) {
|
|
35
|
+
const sheet = await resolve();
|
|
36
|
+
const row = await sheet.addRow(data);
|
|
37
|
+
return {
|
|
38
|
+
rowIndex: row.rowNumber - 2, // 1-indexed; header is row 1
|
|
39
|
+
data: Object.fromEntries(sheet.headerValues.map((h) => { var _a; return [h, (_a = row.get(h)) !== null && _a !== void 0 ? _a : ""]; })),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
async updateRow(rowIndex, data) {
|
|
43
|
+
const sheet = await resolve();
|
|
44
|
+
const rows = await sheet.getRows();
|
|
45
|
+
if (rowIndex < 0 || rowIndex >= rows.length) {
|
|
46
|
+
throw new Error(`Invalid rowIndex ${rowIndex}. Valid range: 0–${rows.length - 1}`);
|
|
47
|
+
}
|
|
48
|
+
const row = rows[rowIndex];
|
|
49
|
+
for (const [key, value] of Object.entries(data)) {
|
|
50
|
+
row.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
await row.save();
|
|
53
|
+
return {
|
|
54
|
+
rowIndex,
|
|
55
|
+
data: Object.fromEntries(sheet.headerValues.map((h) => { var _a; return [h, (_a = row.get(h)) !== null && _a !== void 0 ? _a : ""]; })),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
async deleteRow(rowIndex) {
|
|
59
|
+
const sheet = await resolve();
|
|
60
|
+
const rows = await sheet.getRows();
|
|
61
|
+
if (rowIndex < 0 || rowIndex >= rows.length) {
|
|
62
|
+
throw new Error(`Invalid rowIndex ${rowIndex}. Valid range: 0–${rows.length - 1}`);
|
|
63
|
+
}
|
|
64
|
+
await rows[rowIndex].delete();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createGoogleSheetsApi(credentials, spreadsheetId) {
|
|
69
|
+
const auth = new google_auth_library_1.JWT({
|
|
70
|
+
email: credentials.clientEmail,
|
|
71
|
+
key: parsePrivateKey(credentials.privateKey),
|
|
72
|
+
scopes: [
|
|
73
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
74
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
const doc = new google_spreadsheet_1.GoogleSpreadsheet(spreadsheetId, auth);
|
|
78
|
+
let initialized = false;
|
|
79
|
+
const ensureInitialized = async () => {
|
|
80
|
+
if (!initialized) {
|
|
81
|
+
await doc.loadInfo();
|
|
82
|
+
initialized = true;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
async getInfo() {
|
|
87
|
+
await ensureInitialized();
|
|
88
|
+
return {
|
|
89
|
+
title: doc.title,
|
|
90
|
+
spreadsheetId,
|
|
91
|
+
sheetCount: doc.sheetCount,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
sheet(title) {
|
|
95
|
+
return createScopedSheetApi(doc, async () => {
|
|
96
|
+
await ensureInitialized();
|
|
97
|
+
const sheet = doc.sheetsByTitle[title];
|
|
98
|
+
if (!sheet) {
|
|
99
|
+
throw new Error(`Sheet "${title}" not found. Available sheets: ${Object.keys(doc.sheetsByTitle).join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
return sheet;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
sheetByIndex(index) {
|
|
105
|
+
return createScopedSheetApi(doc, async () => {
|
|
106
|
+
await ensureInitialized();
|
|
107
|
+
const sheet = doc.sheetsByIndex[index];
|
|
108
|
+
if (!sheet) {
|
|
109
|
+
throw new Error(`Sheet at index ${index} not found. Spreadsheet has ${doc.sheetCount} sheet(s).`);
|
|
110
|
+
}
|
|
111
|
+
return sheet;
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { GoogleSheetsApi, GoogleSheetsPluginOptions } from "./types";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export declare function googleSheetsPlugin<TCtx>(options: GoogleSheetsPluginOptions<TCtx>): {
|
|
4
|
+
id: string;
|
|
5
|
+
readonly ctx: GoogleSheetsApi;
|
|
6
|
+
init: (app: {
|
|
7
|
+
ctx: TCtx;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.googleSheetsPlugin = googleSheetsPlugin;
|
|
18
|
+
const GoogleSheetsClient_1 = require("./GoogleSheetsClient");
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
20
|
+
function googleSheetsPlugin(options) {
|
|
21
|
+
let api;
|
|
22
|
+
return {
|
|
23
|
+
id: "googleSheets",
|
|
24
|
+
get ctx() {
|
|
25
|
+
if (!api) {
|
|
26
|
+
throw new Error("googleSheetsPlugin: plugin not yet initialized. " +
|
|
27
|
+
"Ensure it is passed to FlinkApp before use.");
|
|
28
|
+
}
|
|
29
|
+
return api;
|
|
30
|
+
},
|
|
31
|
+
init: async (app) => {
|
|
32
|
+
let credentials = options.credentials;
|
|
33
|
+
let spreadsheetId = options.spreadsheetId;
|
|
34
|
+
if (options.loadCredentials) {
|
|
35
|
+
const loaded = await options.loadCredentials(app.ctx);
|
|
36
|
+
credentials = loaded.credentials;
|
|
37
|
+
if (loaded.spreadsheetId) {
|
|
38
|
+
spreadsheetId = loaded.spreadsheetId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!credentials) {
|
|
42
|
+
throw new Error("googleSheetsPlugin: no credentials provided. " +
|
|
43
|
+
"Supply `credentials` or `loadCredentials` in the plugin options.");
|
|
44
|
+
}
|
|
45
|
+
if (!spreadsheetId) {
|
|
46
|
+
throw new Error("googleSheetsPlugin: no spreadsheetId provided. " +
|
|
47
|
+
"Supply `spreadsheetId` in the plugin options or return it from `loadCredentials`.");
|
|
48
|
+
}
|
|
49
|
+
api = (0, GoogleSheetsClient_1.createGoogleSheetsApi)(credentials, spreadsheetId);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FlinkContext, FlinkTool, FlinkToolProps } from "@flink-app/flink";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { GoogleSheetsPluginCtx } from "../types";
|
|
4
|
+
export declare const Tool: FlinkToolProps;
|
|
5
|
+
type Input = z.infer<typeof Tool.inputSchema>;
|
|
6
|
+
type Output = z.infer<typeof Tool.outputSchema>;
|
|
7
|
+
declare const GoogleSheetsTool: FlinkTool<FlinkContext<GoogleSheetsPluginCtx>, Input, Output>;
|
|
8
|
+
export default GoogleSheetsTool;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Tool = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const RowDataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.string());
|
|
6
|
+
const RowWithIndexSchema = zod_1.z.object({
|
|
7
|
+
rowIndex: zod_1.z.number(),
|
|
8
|
+
data: RowDataSchema,
|
|
9
|
+
});
|
|
10
|
+
exports.Tool = {
|
|
11
|
+
id: "google-sheets",
|
|
12
|
+
description: "Read and write rows in a Google Sheets spreadsheet. " +
|
|
13
|
+
"Supports getRows, appendRow, updateRow, deleteRow, and getInfo operations. " +
|
|
14
|
+
"Rows are referenced by 0-based rowIndex. " +
|
|
15
|
+
"Data is a key-value map of column header to cell value.",
|
|
16
|
+
inputSchema: zod_1.z.strictObject({
|
|
17
|
+
operation: zod_1.z
|
|
18
|
+
.enum(["getRows", "appendRow", "updateRow", "deleteRow", "getInfo"])
|
|
19
|
+
.describe("Operation to perform"),
|
|
20
|
+
sheet: zod_1.z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Sheet title. Defaults to the first sheet when omitted."),
|
|
24
|
+
rowIndex: zod_1.z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("0-based row index. Required for updateRow and deleteRow."),
|
|
28
|
+
data: RowDataSchema.optional().describe("Row data as column-header → value pairs. Required for appendRow and updateRow."),
|
|
29
|
+
}),
|
|
30
|
+
outputSchema: zod_1.z.object({
|
|
31
|
+
rows: zod_1.z.array(RowWithIndexSchema).optional(),
|
|
32
|
+
row: RowWithIndexSchema.optional(),
|
|
33
|
+
info: zod_1.z
|
|
34
|
+
.object({
|
|
35
|
+
title: zod_1.z.string(),
|
|
36
|
+
spreadsheetId: zod_1.z.string(),
|
|
37
|
+
sheetCount: zod_1.z.number(),
|
|
38
|
+
})
|
|
39
|
+
.optional(),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
const GoogleSheetsTool = async ({ input, ctx, }) => {
|
|
43
|
+
const api = ctx.plugins.googleSheets;
|
|
44
|
+
const sheetApi = input.sheet ? api.sheet(input.sheet) : api.sheetByIndex(0);
|
|
45
|
+
switch (input.operation) {
|
|
46
|
+
case "getInfo": {
|
|
47
|
+
const info = await api.getInfo();
|
|
48
|
+
return { success: true, data: { info } };
|
|
49
|
+
}
|
|
50
|
+
case "getRows": {
|
|
51
|
+
const rows = await sheetApi.getRows();
|
|
52
|
+
return { success: true, data: { rows } };
|
|
53
|
+
}
|
|
54
|
+
case "appendRow": {
|
|
55
|
+
if (!input.data) {
|
|
56
|
+
return { success: false, error: "`data` is required for appendRow" };
|
|
57
|
+
}
|
|
58
|
+
const row = await sheetApi.appendRow(input.data);
|
|
59
|
+
return { success: true, data: { row } };
|
|
60
|
+
}
|
|
61
|
+
case "updateRow": {
|
|
62
|
+
if (input.rowIndex === undefined) {
|
|
63
|
+
return { success: false, error: "`rowIndex` is required for updateRow" };
|
|
64
|
+
}
|
|
65
|
+
if (!input.data) {
|
|
66
|
+
return { success: false, error: "`data` is required for updateRow" };
|
|
67
|
+
}
|
|
68
|
+
const row = await sheetApi.updateRow(input.rowIndex, input.data);
|
|
69
|
+
return { success: true, data: { row } };
|
|
70
|
+
}
|
|
71
|
+
case "deleteRow": {
|
|
72
|
+
if (input.rowIndex === undefined) {
|
|
73
|
+
return { success: false, error: "`rowIndex` is required for deleteRow" };
|
|
74
|
+
}
|
|
75
|
+
await sheetApi.deleteRow(input.rowIndex);
|
|
76
|
+
return { success: true, data: {} };
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
return { success: false, error: `Unknown operation: ${input.operation}` };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
exports.default = GoogleSheetsTool;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface GoogleSheetsCredentials {
|
|
2
|
+
/** Service account email (e.g. my-service@project.iam.gserviceaccount.com) */
|
|
3
|
+
clientEmail: string;
|
|
4
|
+
/**
|
|
5
|
+
* Private key in PEM format or base64-encoded service account JSON.
|
|
6
|
+
* Escaped newlines (\\n) are handled automatically.
|
|
7
|
+
*/
|
|
8
|
+
privateKey: string;
|
|
9
|
+
}
|
|
10
|
+
/** A plain record of column header → cell value */
|
|
11
|
+
export type SheetRow = Record<string, string>;
|
|
12
|
+
/** A row with its 0-based position in the sheet */
|
|
13
|
+
export interface SheetRowWithIndex {
|
|
14
|
+
rowIndex: number;
|
|
15
|
+
data: SheetRow;
|
|
16
|
+
}
|
|
17
|
+
export interface ScopedSheetApi {
|
|
18
|
+
/** Read all data rows from this sheet */
|
|
19
|
+
getRows(): Promise<SheetRowWithIndex[]>;
|
|
20
|
+
/** Append a new row. Returns the created row with its index. */
|
|
21
|
+
appendRow(data: SheetRow): Promise<SheetRowWithIndex>;
|
|
22
|
+
/** Update a row by 0-based rowIndex. Returns the updated row. */
|
|
23
|
+
updateRow(rowIndex: number, data: Partial<SheetRow>): Promise<SheetRowWithIndex>;
|
|
24
|
+
/** Delete a row by 0-based rowIndex. */
|
|
25
|
+
deleteRow(rowIndex: number): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface GoogleSheetsApi {
|
|
28
|
+
/** Get spreadsheet metadata */
|
|
29
|
+
getInfo(): Promise<{
|
|
30
|
+
title: string;
|
|
31
|
+
spreadsheetId: string;
|
|
32
|
+
sheetCount: number;
|
|
33
|
+
}>;
|
|
34
|
+
/** Return a scoped API bound to a specific sheet by title */
|
|
35
|
+
sheet(title: string): ScopedSheetApi;
|
|
36
|
+
/** Return a scoped API bound to a sheet by 0-based index */
|
|
37
|
+
sheetByIndex(index: number): ScopedSheetApi;
|
|
38
|
+
}
|
|
39
|
+
export interface GoogleSheetsPluginOptions<TCtx = unknown> {
|
|
40
|
+
/**
|
|
41
|
+
* Target spreadsheet ID (the long ID from the Google Sheets URL).
|
|
42
|
+
* Can be omitted if `loadCredentials` resolves it dynamically.
|
|
43
|
+
*/
|
|
44
|
+
spreadsheetId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Static service account credentials.
|
|
47
|
+
* Use `loadCredentials` instead when credentials come from the database or
|
|
48
|
+
* secrets manager.
|
|
49
|
+
*/
|
|
50
|
+
credentials?: GoogleSheetsCredentials;
|
|
51
|
+
/**
|
|
52
|
+
* Dynamically load credentials (and optionally spreadsheetId) from the app
|
|
53
|
+
* context at startup. Takes priority over static `credentials`.
|
|
54
|
+
*/
|
|
55
|
+
loadCredentials?: (ctx: TCtx) => Promise<{
|
|
56
|
+
credentials: GoogleSheetsCredentials;
|
|
57
|
+
spreadsheetId?: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
export interface GoogleSheetsPluginCtx {
|
|
61
|
+
googleSheets: GoogleSheetsApi;
|
|
62
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flink-app/google-sheets-plugin",
|
|
3
|
+
"version": "2.0.0-alpha.60",
|
|
4
|
+
"description": "Flink plugin for reading and writing Google Sheets using service account authentication",
|
|
5
|
+
"author": "joel@frost.se",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"default": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"google-auth-library": "^10.5.0",
|
|
20
|
+
"google-spreadsheet": "^5.0.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "22.13.10",
|
|
24
|
+
"tsc-watch": "^4.2.9",
|
|
25
|
+
"rimraf": "^5.0.5",
|
|
26
|
+
"@flink-app/flink": "2.0.0-alpha.60"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@flink-app/flink": ">=2.0.0-alpha.60"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"watch": "tsc-watch --project tsconfig.dist.json",
|
|
33
|
+
"build": "tsc --project tsconfig.dist.json",
|
|
34
|
+
"clean": "rimraf dist"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { GoogleSpreadsheet } from "google-spreadsheet";
|
|
2
|
+
import { JWT } from "google-auth-library";
|
|
3
|
+
import { GoogleSheetsCredentials, GoogleSheetsApi, ScopedSheetApi, SheetRow, SheetRowWithIndex } from "./types";
|
|
4
|
+
|
|
5
|
+
function parsePrivateKey(raw: string): string {
|
|
6
|
+
// Handle escaped newlines
|
|
7
|
+
let key = raw.replace(/\\n/g, "\n");
|
|
8
|
+
|
|
9
|
+
// Handle base64-encoded service account JSON
|
|
10
|
+
if (!key.includes("BEGIN PRIVATE KEY")) {
|
|
11
|
+
try {
|
|
12
|
+
const decoded = Buffer.from(key, "base64").toString("utf-8");
|
|
13
|
+
const parsed = JSON.parse(decoded);
|
|
14
|
+
if (parsed.private_key) {
|
|
15
|
+
key = parsed.private_key;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// Not base64-encoded JSON, use as-is
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return key;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createScopedSheetApi(
|
|
26
|
+
doc: GoogleSpreadsheet,
|
|
27
|
+
resolve: () => Promise<import("google-spreadsheet").GoogleSpreadsheetWorksheet>
|
|
28
|
+
): ScopedSheetApi {
|
|
29
|
+
return {
|
|
30
|
+
async getRows(): Promise<SheetRowWithIndex[]> {
|
|
31
|
+
const sheet = await resolve();
|
|
32
|
+
const rows = await sheet.getRows();
|
|
33
|
+
return rows.map((row, index) => ({
|
|
34
|
+
rowIndex: index,
|
|
35
|
+
data: Object.fromEntries(
|
|
36
|
+
sheet.headerValues.map((h) => [h, row.get(h) ?? ""])
|
|
37
|
+
),
|
|
38
|
+
}));
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async appendRow(data: SheetRow): Promise<SheetRowWithIndex> {
|
|
42
|
+
const sheet = await resolve();
|
|
43
|
+
const row = await sheet.addRow(data);
|
|
44
|
+
return {
|
|
45
|
+
rowIndex: row.rowNumber - 2, // 1-indexed; header is row 1
|
|
46
|
+
data: Object.fromEntries(
|
|
47
|
+
sheet.headerValues.map((h) => [h, row.get(h) ?? ""])
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async updateRow(rowIndex: number, data: Partial<SheetRow>): Promise<SheetRowWithIndex> {
|
|
53
|
+
const sheet = await resolve();
|
|
54
|
+
const rows = await sheet.getRows();
|
|
55
|
+
|
|
56
|
+
if (rowIndex < 0 || rowIndex >= rows.length) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Invalid rowIndex ${rowIndex}. Valid range: 0–${rows.length - 1}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const row = rows[rowIndex];
|
|
63
|
+
for (const [key, value] of Object.entries(data)) {
|
|
64
|
+
row.set(key, value);
|
|
65
|
+
}
|
|
66
|
+
await row.save();
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
rowIndex,
|
|
70
|
+
data: Object.fromEntries(
|
|
71
|
+
sheet.headerValues.map((h) => [h, row.get(h) ?? ""])
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async deleteRow(rowIndex: number): Promise<void> {
|
|
77
|
+
const sheet = await resolve();
|
|
78
|
+
const rows = await sheet.getRows();
|
|
79
|
+
|
|
80
|
+
if (rowIndex < 0 || rowIndex >= rows.length) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Invalid rowIndex ${rowIndex}. Valid range: 0–${rows.length - 1}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await rows[rowIndex].delete();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createGoogleSheetsApi(
|
|
92
|
+
credentials: GoogleSheetsCredentials,
|
|
93
|
+
spreadsheetId: string
|
|
94
|
+
): GoogleSheetsApi {
|
|
95
|
+
const auth = new JWT({
|
|
96
|
+
email: credentials.clientEmail,
|
|
97
|
+
key: parsePrivateKey(credentials.privateKey),
|
|
98
|
+
scopes: [
|
|
99
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
100
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const doc = new GoogleSpreadsheet(spreadsheetId, auth);
|
|
105
|
+
let initialized = false;
|
|
106
|
+
|
|
107
|
+
const ensureInitialized = async () => {
|
|
108
|
+
if (!initialized) {
|
|
109
|
+
await doc.loadInfo();
|
|
110
|
+
initialized = true;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
async getInfo() {
|
|
116
|
+
await ensureInitialized();
|
|
117
|
+
return {
|
|
118
|
+
title: doc.title,
|
|
119
|
+
spreadsheetId,
|
|
120
|
+
sheetCount: doc.sheetCount,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
sheet(title: string): ScopedSheetApi {
|
|
125
|
+
return createScopedSheetApi(doc, async () => {
|
|
126
|
+
await ensureInitialized();
|
|
127
|
+
const sheet = doc.sheetsByTitle[title];
|
|
128
|
+
if (!sheet) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Sheet "${title}" not found. Available sheets: ${Object.keys(doc.sheetsByTitle).join(", ")}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return sheet;
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
sheetByIndex(index: number): ScopedSheetApi {
|
|
138
|
+
return createScopedSheetApi(doc, async () => {
|
|
139
|
+
await ensureInitialized();
|
|
140
|
+
const sheet = doc.sheetsByIndex[index];
|
|
141
|
+
if (!sheet) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Sheet at index ${index} not found. Spreadsheet has ${doc.sheetCount} sheet(s).`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return sheet;
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createGoogleSheetsApi } from "./GoogleSheetsClient";
|
|
2
|
+
import { GoogleSheetsApi, GoogleSheetsPluginOptions } from "./types";
|
|
3
|
+
|
|
4
|
+
export * from "./types";
|
|
5
|
+
|
|
6
|
+
export function googleSheetsPlugin<TCtx>(options: GoogleSheetsPluginOptions<TCtx>) {
|
|
7
|
+
let api: GoogleSheetsApi | undefined;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
id: "googleSheets",
|
|
11
|
+
get ctx(): GoogleSheetsApi {
|
|
12
|
+
if (!api) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"googleSheetsPlugin: plugin not yet initialized. " +
|
|
15
|
+
"Ensure it is passed to FlinkApp before use."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return api;
|
|
19
|
+
},
|
|
20
|
+
init: async (app: { ctx: TCtx }) => {
|
|
21
|
+
let credentials = options.credentials;
|
|
22
|
+
let spreadsheetId = options.spreadsheetId;
|
|
23
|
+
|
|
24
|
+
if (options.loadCredentials) {
|
|
25
|
+
const loaded = await options.loadCredentials(app.ctx);
|
|
26
|
+
credentials = loaded.credentials;
|
|
27
|
+
if (loaded.spreadsheetId) {
|
|
28
|
+
spreadsheetId = loaded.spreadsheetId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!credentials) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"googleSheetsPlugin: no credentials provided. " +
|
|
35
|
+
"Supply `credentials` or `loadCredentials` in the plugin options."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!spreadsheetId) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"googleSheetsPlugin: no spreadsheetId provided. " +
|
|
42
|
+
"Supply `spreadsheetId` in the plugin options or return it from `loadCredentials`."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
api = createGoogleSheetsApi(credentials, spreadsheetId);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { FlinkContext, FlinkTool, FlinkToolProps } from "@flink-app/flink";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { GoogleSheetsPluginCtx } from "../types";
|
|
4
|
+
|
|
5
|
+
const RowDataSchema = z.record(z.string(), z.string());
|
|
6
|
+
|
|
7
|
+
const RowWithIndexSchema = z.object({
|
|
8
|
+
rowIndex: z.number(),
|
|
9
|
+
data: RowDataSchema,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const Tool: FlinkToolProps = {
|
|
13
|
+
id: "google-sheets",
|
|
14
|
+
description:
|
|
15
|
+
"Read and write rows in a Google Sheets spreadsheet. " +
|
|
16
|
+
"Supports getRows, appendRow, updateRow, deleteRow, and getInfo operations. " +
|
|
17
|
+
"Rows are referenced by 0-based rowIndex. " +
|
|
18
|
+
"Data is a key-value map of column header to cell value.",
|
|
19
|
+
inputSchema: z.strictObject({
|
|
20
|
+
operation: z
|
|
21
|
+
.enum(["getRows", "appendRow", "updateRow", "deleteRow", "getInfo"])
|
|
22
|
+
.describe("Operation to perform"),
|
|
23
|
+
sheet: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Sheet title. Defaults to the first sheet when omitted."),
|
|
27
|
+
rowIndex: z
|
|
28
|
+
.number()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("0-based row index. Required for updateRow and deleteRow."),
|
|
31
|
+
data: RowDataSchema.optional().describe(
|
|
32
|
+
"Row data as column-header → value pairs. Required for appendRow and updateRow."
|
|
33
|
+
),
|
|
34
|
+
}),
|
|
35
|
+
outputSchema: z.object({
|
|
36
|
+
rows: z.array(RowWithIndexSchema).optional(),
|
|
37
|
+
row: RowWithIndexSchema.optional(),
|
|
38
|
+
info: z
|
|
39
|
+
.object({
|
|
40
|
+
title: z.string(),
|
|
41
|
+
spreadsheetId: z.string(),
|
|
42
|
+
sheetCount: z.number(),
|
|
43
|
+
})
|
|
44
|
+
.optional(),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type Input = z.infer<typeof Tool.inputSchema>;
|
|
49
|
+
type Output = z.infer<typeof Tool.outputSchema>;
|
|
50
|
+
|
|
51
|
+
const GoogleSheetsTool: FlinkTool<FlinkContext<GoogleSheetsPluginCtx>, Input, Output> = async ({
|
|
52
|
+
input,
|
|
53
|
+
ctx,
|
|
54
|
+
}) => {
|
|
55
|
+
const api = ctx.plugins.googleSheets;
|
|
56
|
+
const sheetApi = input.sheet ? api.sheet(input.sheet) : api.sheetByIndex(0);
|
|
57
|
+
|
|
58
|
+
switch (input.operation) {
|
|
59
|
+
case "getInfo": {
|
|
60
|
+
const info = await api.getInfo();
|
|
61
|
+
return { success: true, data: { info } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "getRows": {
|
|
65
|
+
const rows = await sheetApi.getRows();
|
|
66
|
+
return { success: true, data: { rows } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "appendRow": {
|
|
70
|
+
if (!input.data) {
|
|
71
|
+
return { success: false, error: "`data` is required for appendRow" };
|
|
72
|
+
}
|
|
73
|
+
const row = await sheetApi.appendRow(input.data);
|
|
74
|
+
return { success: true, data: { row } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "updateRow": {
|
|
78
|
+
if (input.rowIndex === undefined) {
|
|
79
|
+
return { success: false, error: "`rowIndex` is required for updateRow" };
|
|
80
|
+
}
|
|
81
|
+
if (!input.data) {
|
|
82
|
+
return { success: false, error: "`data` is required for updateRow" };
|
|
83
|
+
}
|
|
84
|
+
const row = await sheetApi.updateRow(input.rowIndex, input.data);
|
|
85
|
+
return { success: true, data: { row } };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "deleteRow": {
|
|
89
|
+
if (input.rowIndex === undefined) {
|
|
90
|
+
return { success: false, error: "`rowIndex` is required for deleteRow" };
|
|
91
|
+
}
|
|
92
|
+
await sheetApi.deleteRow(input.rowIndex);
|
|
93
|
+
return { success: true, data: {} };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
return { success: false, error: `Unknown operation: ${(input as Input).operation}` };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default GoogleSheetsTool;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { GoogleSpreadsheetRow } from "google-spreadsheet";
|
|
2
|
+
|
|
3
|
+
// ─── Credentials ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface GoogleSheetsCredentials {
|
|
6
|
+
/** Service account email (e.g. my-service@project.iam.gserviceaccount.com) */
|
|
7
|
+
clientEmail: string;
|
|
8
|
+
/**
|
|
9
|
+
* Private key in PEM format or base64-encoded service account JSON.
|
|
10
|
+
* Escaped newlines (\\n) are handled automatically.
|
|
11
|
+
*/
|
|
12
|
+
privateKey: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Row types ────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** A plain record of column header → cell value */
|
|
18
|
+
export type SheetRow = Record<string, string>;
|
|
19
|
+
|
|
20
|
+
/** A row with its 0-based position in the sheet */
|
|
21
|
+
export interface SheetRowWithIndex {
|
|
22
|
+
rowIndex: number;
|
|
23
|
+
data: SheetRow;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Scoped sheet API ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface ScopedSheetApi {
|
|
29
|
+
/** Read all data rows from this sheet */
|
|
30
|
+
getRows(): Promise<SheetRowWithIndex[]>;
|
|
31
|
+
/** Append a new row. Returns the created row with its index. */
|
|
32
|
+
appendRow(data: SheetRow): Promise<SheetRowWithIndex>;
|
|
33
|
+
/** Update a row by 0-based rowIndex. Returns the updated row. */
|
|
34
|
+
updateRow(rowIndex: number, data: Partial<SheetRow>): Promise<SheetRowWithIndex>;
|
|
35
|
+
/** Delete a row by 0-based rowIndex. */
|
|
36
|
+
deleteRow(rowIndex: number): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Top-level API ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export interface GoogleSheetsApi {
|
|
42
|
+
/** Get spreadsheet metadata */
|
|
43
|
+
getInfo(): Promise<{ title: string; spreadsheetId: string; sheetCount: number }>;
|
|
44
|
+
/** Return a scoped API bound to a specific sheet by title */
|
|
45
|
+
sheet(title: string): ScopedSheetApi;
|
|
46
|
+
/** Return a scoped API bound to a sheet by 0-based index */
|
|
47
|
+
sheetByIndex(index: number): ScopedSheetApi;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Plugin options ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export interface GoogleSheetsPluginOptions<TCtx = unknown> {
|
|
53
|
+
/**
|
|
54
|
+
* Target spreadsheet ID (the long ID from the Google Sheets URL).
|
|
55
|
+
* Can be omitted if `loadCredentials` resolves it dynamically.
|
|
56
|
+
*/
|
|
57
|
+
spreadsheetId?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Static service account credentials.
|
|
61
|
+
* Use `loadCredentials` instead when credentials come from the database or
|
|
62
|
+
* secrets manager.
|
|
63
|
+
*/
|
|
64
|
+
credentials?: GoogleSheetsCredentials;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dynamically load credentials (and optionally spreadsheetId) from the app
|
|
68
|
+
* context at startup. Takes priority over static `credentials`.
|
|
69
|
+
*/
|
|
70
|
+
loadCredentials?: (ctx: TCtx) => Promise<{
|
|
71
|
+
credentials: GoogleSheetsCredentials;
|
|
72
|
+
spreadsheetId?: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Context extension ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface GoogleSheetsPluginCtx {
|
|
79
|
+
googleSheets: GoogleSheetsApi;
|
|
80
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2017",
|
|
4
|
+
"lib": ["esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"module": "commonjs",
|
|
12
|
+
"moduleResolution": "node",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": false,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"experimentalDecorators": true,
|
|
18
|
+
"checkJs": false,
|
|
19
|
+
"outDir": "dist",
|
|
20
|
+
"typeRoots": ["./node_modules/@types"]
|
|
21
|
+
},
|
|
22
|
+
"include": ["./src/**/*"],
|
|
23
|
+
"exclude": ["./node_modules/*"]
|
|
24
|
+
}
|