@walkeros/server-store-sheets 4.0.1-next-1778230564486
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/README.md +229 -0
- package/dist/dev.d.mts +57 -0
- package/dist/dev.d.ts +57 -0
- package/dist/dev.js +1 -0
- package/dist/dev.js.map +1 -0
- package/dist/dev.mjs +1 -0
- package/dist/dev.mjs.map +1 -0
- package/dist/index.d.mts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -0
- package/dist/walkerOS.json +146 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @walkeros/server-store-sheets
|
|
2
|
+
|
|
3
|
+
Google Sheets store for walkerOS server flows.
|
|
4
|
+
|
|
5
|
+
Zero runtime dependencies, uses raw `fetch` against the Sheets v4 REST API with
|
|
6
|
+
built-in auth (ADC on Cloud Run / service account JWT elsewhere). Designed for
|
|
7
|
+
demos and small-scale prototyping where the spreadsheet IS the operator-facing
|
|
8
|
+
UI for tweaking lookup data.
|
|
9
|
+
|
|
10
|
+
## Caveats and quotas (read first)
|
|
11
|
+
|
|
12
|
+
The Sheets API is rate-limited and slow. Wiring this store directly into a
|
|
13
|
+
high-throughput pipeline will burn quota in seconds.
|
|
14
|
+
|
|
15
|
+
- **Quota:** 60 read requests / minute / user / project, 60 write requests /
|
|
16
|
+
minute / user / project. Sheets, not BigQuery.
|
|
17
|
+
- **Latency:** 200 to 800 ms per HTTP round-trip.
|
|
18
|
+
- **No internal cache:** the package does NOT cache reads. Cache is the
|
|
19
|
+
consumer's job, see "Wiring with the core cache" below.
|
|
20
|
+
- **Concurrency:** last writer wins on the same cell. There is no transactional
|
|
21
|
+
`getAndSet`, two pipeline instances writing the same key WILL race.
|
|
22
|
+
- **Single-writer model:** if two pipeline instances both write to the same
|
|
23
|
+
sheet, their `keyToRow` indexes diverge. Run with one writer.
|
|
24
|
+
- **Demo and small-prototype grade only.** Not a production CRM substitute.
|
|
25
|
+
|
|
26
|
+
## Quick Start (Bundled Mode)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"version": 4,
|
|
31
|
+
"flows": {
|
|
32
|
+
"default": {
|
|
33
|
+
"config": { "platform": "server" },
|
|
34
|
+
"stores": {
|
|
35
|
+
"crm": {
|
|
36
|
+
"package": "@walkeros/server-store-sheets",
|
|
37
|
+
"config": {
|
|
38
|
+
"settings": {
|
|
39
|
+
"id": "1AbCdEfGhIjKlMnOpQrStUvWxYz",
|
|
40
|
+
"sheet": "Customers"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Wiring with the core cache
|
|
51
|
+
|
|
52
|
+
To absorb the Sheets quota, wire the consuming transformer or destination with
|
|
53
|
+
the core `Cache` config, backed by a fast in-memory store:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"version": 4,
|
|
58
|
+
"flows": {
|
|
59
|
+
"default": {
|
|
60
|
+
"config": { "platform": "server" },
|
|
61
|
+
"stores": {
|
|
62
|
+
"crm": {
|
|
63
|
+
"package": "@walkeros/server-store-sheets",
|
|
64
|
+
"config": {
|
|
65
|
+
"settings": {
|
|
66
|
+
"id": "1AbCdEfGhIjKlMnOpQrStUvWxYz",
|
|
67
|
+
"sheet": "Customers"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"lookupCache": {
|
|
72
|
+
"package": "@walkeros/store-memory",
|
|
73
|
+
"config": { "settings": {} }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"transformers": {
|
|
77
|
+
"enrich": {
|
|
78
|
+
"package": "@walkeros/transformer-enrich",
|
|
79
|
+
"env": {
|
|
80
|
+
"store": "$store.crm",
|
|
81
|
+
"cache": "$store.lookupCache"
|
|
82
|
+
},
|
|
83
|
+
"config": {
|
|
84
|
+
"settings": {
|
|
85
|
+
"cache": { "ttl": 60000 }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use `ttl: 5000` for demos (fast iteration), `ttl: 60000` or higher for
|
|
96
|
+
production-ish loads. Without a cache, every event hits Sheets directly and
|
|
97
|
+
trips the 60 req/min quota in under one second.
|
|
98
|
+
|
|
99
|
+
## Integrated Mode
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { storeSheetsInit } from '@walkeros/server-store-sheets';
|
|
103
|
+
|
|
104
|
+
const store = await storeSheetsInit({
|
|
105
|
+
collector,
|
|
106
|
+
logger,
|
|
107
|
+
id: 'crm',
|
|
108
|
+
config: {
|
|
109
|
+
settings: {
|
|
110
|
+
id: '1AbCdEfGhIjKlMnOpQrStUvWxYz',
|
|
111
|
+
sheet: 'Customers',
|
|
112
|
+
// Omit credentials for ADC on Cloud Run / GKE,
|
|
113
|
+
// or pass SA JSON: credentials: process.env.SHEETS_SA_KEY
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
env: {},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const customer = await store.get('alice'); // unknown (parsed JSON) | undefined
|
|
120
|
+
await store.set('bob', { tier: 'silver' });
|
|
121
|
+
await store.delete('charlie');
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Each value is JSON-stringified into one cell (the `value` column). Reads
|
|
125
|
+
JSON-parse the cell back. A non-parseable cell logs a debug line and returns
|
|
126
|
+
`undefined`.
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
| Setting | Type | Required | Default | Description |
|
|
131
|
+
| ------------- | ------------------ | -------- | ---------- | --------------------------------------------------------- |
|
|
132
|
+
| `id` | `string` | Yes | (none) | Spreadsheet ID, the segment between `/d/` and `/edit` |
|
|
133
|
+
| `sheet` | `string` | No | `'Sheet1'` | Sheet (tab) name within the spreadsheet |
|
|
134
|
+
| `key` | `string` | No | `'A'` | Column letter for keys (the lookup column) |
|
|
135
|
+
| `value` | `string` | No | `'B'` | Column letter for values (JSON-serialized blob) |
|
|
136
|
+
| `headerRows` | `number` | No | `1` | Number of header rows to skip when reading the key column |
|
|
137
|
+
| `credentials` | `string \| object` | No | ADC | Service account JSON or string. Omit on GCP for ADC |
|
|
138
|
+
|
|
139
|
+
## Provisioning
|
|
140
|
+
|
|
141
|
+
The package ships an idempotent `setup()` lifecycle, invoked only by the
|
|
142
|
+
explicit operator command:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
walkeros setup store.<id>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
It never runs automatically. It verifies the spreadsheet exists and (if
|
|
149
|
+
configured) writes the `setup.headers` row.
|
|
150
|
+
|
|
151
|
+
### `Setup` options
|
|
152
|
+
|
|
153
|
+
| Option | Type | Default | Notes |
|
|
154
|
+
| --------- | ---------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
155
|
+
| `headers` | `string[]` | (none) | Header values written to row 1 of the configured sheet. Idempotent overwrite, no drift detect. |
|
|
156
|
+
|
|
157
|
+
`id` is taken from `settings.id` and is NOT duplicated under `setup`.
|
|
158
|
+
|
|
159
|
+
### Behavior
|
|
160
|
+
|
|
161
|
+
- **Existence probe:** setup issues
|
|
162
|
+
`GET /spreadsheets/<id>?fields=spreadsheetId` and throws an actionable error
|
|
163
|
+
on 404.
|
|
164
|
+
- **Header write:** when `setup.headers` is provided, setup issues
|
|
165
|
+
`PUT /values/<sheet>!A1:<lastCol>1?valueInputOption=RAW` with the headers as
|
|
166
|
+
the row values. Re-running with the same headers is a no-op overwrite.
|
|
167
|
+
- **No `shareWith`:** Drive API integration is intentionally out of scope in
|
|
168
|
+
this version (it requires a separate OAuth scope). Share the spreadsheet
|
|
169
|
+
manually with the service account email before running setup.
|
|
170
|
+
|
|
171
|
+
### Runtime hard-fail
|
|
172
|
+
|
|
173
|
+
The first call to `init()` issues a single
|
|
174
|
+
`GET /spreadsheets/<id>?fields=spreadsheetId` per process per spreadsheet ID. On
|
|
175
|
+
404 it throws:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Spreadsheet not found: <id>. Run "walkeros setup store.<id>" to ensure the sheet exists and is shared with the service account.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Operators see the error pointing at the exact command to fix it. Subsequent
|
|
182
|
+
calls in the same process skip the check via an in-memory cache.
|
|
183
|
+
|
|
184
|
+
## Authentication
|
|
185
|
+
|
|
186
|
+
### Cloud Run / GKE (ADC)
|
|
187
|
+
|
|
188
|
+
When running on GCP infrastructure, omit `credentials`. The store fetches tokens
|
|
189
|
+
from the metadata server automatically. Required OAuth scope:
|
|
190
|
+
`https://www.googleapis.com/auth/spreadsheets`.
|
|
191
|
+
|
|
192
|
+
### Non-GCP (Service Account)
|
|
193
|
+
|
|
194
|
+
Pass a service account JSON as a string (from `$env.SHEETS_SA_KEY`) or as an
|
|
195
|
+
object with `client_email` and `private_key` fields. The store signs JWTs
|
|
196
|
+
locally and exchanges them for access tokens.
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"credentials": "$env.SHEETS_SA_KEY"
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The service account email needs Editor (or at least Viewer plus the explicit
|
|
205
|
+
`setup.headers` cells) access to the spreadsheet. Share the sheet with the
|
|
206
|
+
service account email before running setup.
|
|
207
|
+
|
|
208
|
+
## API
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const value = await store.get('alice'); // unknown | undefined
|
|
212
|
+
await store.set('bob', { tier: 'silver' }); // void
|
|
213
|
+
await store.delete('charlie'); // void
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`get()` returns the JSON-parsed value, or `undefined` if the key is unknown, the
|
|
217
|
+
value cell is empty, or the cell is not valid JSON. `set()` for an unknown key
|
|
218
|
+
appends a new row, capturing the row index from the API response. `set()` for a
|
|
219
|
+
known key updates the existing value cell. `delete()` blanks the value cell, the
|
|
220
|
+
row stays in place to keep `keyToRow` indexes stable.
|
|
221
|
+
|
|
222
|
+
## Limitations
|
|
223
|
+
|
|
224
|
+
- **Single-cell value shape.** Multi-column structured rows are out of scope,
|
|
225
|
+
ship a richer schema in a later phase if customers ask.
|
|
226
|
+
- **No drift detection on header content.** If an operator manually edits row 1,
|
|
227
|
+
the next `walkeros setup store.<id>` overwrites it without warning.
|
|
228
|
+
- **No transactional updates.** `set` is two HTTP calls (read index, write
|
|
229
|
+
cell). Concurrent writers can interleave.
|
package/dist/dev.d.mts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as _walkeros_core_dev from '@walkeros/core/dev';
|
|
2
|
+
import { z } from '@walkeros/core/dev';
|
|
3
|
+
import { Flow, Store } from '@walkeros/core';
|
|
4
|
+
|
|
5
|
+
declare const SettingsSchema: z.ZodObject<{
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
sheet: z.ZodDefault<z.ZodString>;
|
|
8
|
+
key: z.ZodDefault<z.ZodString>;
|
|
9
|
+
value: z.ZodDefault<z.ZodString>;
|
|
10
|
+
headerRows: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
credentials: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
12
|
+
client_email: z.ZodString;
|
|
13
|
+
private_key: z.ZodString;
|
|
14
|
+
}, z.core.$strip>]>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
type Settings = z.infer<typeof SettingsSchema>;
|
|
17
|
+
|
|
18
|
+
declare const SetupSchema: z.ZodObject<{
|
|
19
|
+
headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
|
|
22
|
+
declare const settings: _walkeros_core_dev.JSONSchema;
|
|
23
|
+
declare const setup: _walkeros_core_dev.JSONSchema;
|
|
24
|
+
|
|
25
|
+
type index$1_Settings = Settings;
|
|
26
|
+
declare const index$1_SettingsSchema: typeof SettingsSchema;
|
|
27
|
+
declare const index$1_SetupSchema: typeof SetupSchema;
|
|
28
|
+
declare const index$1_settings: typeof settings;
|
|
29
|
+
declare const index$1_setup: typeof setup;
|
|
30
|
+
declare namespace index$1 {
|
|
31
|
+
export { type index$1_Settings as Settings, index$1_SettingsSchema as SettingsSchema, index$1_SetupSchema as SetupSchema, index$1_settings as settings, index$1_setup as setup };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read a JSON value with ADC (Cloud Run / GKE service account). */
|
|
35
|
+
declare const readWithAdc: Flow.StepExample;
|
|
36
|
+
/** Write a JSON value with an explicit service account JSON. */
|
|
37
|
+
declare const writeWithServiceAccount: Flow.StepExample;
|
|
38
|
+
|
|
39
|
+
declare const step_readWithAdc: typeof readWithAdc;
|
|
40
|
+
declare const step_writeWithServiceAccount: typeof writeWithServiceAccount;
|
|
41
|
+
declare namespace step {
|
|
42
|
+
export { step_readWithAdc as readWithAdc, step_writeWithServiceAccount as writeWithServiceAccount };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cloud Run with ADC, no credentials field needed. */
|
|
46
|
+
declare const cloudRunAdc: Store.Config;
|
|
47
|
+
/** Explicit service account JSON for non-GCP environments. */
|
|
48
|
+
declare const serviceAccount: Store.Config;
|
|
49
|
+
|
|
50
|
+
declare const index_cloudRunAdc: typeof cloudRunAdc;
|
|
51
|
+
declare const index_serviceAccount: typeof serviceAccount;
|
|
52
|
+
declare const index_step: typeof step;
|
|
53
|
+
declare namespace index {
|
|
54
|
+
export { index_cloudRunAdc as cloudRunAdc, index_serviceAccount as serviceAccount, index_step as step };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { index as examples, index$1 as schemas };
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as _walkeros_core_dev from '@walkeros/core/dev';
|
|
2
|
+
import { z } from '@walkeros/core/dev';
|
|
3
|
+
import { Flow, Store } from '@walkeros/core';
|
|
4
|
+
|
|
5
|
+
declare const SettingsSchema: z.ZodObject<{
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
sheet: z.ZodDefault<z.ZodString>;
|
|
8
|
+
key: z.ZodDefault<z.ZodString>;
|
|
9
|
+
value: z.ZodDefault<z.ZodString>;
|
|
10
|
+
headerRows: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
credentials: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
12
|
+
client_email: z.ZodString;
|
|
13
|
+
private_key: z.ZodString;
|
|
14
|
+
}, z.core.$strip>]>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
type Settings = z.infer<typeof SettingsSchema>;
|
|
17
|
+
|
|
18
|
+
declare const SetupSchema: z.ZodObject<{
|
|
19
|
+
headers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
|
|
22
|
+
declare const settings: _walkeros_core_dev.JSONSchema;
|
|
23
|
+
declare const setup: _walkeros_core_dev.JSONSchema;
|
|
24
|
+
|
|
25
|
+
type index$1_Settings = Settings;
|
|
26
|
+
declare const index$1_SettingsSchema: typeof SettingsSchema;
|
|
27
|
+
declare const index$1_SetupSchema: typeof SetupSchema;
|
|
28
|
+
declare const index$1_settings: typeof settings;
|
|
29
|
+
declare const index$1_setup: typeof setup;
|
|
30
|
+
declare namespace index$1 {
|
|
31
|
+
export { type index$1_Settings as Settings, index$1_SettingsSchema as SettingsSchema, index$1_SetupSchema as SetupSchema, index$1_settings as settings, index$1_setup as setup };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read a JSON value with ADC (Cloud Run / GKE service account). */
|
|
35
|
+
declare const readWithAdc: Flow.StepExample;
|
|
36
|
+
/** Write a JSON value with an explicit service account JSON. */
|
|
37
|
+
declare const writeWithServiceAccount: Flow.StepExample;
|
|
38
|
+
|
|
39
|
+
declare const step_readWithAdc: typeof readWithAdc;
|
|
40
|
+
declare const step_writeWithServiceAccount: typeof writeWithServiceAccount;
|
|
41
|
+
declare namespace step {
|
|
42
|
+
export { step_readWithAdc as readWithAdc, step_writeWithServiceAccount as writeWithServiceAccount };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cloud Run with ADC, no credentials field needed. */
|
|
46
|
+
declare const cloudRunAdc: Store.Config;
|
|
47
|
+
/** Explicit service account JSON for non-GCP environments. */
|
|
48
|
+
declare const serviceAccount: Store.Config;
|
|
49
|
+
|
|
50
|
+
declare const index_cloudRunAdc: typeof cloudRunAdc;
|
|
51
|
+
declare const index_serviceAccount: typeof serviceAccount;
|
|
52
|
+
declare const index_step: typeof step;
|
|
53
|
+
declare namespace index {
|
|
54
|
+
export { index_cloudRunAdc as cloudRunAdc, index_serviceAccount as serviceAccount, index_step as step };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { index as examples, index$1 as schemas };
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,o=Object.prototype.hasOwnProperty,n=(e,r)=>{for(var i in r)t(e,i,{get:r[i],enumerable:!0})},s={};n(s,{examples:()=>v,schemas:()=>a}),module.exports=(e=s,((e,n,s,a)=>{if(n&&"object"==typeof n||"function"==typeof n)for(let c of i(n))o.call(e,c)||c===s||t(e,c,{get:()=>n[c],enumerable:!(a=r(n,c))||a.enumerable});return e})(t({},"__esModule",{value:!0}),e));var a={};n(a,{SettingsSchema:()=>l,SetupSchema:()=>h,settings:()=>p,setup:()=>b});var c=require("@walkeros/core/dev"),d=require("@walkeros/core/dev"),l=d.z.object({id:d.z.string().min(1).describe("Spreadsheet ID, the segment between /d/ and /edit in the URL"),sheet:d.z.string().default("Sheet1").describe("Sheet (tab) name within the spreadsheet"),key:d.z.string().default("A").describe("Column letter for keys (the lookup column)"),value:d.z.string().default("B").describe("Column letter for values (JSON-serialized blob)"),headerRows:d.z.number().int().min(0).default(1).describe("Number of header rows to skip when reading the key column"),credentials:d.z.union([d.z.string(),d.z.object({client_email:d.z.string().describe("Service account email"),private_key:d.z.string().describe("RSA private key in PEM format")})]).optional().describe("Service account JSON (string or object). Omit for ADC on Cloud Run/GKE")}),u=require("@walkeros/core/dev"),h=u.z.object({headers:u.z.array(u.z.string()).optional().describe("Header values to write into row 1 of the configured sheet")}).describe('Provisioning options for "walkeros setup store.<id>". Idempotent: re-running with the same headers is a no-op overwrite, never alters existing data rows.'),p=(0,c.zodToSchema)(l),b=(0,c.zodToSchema)(h),v={};n(v,{cloudRunAdc:()=>S,serviceAccount:()=>w,step:()=>m});var m={};n(m,{readWithAdc:()=>g,writeWithServiceAccount:()=>f});var g={title:"Read with ADC",description:"Read a value from the Sheets store using ADC, no credentials field needed on Cloud Run or GKE",in:{operation:"get",key:"alice"},out:[["get","alice",'{ tier: "gold" }']]},f={title:"Write with service account",description:"Append or update a JSON value in the configured sheet using an explicit service account JSON",in:{operation:"set",key:"bob",value:{tier:"silver"}},out:[["set","bob",'{ tier: "silver" }']]},S={settings:{id:"1AbCdEfGhIjKlMnOpQrStUvWxYz",sheet:"Customers"}},w={settings:{id:"1AbCdEfGhIjKlMnOpQrStUvWxYz",sheet:"Customers",credentials:"$env.SHEETS_SA_KEY"}};//# sourceMappingURL=dev.js.map
|
package/dist/dev.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dev.ts","../src/schemas/index.ts","../src/schemas/settings.ts","../src/schemas/setup.ts","../src/examples/index.ts","../src/examples/step.ts"],"sourcesContent":["export * as schemas from './schemas';\nexport * as examples from './examples';\n","import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\nimport { SetupSchema } from './setup';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport { SetupSchema } from './setup';\n\nexport const settings = zodToSchema(SettingsSchema);\nexport const setup = zodToSchema(SetupSchema);\n","import { z } from '@walkeros/core/dev';\n\nexport const SettingsSchema = z.object({\n id: z\n .string()\n .min(1)\n .describe('Spreadsheet ID, the segment between /d/ and /edit in the URL'),\n sheet: z\n .string()\n .default('Sheet1')\n .describe('Sheet (tab) name within the spreadsheet'),\n key: z\n .string()\n .default('A')\n .describe('Column letter for keys (the lookup column)'),\n value: z\n .string()\n .default('B')\n .describe('Column letter for values (JSON-serialized blob)'),\n headerRows: z\n .number()\n .int()\n .min(0)\n .default(1)\n .describe('Number of header rows to skip when reading the key column'),\n credentials: z\n .union([\n z.string(),\n z.object({\n client_email: z.string().describe('Service account email'),\n private_key: z.string().describe('RSA private key in PEM format'),\n }),\n ])\n .optional()\n .describe(\n 'Service account JSON (string or object). Omit for ADC on Cloud Run/GKE',\n ),\n});\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","import { z } from '@walkeros/core/dev';\n\nexport const SetupSchema = z\n .object({\n headers: z\n .array(z.string())\n .optional()\n .describe('Header values to write into row 1 of the configured sheet'),\n })\n .describe(\n 'Provisioning options for \"walkeros setup store.<id>\". Idempotent: re-running with the same headers is a no-op overwrite, never alters existing data rows.',\n );\n\nexport type Setup = z.infer<typeof SetupSchema>;\n","import type { Store } from '@walkeros/core';\n\n/** Cloud Run with ADC, no credentials field needed. */\nexport const cloudRunAdc: Store.Config = {\n settings: {\n id: '1AbCdEfGhIjKlMnOpQrStUvWxYz',\n sheet: 'Customers',\n },\n};\n\n/** Explicit service account JSON for non-GCP environments. */\nexport const serviceAccount: Store.Config = {\n settings: {\n id: '1AbCdEfGhIjKlMnOpQrStUvWxYz',\n sheet: 'Customers',\n credentials: '$env.SHEETS_SA_KEY',\n },\n};\n\nexport * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\n/** Read a JSON value with ADC (Cloud Run / GKE service account). */\nexport const readWithAdc: Flow.StepExample = {\n title: 'Read with ADC',\n description:\n 'Read a value from the Sheets store using ADC, no credentials field needed on Cloud Run or GKE',\n in: { operation: 'get', key: 'alice' },\n out: [['get', 'alice', '{ tier: \"gold\" }']],\n};\n\n/** Write a JSON value with an explicit service account JSON. */\nexport const writeWithServiceAccount: Flow.StepExample = {\n title: 'Write with service account',\n description:\n 'Append or update a JSON value in the configured sheet using an explicit service account JSON',\n in: { operation: 'set', key: 'bob', value: { tier: 'silver' } },\n out: [['set', 'bob', '{ tier: \"silver\" }']],\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,cAA4B;;;ACA5B,iBAAkB;AAEX,IAAM,iBAAiB,aAAE,OAAO;AAAA,EACrC,IAAI,aACD,OAAO,EACP,IAAI,CAAC,EACL,SAAS,8DAA8D;AAAA,EAC1E,OAAO,aACJ,OAAO,EACP,QAAQ,QAAQ,EAChB,SAAS,yCAAyC;AAAA,EACrD,KAAK,aACF,OAAO,EACP,QAAQ,GAAG,EACX,SAAS,4CAA4C;AAAA,EACxD,OAAO,aACJ,OAAO,EACP,QAAQ,GAAG,EACX,SAAS,iDAAiD;AAAA,EAC7D,YAAY,aACT,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,QAAQ,CAAC,EACT,SAAS,2DAA2D;AAAA,EACvE,aAAa,aACV,MAAM;AAAA,IACL,aAAE,OAAO;AAAA,IACT,aAAE,OAAO;AAAA,MACP,cAAc,aAAE,OAAO,EAAE,SAAS,uBAAuB;AAAA,MACzD,aAAa,aAAE,OAAO,EAAE,SAAS,+BAA+B;AAAA,IAClE,CAAC;AAAA,EACH,CAAC,EACA,SAAS,EACT;AAAA,IACC;AAAA,EACF;AACJ,CAAC;;;ACrCD,IAAAC,cAAkB;AAEX,IAAM,cAAc,cACxB,OAAO;AAAA,EACN,SAAS,cACN,MAAM,cAAE,OAAO,CAAC,EAChB,SAAS,EACT,SAAS,2DAA2D;AACzE,CAAC,EACA;AAAA,EACC;AACF;;;AFJK,IAAM,eAAW,yBAAY,cAAc;AAC3C,IAAM,YAAQ,yBAAY,WAAW;;;AGR5C;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAGO,IAAM,cAAgC;AAAA,EAC3C,OAAO;AAAA,EACP,aACE;AAAA,EACF,IAAI,EAAE,WAAW,OAAO,KAAK,QAAQ;AAAA,EACrC,KAAK,CAAC,CAAC,OAAO,SAAS,kBAAkB,CAAC;AAC5C;AAGO,IAAM,0BAA4C;AAAA,EACvD,OAAO;AAAA,EACP,aACE;AAAA,EACF,IAAI,EAAE,WAAW,OAAO,KAAK,OAAO,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EAC9D,KAAK,CAAC,CAAC,OAAO,OAAO,oBAAoB,CAAC;AAC5C;;;ADfO,IAAM,cAA4B;AAAA,EACvC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,EACT;AACF;AAGO,IAAM,iBAA+B;AAAA,EAC1C,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,EACf;AACF;","names":["import_dev","import_dev"]}
|
package/dist/dev.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,t=(t,r)=>{for(var i in r)e(t,i,{get:r[i],enumerable:!0})},r={};t(r,{SettingsSchema:()=>s,SetupSchema:()=>a,settings:()=>d,setup:()=>c});import{zodToSchema as i}from"@walkeros/core/dev";import{z as o}from"@walkeros/core/dev";var s=o.object({id:o.string().min(1).describe("Spreadsheet ID, the segment between /d/ and /edit in the URL"),sheet:o.string().default("Sheet1").describe("Sheet (tab) name within the spreadsheet"),key:o.string().default("A").describe("Column letter for keys (the lookup column)"),value:o.string().default("B").describe("Column letter for values (JSON-serialized blob)"),headerRows:o.number().int().min(0).default(1).describe("Number of header rows to skip when reading the key column"),credentials:o.union([o.string(),o.object({client_email:o.string().describe("Service account email"),private_key:o.string().describe("RSA private key in PEM format")})]).optional().describe("Service account JSON (string or object). Omit for ADC on Cloud Run/GKE")});import{z as n}from"@walkeros/core/dev";var a=n.object({headers:n.array(n.string()).optional().describe("Header values to write into row 1 of the configured sheet")}).describe('Provisioning options for "walkeros setup store.<id>". Idempotent: re-running with the same headers is a no-op overwrite, never alters existing data rows.'),d=i(s),c=i(a),l={};t(l,{cloudRunAdc:()=>v,serviceAccount:()=>m,step:()=>u});var u={};t(u,{readWithAdc:()=>h,writeWithServiceAccount:()=>p});var h={title:"Read with ADC",description:"Read a value from the Sheets store using ADC, no credentials field needed on Cloud Run or GKE",in:{operation:"get",key:"alice"},out:[["get","alice",'{ tier: "gold" }']]},p={title:"Write with service account",description:"Append or update a JSON value in the configured sheet using an explicit service account JSON",in:{operation:"set",key:"bob",value:{tier:"silver"}},out:[["set","bob",'{ tier: "silver" }']]},v={settings:{id:"1AbCdEfGhIjKlMnOpQrStUvWxYz",sheet:"Customers"}},m={settings:{id:"1AbCdEfGhIjKlMnOpQrStUvWxYz",sheet:"Customers",credentials:"$env.SHEETS_SA_KEY"}};export{l as examples,r as schemas};//# sourceMappingURL=dev.mjs.map
|
package/dist/dev.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas/index.ts","../src/schemas/settings.ts","../src/schemas/setup.ts","../src/examples/index.ts","../src/examples/step.ts"],"sourcesContent":["import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\nimport { SetupSchema } from './setup';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport { SetupSchema } from './setup';\n\nexport const settings = zodToSchema(SettingsSchema);\nexport const setup = zodToSchema(SetupSchema);\n","import { z } from '@walkeros/core/dev';\n\nexport const SettingsSchema = z.object({\n id: z\n .string()\n .min(1)\n .describe('Spreadsheet ID, the segment between /d/ and /edit in the URL'),\n sheet: z\n .string()\n .default('Sheet1')\n .describe('Sheet (tab) name within the spreadsheet'),\n key: z\n .string()\n .default('A')\n .describe('Column letter for keys (the lookup column)'),\n value: z\n .string()\n .default('B')\n .describe('Column letter for values (JSON-serialized blob)'),\n headerRows: z\n .number()\n .int()\n .min(0)\n .default(1)\n .describe('Number of header rows to skip when reading the key column'),\n credentials: z\n .union([\n z.string(),\n z.object({\n client_email: z.string().describe('Service account email'),\n private_key: z.string().describe('RSA private key in PEM format'),\n }),\n ])\n .optional()\n .describe(\n 'Service account JSON (string or object). Omit for ADC on Cloud Run/GKE',\n ),\n});\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","import { z } from '@walkeros/core/dev';\n\nexport const SetupSchema = z\n .object({\n headers: z\n .array(z.string())\n .optional()\n .describe('Header values to write into row 1 of the configured sheet'),\n })\n .describe(\n 'Provisioning options for \"walkeros setup store.<id>\". Idempotent: re-running with the same headers is a no-op overwrite, never alters existing data rows.',\n );\n\nexport type Setup = z.infer<typeof SetupSchema>;\n","import type { Store } from '@walkeros/core';\n\n/** Cloud Run with ADC, no credentials field needed. */\nexport const cloudRunAdc: Store.Config = {\n settings: {\n id: '1AbCdEfGhIjKlMnOpQrStUvWxYz',\n sheet: 'Customers',\n },\n};\n\n/** Explicit service account JSON for non-GCP environments. */\nexport const serviceAccount: Store.Config = {\n settings: {\n id: '1AbCdEfGhIjKlMnOpQrStUvWxYz',\n sheet: 'Customers',\n credentials: '$env.SHEETS_SA_KEY',\n },\n};\n\nexport * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\n/** Read a JSON value with ADC (Cloud Run / GKE service account). */\nexport const readWithAdc: Flow.StepExample = {\n title: 'Read with ADC',\n description:\n 'Read a value from the Sheets store using ADC, no credentials field needed on Cloud Run or GKE',\n in: { operation: 'get', key: 'alice' },\n out: [['get', 'alice', '{ tier: \"gold\" }']],\n};\n\n/** Write a JSON value with an explicit service account JSON. */\nexport const writeWithServiceAccount: Flow.StepExample = {\n title: 'Write with service account',\n description:\n 'Append or update a JSON value in the configured sheet using an explicit service account JSON',\n in: { operation: 'set', key: 'bob', value: { tier: 'silver' } },\n out: [['set', 'bob', '{ tier: \"silver\" }']],\n};\n"],"mappings":";;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,mBAAmB;;;ACA5B,SAAS,SAAS;AAEX,IAAM,iBAAiB,EAAE,OAAO;AAAA,EACrC,IAAI,EACD,OAAO,EACP,IAAI,CAAC,EACL,SAAS,8DAA8D;AAAA,EAC1E,OAAO,EACJ,OAAO,EACP,QAAQ,QAAQ,EAChB,SAAS,yCAAyC;AAAA,EACrD,KAAK,EACF,OAAO,EACP,QAAQ,GAAG,EACX,SAAS,4CAA4C;AAAA,EACxD,OAAO,EACJ,OAAO,EACP,QAAQ,GAAG,EACX,SAAS,iDAAiD;AAAA,EAC7D,YAAY,EACT,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,QAAQ,CAAC,EACT,SAAS,2DAA2D;AAAA,EACvE,aAAa,EACV,MAAM;AAAA,IACL,EAAE,OAAO;AAAA,IACT,EAAE,OAAO;AAAA,MACP,cAAc,EAAE,OAAO,EAAE,SAAS,uBAAuB;AAAA,MACzD,aAAa,EAAE,OAAO,EAAE,SAAS,+BAA+B;AAAA,IAClE,CAAC;AAAA,EACH,CAAC,EACA,SAAS,EACT;AAAA,IACC;AAAA,EACF;AACJ,CAAC;;;ACrCD,SAAS,KAAAA,UAAS;AAEX,IAAM,cAAcA,GACxB,OAAO;AAAA,EACN,SAASA,GACN,MAAMA,GAAE,OAAO,CAAC,EAChB,SAAS,EACT,SAAS,2DAA2D;AACzE,CAAC,EACA;AAAA,EACC;AACF;;;AFJK,IAAM,WAAW,YAAY,cAAc;AAC3C,IAAM,QAAQ,YAAY,WAAW;;;AGR5C;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAAA;AAGO,IAAM,cAAgC;AAAA,EAC3C,OAAO;AAAA,EACP,aACE;AAAA,EACF,IAAI,EAAE,WAAW,OAAO,KAAK,QAAQ;AAAA,EACrC,KAAK,CAAC,CAAC,OAAO,SAAS,kBAAkB,CAAC;AAC5C;AAGO,IAAM,0BAA4C;AAAA,EACvD,OAAO;AAAA,EACP,aACE;AAAA,EACF,IAAI,EAAE,WAAW,OAAO,KAAK,OAAO,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EAC9D,KAAK,CAAC,CAAC,OAAO,OAAO,oBAAoB,CAAC;AAC5C;;;ADfO,IAAM,cAA4B;AAAA,EACvC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,EACT;AACF;AAGO,IAAM,iBAA+B;AAAA,EAC1C,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,EACf;AACF;","names":["z"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Store, SetupFn } from '@walkeros/core';
|
|
2
|
+
|
|
3
|
+
interface SheetsStoreSettings {
|
|
4
|
+
/** Spreadsheet ID, the path segment between /d/ and /edit in the URL. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Sheet (tab) name within the spreadsheet. Default: 'Sheet1'. */
|
|
7
|
+
sheet?: string;
|
|
8
|
+
/** Column letter for keys (the lookup column). Default: 'A'. */
|
|
9
|
+
key?: string;
|
|
10
|
+
/** Column letter for values (JSON-serialized blob). Default: 'B'. */
|
|
11
|
+
value?: string;
|
|
12
|
+
/** Number of header rows to skip when reading the key column. Default: 1. */
|
|
13
|
+
headerRows?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Service account credentials for non-GCP environments. JSON string or
|
|
16
|
+
* parsed object. Omit for ADC via the GCP metadata server (Cloud Run, GKE).
|
|
17
|
+
*/
|
|
18
|
+
credentials?: string | ServiceAccountCredentials;
|
|
19
|
+
}
|
|
20
|
+
interface ServiceAccountCredentials {
|
|
21
|
+
/** Service account email (from SA JSON `client_email` field). */
|
|
22
|
+
client_email: string;
|
|
23
|
+
/** RSA private key in PEM format (from SA JSON `private_key` field). */
|
|
24
|
+
private_key: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Provisioning options for `walkeros setup store.<id>`.
|
|
28
|
+
* Triggered only by the explicit CLI command. Idempotent.
|
|
29
|
+
*
|
|
30
|
+
* `id` lives in `Settings`, not here (no duplication). Phase 1 omits
|
|
31
|
+
* `shareWith` (Drive API integration); that ships in a later phase if requested.
|
|
32
|
+
*/
|
|
33
|
+
interface Setup {
|
|
34
|
+
/**
|
|
35
|
+
* Header values to write into row 1 of the configured sheet on setup.
|
|
36
|
+
* Idempotent: re-running with the same headers is a no-op overwrite.
|
|
37
|
+
* Optional: when omitted, setup verifies the spreadsheet exists and exits.
|
|
38
|
+
*/
|
|
39
|
+
headers?: string[];
|
|
40
|
+
}
|
|
41
|
+
type Types = Store.Types<SheetsStoreSettings, Store.BaseEnv, SheetsStoreSettings, Setup>;
|
|
42
|
+
|
|
43
|
+
declare const storeSheetsInit: Store.Init<Types>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default setup options. `headers` is intentionally omitted so callers must
|
|
47
|
+
* opt in to writing a header row.
|
|
48
|
+
*/
|
|
49
|
+
declare const DEFAULT_SETUP: Setup;
|
|
50
|
+
interface SetupResult {
|
|
51
|
+
headersWritten: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Public alias kept for callers that imported the prior shape.
|
|
55
|
+
* Equivalent to the framework's `Store.Config<Types>`.
|
|
56
|
+
*/
|
|
57
|
+
type SheetsStoreConfig = Store.Config<Types>;
|
|
58
|
+
/**
|
|
59
|
+
* Provision a Google Sheets store described in the flow config.
|
|
60
|
+
* Verifies the spreadsheet exists and (idempotently) writes the configured
|
|
61
|
+
* `setup.headers` row. Never alters existing data: re-running with the same
|
|
62
|
+
* headers is a no-op overwrite.
|
|
63
|
+
*/
|
|
64
|
+
declare const setup: SetupFn<SheetsStoreConfig, Store.BaseEnv>;
|
|
65
|
+
|
|
66
|
+
export { DEFAULT_SETUP, type ServiceAccountCredentials, type Setup, type SetupResult, type SheetsStoreConfig, type SheetsStoreSettings, type Types, storeSheetsInit as default, setup, storeSheetsInit };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Store, SetupFn } from '@walkeros/core';
|
|
2
|
+
|
|
3
|
+
interface SheetsStoreSettings {
|
|
4
|
+
/** Spreadsheet ID, the path segment between /d/ and /edit in the URL. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Sheet (tab) name within the spreadsheet. Default: 'Sheet1'. */
|
|
7
|
+
sheet?: string;
|
|
8
|
+
/** Column letter for keys (the lookup column). Default: 'A'. */
|
|
9
|
+
key?: string;
|
|
10
|
+
/** Column letter for values (JSON-serialized blob). Default: 'B'. */
|
|
11
|
+
value?: string;
|
|
12
|
+
/** Number of header rows to skip when reading the key column. Default: 1. */
|
|
13
|
+
headerRows?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Service account credentials for non-GCP environments. JSON string or
|
|
16
|
+
* parsed object. Omit for ADC via the GCP metadata server (Cloud Run, GKE).
|
|
17
|
+
*/
|
|
18
|
+
credentials?: string | ServiceAccountCredentials;
|
|
19
|
+
}
|
|
20
|
+
interface ServiceAccountCredentials {
|
|
21
|
+
/** Service account email (from SA JSON `client_email` field). */
|
|
22
|
+
client_email: string;
|
|
23
|
+
/** RSA private key in PEM format (from SA JSON `private_key` field). */
|
|
24
|
+
private_key: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Provisioning options for `walkeros setup store.<id>`.
|
|
28
|
+
* Triggered only by the explicit CLI command. Idempotent.
|
|
29
|
+
*
|
|
30
|
+
* `id` lives in `Settings`, not here (no duplication). Phase 1 omits
|
|
31
|
+
* `shareWith` (Drive API integration); that ships in a later phase if requested.
|
|
32
|
+
*/
|
|
33
|
+
interface Setup {
|
|
34
|
+
/**
|
|
35
|
+
* Header values to write into row 1 of the configured sheet on setup.
|
|
36
|
+
* Idempotent: re-running with the same headers is a no-op overwrite.
|
|
37
|
+
* Optional: when omitted, setup verifies the spreadsheet exists and exits.
|
|
38
|
+
*/
|
|
39
|
+
headers?: string[];
|
|
40
|
+
}
|
|
41
|
+
type Types = Store.Types<SheetsStoreSettings, Store.BaseEnv, SheetsStoreSettings, Setup>;
|
|
42
|
+
|
|
43
|
+
declare const storeSheetsInit: Store.Init<Types>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default setup options. `headers` is intentionally omitted so callers must
|
|
47
|
+
* opt in to writing a header row.
|
|
48
|
+
*/
|
|
49
|
+
declare const DEFAULT_SETUP: Setup;
|
|
50
|
+
interface SetupResult {
|
|
51
|
+
headersWritten: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Public alias kept for callers that imported the prior shape.
|
|
55
|
+
* Equivalent to the framework's `Store.Config<Types>`.
|
|
56
|
+
*/
|
|
57
|
+
type SheetsStoreConfig = Store.Config<Types>;
|
|
58
|
+
/**
|
|
59
|
+
* Provision a Google Sheets store described in the flow config.
|
|
60
|
+
* Verifies the spreadsheet exists and (idempotently) writes the configured
|
|
61
|
+
* `setup.headers` row. Never alters existing data: re-running with the same
|
|
62
|
+
* headers is a no-op overwrite.
|
|
63
|
+
*/
|
|
64
|
+
declare const setup: SetupFn<SheetsStoreConfig, Store.BaseEnv>;
|
|
65
|
+
|
|
66
|
+
export { DEFAULT_SETUP, type ServiceAccountCredentials, type Setup, type SetupResult, type SheetsStoreConfig, type SheetsStoreSettings, type Types, storeSheetsInit as default, setup, storeSheetsInit };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.prototype.hasOwnProperty,s={};((e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:!0})})(s,{DEFAULT_SETUP:()=>v,default:()=>C,setup:()=>m,storeSheetsInit:()=>C}),module.exports=(e=s,((e,s,a,i)=>{if(s&&"object"==typeof s||"function"==typeof s)for(let u of n(s))o.call(e,u)||u===a||t(e,u,{get:()=>s[u],enumerable:!(i=r(s,u))||i.enumerable});return e})(t({},"__esModule",{value:!0}),e));var a=require("crypto"),i="http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",u="https://oauth2.googleapis.com/token",c="https://www.googleapis.com/auth/spreadsheets",d=6e4;function f(e){let t;return async function(){return t&&Date.now()<t.expiresAt||(t=e?await async function(e){const t=Math.floor(Date.now()/1e3),r=function(e,t){const r=p(JSON.stringify({alg:"RS256",typ:"JWT"})),n=p(JSON.stringify(e)),o=(0,a.createSign)("RSA-SHA256");return o.update(`${r}.${n}`),`${r}.${n}.${o.sign(t,"base64url")}`}({iss:e.client_email,scope:c,aud:u,iat:t,exp:t+3600},e.private_key),n=await fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${r}`});if(!n.ok)throw new Error(`Token exchange error: ${n.status}`);const o=await n.json();return{token:o.access_token,expiresAt:Date.now()+1e3*o.expires_in-d}}(e):await async function(){const e=await fetch(i,{headers:{"Metadata-Flavor":"Google"},signal:AbortSignal.timeout(5e3)});if(!e.ok)throw new Error(`Metadata server error: ${e.status}`);const t=await e.json();return{token:t.access_token,expiresAt:Date.now()+1e3*t.expires_in-d}}()),t.token}}function p(e){return("string"==typeof e?Buffer.from(e):e).toString("base64url")}var h=require("@walkeros/core"),l="https://sheets.googleapis.com/v4/spreadsheets";function g(e){return e.sheet??"Sheet1"}function w(e,t,r){return`${$(e)}!${t}${r}`}function y(e,t){const r=function(e){if(e<1)throw new Error(`columnLetter: index must be >=1, got ${e}`);let t=e,r="";for(;t>0;){const e=(t-1)%26;r=String.fromCharCode(65+e)+r,t=Math.floor((t-1)/26)}return r}(t);return`${$(e)}!A1:${r}1`}function $(e){return/^[A-Za-z0-9_]+$/.test(e)?e:`'${e.replace(/'/g,"''")}'`}var v={},m=async e=>{const{config:t,logger:r,id:n}=e,o=(0,h.resolveSetup)(t.setup,v);if(!o)return void r.debug("setup: skipped (config.setup is false or unset)");!function(e){if(!e||"string"!=typeof e.id||0===e.id.length)throw new Error("setup: settings.id (spreadsheet ID) is required")}(t.settings);const s=t.settings,a=f(function(e){if(!e)return;if("string"==typeof e){let r;try{r=JSON.parse(e)}catch{return}return function(e){return"object"==typeof e&&null!==e}(t=r)&&"string"==typeof t.client_email&&"string"==typeof t.private_key?r:void 0}var t;return e}(s.credentials)),i=await a(),u=`${l}/${encodeURIComponent(s.id)}?fields=spreadsheetId`,c=await fetch(u,{headers:{Authorization:`Bearer ${i}`}});if(404===c.status)throw new Error(`Spreadsheet not found: ${s.id}. Run "walkeros setup store.${n}" to ensure the sheet exists and is shared with the service account.`);if(!c.ok){const e=await S(c);throw new Error(`setup: spreadsheet probe failed (${c.status}): ${e}`)}let d=!1;if(o.headers&&o.headers.length>0){const e=y(g(s),o.headers.length),t=`${l}/${encodeURIComponent(s.id)}/values/${encodeURIComponent(e)}?valueInputOption=RAW`,n={values:[o.headers],range:e},a=await fetch(t,{method:"PUT",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json"},body:JSON.stringify(n)});if(!a.ok){const e=await S(a);throw new Error(`setup: header write failed (${a.status}): ${e}`)}d=!0,r.info("setup: headers written",{spreadsheetId:s.id,headers:o.headers.length})}else r.debug("setup: spreadsheet verified, no headers configured",{spreadsheetId:s.id});return{headersWritten:d}};async function S(e){try{return await e.text()}catch{return""}}var k=new Map;function A(e){return"object"==typeof e&&null!==e}function b(e){if(e){if("string"==typeof e){let r;try{r=JSON.parse(e)}catch{return}return A(t=r)&&"string"==typeof t.client_email&&"string"==typeof t.private_key?r:void 0}var t;return e}}async function I(e,t,r,n,o){const s=await o(),a=function(e,t,r){return`${$(e)}!${t}${r}:${t}`}(t,r,n+1),i=`${l}/${encodeURIComponent(e)}/values/${encodeURIComponent(a)}`,u=await fetch(i,{headers:{Authorization:`Bearer ${s}`}});if(!u.ok)throw new Error(`storeSheetsInit: failed to read key column (${u.status})`);const c=function(e){if(!A(e))return[];const t=e.values;if(!Array.isArray(t))return[];const r=[];for(const e of t)Array.isArray(e)&&"string"==typeof e[0]?r.push(e[0]):r.push("");return r}(await u.json()),d=new Map;return c.forEach((e,t)=>{e&&d.set(e,n+t+1)}),d}async function O(e,t,r,n,o,s){const a=await s(),i=w(t,r,n),u=`${l}/${encodeURIComponent(e)}/values/${encodeURIComponent(i)}?valueInputOption=RAW`,c=await fetch(u,{method:"PUT",headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"},body:JSON.stringify({values:[[o]],range:i})});if(!c.ok)throw new Error(`storeSheets.writeCell: write failed (${c.status})`)}async function R(e,t,r,n,o,s){const a=await s(),i=function(e,t,r){return`${$(e)}!${t}:${r}`}(t,r,n),u=`${l}/${encodeURIComponent(e)}/values/${encodeURIComponent(i)}:append?valueInputOption=RAW`,c=await fetch(u,{method:"POST",headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"},body:JSON.stringify({values:[o]})});if(!c.ok)throw new Error(`storeSheets.appendRow: append failed (${c.status})`);const d=await c.json();if(!A(d))return;const f=d.updates;if(!A(f))return;const p=f.updatedRange;return"string"==typeof p?function(e){const t=/![A-Z]+(\d+)/.exec(e);if(!t)return;const r=Number.parseInt(t[1],10);return Number.isFinite(r)?r:void 0}(p):void 0}var C=async e=>{!function(e){if(!e||"string"!=typeof e.id||0===e.id.length)throw new Error("storeSheetsInit: settings.id (spreadsheet ID) is required (non-empty string)")}(e.config.settings);const t=e.config.settings,{logger:r}=e,n=e.id,o=f(b(t.credentials)),s=g(t),a=function(e){return(e.key??"A").toUpperCase()}(t),i=function(e){return(e.value??"B").toUpperCase()}(t),u=function(e){return e.headerRows??1}(t);await async function(e,t,r,n){const o=k.get(e);if(void 0!==o)return void await o;const s=(async()=>{let o;try{const t=await r(),n=`${l}/${encodeURIComponent(e)}?fields=spreadsheetId`;o=await fetch(n,{headers:{Authorization:`Bearer ${t}`}})}catch(t){return n.debug("ensureSpreadsheetExists check failed (non-fatal)",{spreadsheetId:e,error:t instanceof Error?t.message:String(t)}),!0}if(404===o.status)throw new Error(`Spreadsheet not found: ${e}. Run "walkeros setup store.${t}" to ensure the sheet exists and is shared with the service account.`);return o.ok||n.debug("ensureSpreadsheetExists check failed (non-fatal)",{spreadsheetId:e,status:o.status}),!0})();k.set(e,s);try{await s}catch(t){throw k.delete(e),t}}(t.id,n,o,r);const c=await I(t.id,s,a,u,o);return{type:"sheets",config:{settings:t,env:e.config.env,id:e.config.id,logger:e.config.logger},setup:m,async get(e){const n=c.get(e);if(void 0===n)return;const a=await async function(e,t,r,n,o){const s=await o(),a=w(t,r,n),i=`${l}/${encodeURIComponent(e)}/values/${encodeURIComponent(a)}`,u=await fetch(i,{headers:{Authorization:`Bearer ${s}`}});if(!u.ok)return;const c=await u.json();if(!A(c))return;const d=c.values;if(!Array.isArray(d)||0===d.length)return;const f=d[0];if(!Array.isArray(f)||0===f.length)return;const p=f[0];return"string"==typeof p?p:void 0}(t.id,s,i,n,o);if(void 0!==a&&""!==a)try{return JSON.parse(a)}catch(t){return void r.debug("storeSheets.get: JSON parse failed",{key:e,error:t instanceof Error?t.message:String(t)})}},async set(e,r){const n=JSON.stringify(r),u=c.get(e);if(void 0===u){const r=await R(t.id,s,a,i,[e,n],o);void 0!==r&&c.set(e,r)}else await O(t.id,s,i,u,n,o)},async delete(e){const r=c.get(e);void 0!==r&&await O(t.id,s,i,r,"",o)}}};//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/auth.ts","../src/setup.ts","../src/setup-helpers.ts","../src/store.ts"],"sourcesContent":["export { storeSheetsInit } from './store';\nexport { setup, DEFAULT_SETUP } from './setup';\nexport type { SetupResult, SheetsStoreConfig } from './setup';\nexport type {\n ServiceAccountCredentials,\n Setup,\n SheetsStoreSettings,\n Types,\n} from './types';\nexport { storeSheetsInit as default } from './store';\n","import { createSign } from 'node:crypto';\nimport type { ServiceAccountCredentials } from './types';\n\nexport type TokenProvider = () => Promise<string>;\n\nconst METADATA_URL =\n 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token';\nconst OAUTH_URL = 'https://oauth2.googleapis.com/token';\nconst SCOPE = 'https://www.googleapis.com/auth/spreadsheets';\nconst REFRESH_MARGIN_MS = 60_000;\n\ninterface CachedToken {\n token: string;\n expiresAt: number;\n}\n\nexport function createTokenProvider(\n credentials?: ServiceAccountCredentials,\n): TokenProvider {\n let cached: CachedToken | undefined;\n\n return async function getToken(): Promise<string> {\n if (cached && Date.now() < cached.expiresAt) return cached.token;\n\n cached = credentials\n ? await fetchServiceAccountToken(credentials)\n : await fetchMetadataToken();\n\n return cached.token;\n };\n}\n\nasync function fetchMetadataToken(): Promise<CachedToken> {\n const res = await fetch(METADATA_URL, {\n headers: { 'Metadata-Flavor': 'Google' },\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`Metadata server error: ${res.status}`);\n\n const data = (await res.json()) as {\n access_token: string;\n expires_in: number;\n };\n return {\n token: data.access_token,\n expiresAt: Date.now() + data.expires_in * 1000 - REFRESH_MARGIN_MS,\n };\n}\n\nasync function fetchServiceAccountToken(\n creds: ServiceAccountCredentials,\n): Promise<CachedToken> {\n const now = Math.floor(Date.now() / 1000);\n const jwt = signJwt(\n {\n iss: creds.client_email,\n scope: SCOPE,\n aud: OAUTH_URL,\n iat: now,\n exp: now + 3600,\n },\n creds.private_key,\n );\n\n const res = await fetch(OAUTH_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,\n });\n if (!res.ok) throw new Error(`Token exchange error: ${res.status}`);\n\n const data = (await res.json()) as {\n access_token: string;\n expires_in: number;\n };\n return {\n token: data.access_token,\n expiresAt: Date.now() + data.expires_in * 1000 - REFRESH_MARGIN_MS,\n };\n}\n\nfunction base64url(input: string | Buffer): string {\n const buf = typeof input === 'string' ? Buffer.from(input) : input;\n return buf.toString('base64url');\n}\n\nfunction signJwt(payload: object, privateKey: string): string {\n const header = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const body = base64url(JSON.stringify(payload));\n const sign = createSign('RSA-SHA256');\n sign.update(`${header}.${body}`);\n return `${header}.${body}.${sign.sign(privateKey, 'base64url')}`;\n}\n","import type { LifecycleContext, SetupFn, Store } from '@walkeros/core';\nimport { resolveSetup } from '@walkeros/core';\nimport type {\n ServiceAccountCredentials,\n Setup,\n SheetsStoreSettings,\n Types,\n} from './types';\nimport { createTokenProvider } from './auth';\nimport { SHEETS_BASE, buildHeaderRange, resolveSheet } from './setup-helpers';\n\n/**\n * Default setup options. `headers` is intentionally omitted so callers must\n * opt in to writing a header row.\n */\nexport const DEFAULT_SETUP: Setup = {};\n\nexport interface SetupResult {\n headersWritten: boolean;\n}\n\n/**\n * Public alias kept for callers that imported the prior shape.\n * Equivalent to the framework's `Store.Config<Types>`.\n */\nexport type SheetsStoreConfig = Store.Config<Types>;\n\n/**\n * Provision a Google Sheets store described in the flow config.\n * Verifies the spreadsheet exists and (idempotently) writes the configured\n * `setup.headers` row. Never alters existing data: re-running with the same\n * headers is a no-op overwrite.\n */\nexport const setup: SetupFn<SheetsStoreConfig, Store.BaseEnv> = async (\n context: LifecycleContext<SheetsStoreConfig, Store.BaseEnv>,\n) => {\n const { config, logger, id } = context;\n const options = resolveSetup(config.setup, DEFAULT_SETUP);\n if (!options) {\n logger.debug('setup: skipped (config.setup is false or unset)');\n return;\n }\n\n assertSheetsSettings(config.settings);\n const settings = config.settings;\n\n const creds = parseCredentials(settings.credentials);\n const getToken = createTokenProvider(creds);\n const token = await getToken();\n\n const probeUrl = `${SHEETS_BASE}/${encodeURIComponent(settings.id)}?fields=spreadsheetId`;\n const probeRes = await fetch(probeUrl, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (probeRes.status === 404) {\n throw new Error(\n `Spreadsheet not found: ${settings.id}. Run \"walkeros setup store.${id}\" to ensure the sheet exists and is shared with the service account.`,\n );\n }\n if (!probeRes.ok) {\n const text = await safeText(probeRes);\n throw new Error(\n `setup: spreadsheet probe failed (${probeRes.status}): ${text}`,\n );\n }\n\n let headersWritten = false;\n if (options.headers && options.headers.length > 0) {\n const sheet = resolveSheet(settings);\n const range = buildHeaderRange(sheet, options.headers.length);\n const url = `${SHEETS_BASE}/${encodeURIComponent(settings.id)}/values/${encodeURIComponent(range)}?valueInputOption=RAW`;\n const body = { values: [options.headers], range };\n const writeRes = await fetch(url, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n if (!writeRes.ok) {\n const text = await safeText(writeRes);\n throw new Error(\n `setup: header write failed (${writeRes.status}): ${text}`,\n );\n }\n headersWritten = true;\n logger.info('setup: headers written', {\n spreadsheetId: settings.id,\n headers: options.headers.length,\n });\n } else {\n logger.debug('setup: spreadsheet verified, no headers configured', {\n spreadsheetId: settings.id,\n });\n }\n\n return { headersWritten };\n};\n\nfunction assertSheetsSettings(\n settings: Partial<SheetsStoreSettings> | undefined,\n): asserts settings is SheetsStoreSettings {\n if (\n !settings ||\n typeof settings.id !== 'string' ||\n settings.id.length === 0\n ) {\n throw new Error('setup: settings.id (spreadsheet ID) is required');\n }\n}\n\nfunction parseCredentials(\n credentials?: string | ServiceAccountCredentials,\n): ServiceAccountCredentials | undefined {\n if (!credentials) return undefined;\n if (typeof credentials === 'string') {\n let parsed: unknown;\n try {\n parsed = JSON.parse(credentials);\n } catch {\n return undefined;\n }\n if (isServiceAccountShape(parsed)) return parsed;\n return undefined;\n }\n return credentials;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isServiceAccountShape(\n value: unknown,\n): value is ServiceAccountCredentials {\n if (!isRecord(value)) return false;\n return (\n typeof value.client_email === 'string' &&\n typeof value.private_key === 'string'\n );\n}\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return await res.text();\n } catch {\n return '';\n }\n}\n","import type { SheetsStoreSettings } from './types';\n\nexport const SHEETS_BASE = 'https://sheets.googleapis.com/v4/spreadsheets';\n\nexport function resolveSheet(settings: SheetsStoreSettings): string {\n return settings.sheet ?? 'Sheet1';\n}\n\nexport function resolveKeyColumn(settings: SheetsStoreSettings): string {\n return (settings.key ?? 'A').toUpperCase();\n}\n\nexport function resolveValueColumn(settings: SheetsStoreSettings): string {\n return (settings.value ?? 'B').toUpperCase();\n}\n\nexport function resolveHeaderRows(settings: SheetsStoreSettings): number {\n return settings.headerRows ?? 1;\n}\n\n/** Build an A1 range like 'Sheet1!A2:A' for reading a whole column from row N. */\nexport function buildColumnRange(\n sheet: string,\n column: string,\n fromRow: number,\n): string {\n return `${encodeSheet(sheet)}!${column}${fromRow}:${column}`;\n}\n\n/** Build an A1 range like 'Sheet1!B5' for a single cell. */\nexport function buildCellRange(\n sheet: string,\n column: string,\n row: number,\n): string {\n return `${encodeSheet(sheet)}!${column}${row}`;\n}\n\n/** Build an A1 range for writing the header row. */\nexport function buildHeaderRange(sheet: string, columnsCount: number): string {\n const last = columnLetter(columnsCount);\n return `${encodeSheet(sheet)}!A1:${last}1`;\n}\n\n/** Build an A1 range for appending rows across the key+value columns. */\nexport function buildAppendRange(\n sheet: string,\n keyColumn: string,\n valueColumn: string,\n): string {\n return `${encodeSheet(sheet)}!${keyColumn}:${valueColumn}`;\n}\n\n/** Convert a 1-based column index into a column letter (1 -> A, 27 -> AA). */\nexport function columnLetter(index: number): string {\n if (index < 1) {\n throw new Error(`columnLetter: index must be >=1, got ${index}`);\n }\n let n = index;\n let out = '';\n while (n > 0) {\n const rem = (n - 1) % 26;\n out = String.fromCharCode(65 + rem) + out;\n n = Math.floor((n - 1) / 26);\n }\n return out;\n}\n\n/** Sheet names with spaces or special chars must be wrapped in single quotes. */\nexport function encodeSheet(name: string): string {\n if (/^[A-Za-z0-9_]+$/.test(name)) return name;\n return `'${name.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Parse a row index out of a Sheets API `updates.updatedRange` value such as\n * `Sheet1!A5:B5` or `'My Sheet'!A5:B5`. Returns the first row number found.\n */\nexport function parseRowFromRange(range: string): number | undefined {\n const match = /![A-Z]+(\\d+)/.exec(range);\n if (!match) return undefined;\n const row = Number.parseInt(match[1], 10);\n return Number.isFinite(row) ? row : undefined;\n}\n","import type { Logger, Store } from '@walkeros/core';\nimport type {\n ServiceAccountCredentials,\n SheetsStoreSettings,\n Types,\n} from './types';\nimport { createTokenProvider, type TokenProvider } from './auth';\nimport { setup as sheetsSetup } from './setup';\nimport {\n SHEETS_BASE,\n buildAppendRange,\n buildCellRange,\n buildColumnRange,\n parseRowFromRange,\n resolveHeaderRows,\n resolveKeyColumn,\n resolveSheet,\n resolveValueColumn,\n} from './setup-helpers';\n\n/**\n * Module-level cache for the spreadsheet existence pre-check, keyed by\n * spreadsheet ID. Shared across `storeSheetsInit` invocations so the same\n * spreadsheet is probed at most once per process.\n */\nconst spreadsheetExistsCache: Map<string, Promise<boolean>> = new Map();\n\n/** @internal Test-only: clear the existence cache. */\nexport function __resetSpreadsheetExistenceCache(): void {\n spreadsheetExistsCache.clear();\n}\n\n/** @internal Test-only: seed the existence cache for a spreadsheet. */\nexport function __seedSpreadsheetExists(spreadsheetId: string): void {\n spreadsheetExistsCache.set(spreadsheetId, Promise.resolve(true));\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isServiceAccountShape(\n value: unknown,\n): value is ServiceAccountCredentials {\n if (!isRecord(value)) return false;\n return (\n typeof value.client_email === 'string' &&\n typeof value.private_key === 'string'\n );\n}\n\nfunction parseCredentials(\n credentials?: string | ServiceAccountCredentials,\n): ServiceAccountCredentials | undefined {\n if (!credentials) return undefined;\n if (typeof credentials === 'string') {\n let parsed: unknown;\n try {\n parsed = JSON.parse(credentials);\n } catch {\n return undefined;\n }\n if (isServiceAccountShape(parsed)) return parsed;\n return undefined;\n }\n return credentials;\n}\n\nfunction assertSheetsSettings(\n settings: Partial<SheetsStoreSettings> | undefined,\n): asserts settings is SheetsStoreSettings {\n if (\n !settings ||\n typeof settings.id !== 'string' ||\n settings.id.length === 0\n ) {\n throw new Error(\n 'storeSheetsInit: settings.id (spreadsheet ID) is required (non-empty string)',\n );\n }\n}\n\nasync function ensureSpreadsheetExists(\n spreadsheetId: string,\n storeId: string,\n getToken: TokenProvider,\n logger: Logger.Instance,\n): Promise<void> {\n const existing = spreadsheetExistsCache.get(spreadsheetId);\n if (existing !== undefined) {\n await existing;\n return;\n }\n\n const promise = (async (): Promise<boolean> => {\n let res: Response;\n try {\n const token = await getToken();\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}?fields=spreadsheetId`;\n res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n } catch (err) {\n logger.debug('ensureSpreadsheetExists check failed (non-fatal)', {\n spreadsheetId,\n error: err instanceof Error ? err.message : String(err),\n });\n return true;\n }\n\n if (res.status === 404) {\n throw new Error(\n `Spreadsheet not found: ${spreadsheetId}. Run \"walkeros setup store.${storeId}\" to ensure the sheet exists and is shared with the service account.`,\n );\n }\n if (!res.ok) {\n logger.debug('ensureSpreadsheetExists check failed (non-fatal)', {\n spreadsheetId,\n status: res.status,\n });\n }\n return true;\n })();\n\n spreadsheetExistsCache.set(spreadsheetId, promise);\n try {\n await promise;\n } catch (err) {\n spreadsheetExistsCache.delete(spreadsheetId);\n throw err;\n }\n}\n\ninterface KeyColumnResponse {\n values?: unknown[];\n}\n\nfunction readKeyColumnRows(payload: unknown): string[] {\n if (!isRecord(payload)) return [];\n const values = (payload as KeyColumnResponse).values;\n if (!Array.isArray(values)) return [];\n const out: string[] = [];\n for (const row of values) {\n if (Array.isArray(row) && typeof row[0] === 'string') {\n out.push(row[0]);\n } else {\n out.push('');\n }\n }\n return out;\n}\n\nasync function buildKeyIndex(\n spreadsheetId: string,\n sheet: string,\n keyCol: string,\n headerRows: number,\n getToken: TokenProvider,\n): Promise<Map<string, number>> {\n const token = await getToken();\n const range = buildColumnRange(sheet, keyCol, headerRows + 1);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) {\n throw new Error(\n `storeSheetsInit: failed to read key column (${res.status})`,\n );\n }\n const payload: unknown = await res.json();\n const rows = readKeyColumnRows(payload);\n const index = new Map<string, number>();\n rows.forEach((key, i) => {\n if (key) index.set(key, headerRows + i + 1);\n });\n return index;\n}\n\nasync function readCell(\n spreadsheetId: string,\n sheet: string,\n column: string,\n row: number,\n getToken: TokenProvider,\n): Promise<string | undefined> {\n const token = await getToken();\n const range = buildCellRange(sheet, column, row);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) return undefined;\n const payload: unknown = await res.json();\n if (!isRecord(payload)) return undefined;\n const values = (payload as KeyColumnResponse).values;\n if (!Array.isArray(values) || values.length === 0) return undefined;\n const first = values[0];\n if (!Array.isArray(first) || first.length === 0) return undefined;\n const cell = first[0];\n return typeof cell === 'string' ? cell : undefined;\n}\n\nasync function writeCell(\n spreadsheetId: string,\n sheet: string,\n column: string,\n row: number,\n value: string,\n getToken: TokenProvider,\n): Promise<void> {\n const token = await getToken();\n const range = buildCellRange(sheet, column, row);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}?valueInputOption=RAW`;\n const res = await fetch(url, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ values: [[value]], range }),\n });\n if (!res.ok) {\n throw new Error(`storeSheets.writeCell: write failed (${res.status})`);\n }\n}\n\ninterface AppendResponse {\n updates?: { updatedRange?: unknown };\n}\n\nasync function appendRow(\n spreadsheetId: string,\n sheet: string,\n keyCol: string,\n valueCol: string,\n values: [string, string],\n getToken: TokenProvider,\n): Promise<number | undefined> {\n const token = await getToken();\n const range = buildAppendRange(sheet, keyCol, valueCol);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}:append?valueInputOption=RAW`;\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ values: [values] }),\n });\n if (!res.ok) {\n throw new Error(`storeSheets.appendRow: append failed (${res.status})`);\n }\n const payload: unknown = await res.json();\n if (!isRecord(payload)) return undefined;\n const updates = (payload as AppendResponse).updates;\n if (!isRecord(updates)) return undefined;\n const updatedRange = updates.updatedRange;\n if (typeof updatedRange !== 'string') return undefined;\n return parseRowFromRange(updatedRange);\n}\n\nexport const storeSheetsInit: Store.Init<Types> = async (context) => {\n assertSheetsSettings(context.config.settings);\n const settings: SheetsStoreSettings = context.config.settings;\n const { logger } = context;\n const id = context.id;\n const creds = parseCredentials(settings.credentials);\n const getToken = createTokenProvider(creds);\n const sheet = resolveSheet(settings);\n const keyCol = resolveKeyColumn(settings);\n const valueCol = resolveValueColumn(settings);\n const headerRows = resolveHeaderRows(settings);\n\n await ensureSpreadsheetExists(settings.id, id, getToken, logger);\n\n const keyToRow = await buildKeyIndex(\n settings.id,\n sheet,\n keyCol,\n headerRows,\n getToken,\n );\n\n const config: Store.Config<Types> = {\n settings,\n env: context.config.env,\n id: context.config.id,\n logger: context.config.logger,\n };\n\n return {\n type: 'sheets',\n config,\n setup: sheetsSetup,\n\n async get(key: string): Promise<unknown> {\n const row = keyToRow.get(key);\n if (row === undefined) return undefined;\n const cell = await readCell(settings.id, sheet, valueCol, row, getToken);\n if (cell === undefined || cell === '') return undefined;\n try {\n return JSON.parse(cell);\n } catch (err) {\n logger.debug('storeSheets.get: JSON parse failed', {\n key,\n error: err instanceof Error ? err.message : String(err),\n });\n return undefined;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n const serialized = JSON.stringify(value);\n const existingRow = keyToRow.get(key);\n if (existingRow === undefined) {\n const newRow = await appendRow(\n settings.id,\n sheet,\n keyCol,\n valueCol,\n [key, serialized],\n getToken,\n );\n if (newRow !== undefined) keyToRow.set(key, newRow);\n } else {\n await writeCell(\n settings.id,\n sheet,\n valueCol,\n existingRow,\n serialized,\n getToken,\n );\n }\n },\n\n async delete(key: string): Promise<void> {\n const row = keyToRow.get(key);\n if (row === undefined) return;\n await writeCell(settings.id, sheet, valueCol, row, '', getToken);\n },\n };\n};\n"],"mappings":"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","names":["isRecord","isServiceAccountShape","parseCredentials","assertSheetsSettings"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createSign as e}from"crypto";var t="http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",n="https://oauth2.googleapis.com/token",r="https://www.googleapis.com/auth/spreadsheets",o=6e4;function s(s){let i;return async function(){return i&&Date.now()<i.expiresAt||(i=s?await async function(t){const s=Math.floor(Date.now()/1e3),i=function(t,n){const r=a(JSON.stringify({alg:"RS256",typ:"JWT"})),o=a(JSON.stringify(t)),s=e("RSA-SHA256");return s.update(`${r}.${o}`),`${r}.${o}.${s.sign(n,"base64url")}`}({iss:t.client_email,scope:r,aud:n,iat:s,exp:s+3600},t.private_key),c=await fetch(n,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${i}`});if(!c.ok)throw new Error(`Token exchange error: ${c.status}`);const u=await c.json();return{token:u.access_token,expiresAt:Date.now()+1e3*u.expires_in-o}}(s):await async function(){const e=await fetch(t,{headers:{"Metadata-Flavor":"Google"},signal:AbortSignal.timeout(5e3)});if(!e.ok)throw new Error(`Metadata server error: ${e.status}`);const n=await e.json();return{token:n.access_token,expiresAt:Date.now()+1e3*n.expires_in-o}}()),i.token}}function a(e){return("string"==typeof e?Buffer.from(e):e).toString("base64url")}import{resolveSetup as i}from"@walkeros/core";var c="https://sheets.googleapis.com/v4/spreadsheets";function u(e){return e.sheet??"Sheet1"}function d(e,t,n){return`${p(e)}!${t}${n}`}function f(e,t){const n=function(e){if(e<1)throw new Error(`columnLetter: index must be >=1, got ${e}`);let t=e,n="";for(;t>0;){const e=(t-1)%26;n=String.fromCharCode(65+e)+n,t=Math.floor((t-1)/26)}return n}(t);return`${p(e)}!A1:${n}1`}function p(e){return/^[A-Za-z0-9_]+$/.test(e)?e:`'${e.replace(/'/g,"''")}'`}var h={},l=async e=>{const{config:t,logger:n,id:r}=e,o=i(t.setup,h);if(!o)return void n.debug("setup: skipped (config.setup is false or unset)");!function(e){if(!e||"string"!=typeof e.id||0===e.id.length)throw new Error("setup: settings.id (spreadsheet ID) is required")}(t.settings);const a=t.settings,d=s(function(e){if(!e)return;if("string"==typeof e){let n;try{n=JSON.parse(e)}catch{return}return function(e){return"object"==typeof e&&null!==e}(t=n)&&"string"==typeof t.client_email&&"string"==typeof t.private_key?n:void 0}var t;return e}(a.credentials)),p=await d(),l=`${c}/${encodeURIComponent(a.id)}?fields=spreadsheetId`,w=await fetch(l,{headers:{Authorization:`Bearer ${p}`}});if(404===w.status)throw new Error(`Spreadsheet not found: ${a.id}. Run "walkeros setup store.${r}" to ensure the sheet exists and is shared with the service account.`);if(!w.ok){const e=await g(w);throw new Error(`setup: spreadsheet probe failed (${w.status}): ${e}`)}let y=!1;if(o.headers&&o.headers.length>0){const e=f(u(a),o.headers.length),t=`${c}/${encodeURIComponent(a.id)}/values/${encodeURIComponent(e)}?valueInputOption=RAW`,r={values:[o.headers],range:e},s=await fetch(t,{method:"PUT",headers:{Authorization:`Bearer ${p}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!s.ok){const e=await g(s);throw new Error(`setup: header write failed (${s.status}): ${e}`)}y=!0,n.info("setup: headers written",{spreadsheetId:a.id,headers:o.headers.length})}else n.debug("setup: spreadsheet verified, no headers configured",{spreadsheetId:a.id});return{headersWritten:y}};async function g(e){try{return await e.text()}catch{return""}}var w=new Map;function y(e){return"object"==typeof e&&null!==e}function $(e){if(e){if("string"==typeof e){let n;try{n=JSON.parse(e)}catch{return}return y(t=n)&&"string"==typeof t.client_email&&"string"==typeof t.private_key?n:void 0}var t;return e}}async function v(e,t,n,r,o){const s=await o(),a=function(e,t,n){return`${p(e)}!${t}${n}:${t}`}(t,n,r+1),i=`${c}/${encodeURIComponent(e)}/values/${encodeURIComponent(a)}`,u=await fetch(i,{headers:{Authorization:`Bearer ${s}`}});if(!u.ok)throw new Error(`storeSheetsInit: failed to read key column (${u.status})`);const d=function(e){if(!y(e))return[];const t=e.values;if(!Array.isArray(t))return[];const n=[];for(const e of t)Array.isArray(e)&&"string"==typeof e[0]?n.push(e[0]):n.push("");return n}(await u.json()),f=new Map;return d.forEach((e,t)=>{e&&f.set(e,r+t+1)}),f}async function m(e,t,n,r,o,s){const a=await s(),i=d(t,n,r),u=`${c}/${encodeURIComponent(e)}/values/${encodeURIComponent(i)}?valueInputOption=RAW`,f=await fetch(u,{method:"PUT",headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"},body:JSON.stringify({values:[[o]],range:i})});if(!f.ok)throw new Error(`storeSheets.writeCell: write failed (${f.status})`)}async function S(e,t,n,r,o,s){const a=await s(),i=function(e,t,n){return`${p(e)}!${t}:${n}`}(t,n,r),u=`${c}/${encodeURIComponent(e)}/values/${encodeURIComponent(i)}:append?valueInputOption=RAW`,d=await fetch(u,{method:"POST",headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"},body:JSON.stringify({values:[o]})});if(!d.ok)throw new Error(`storeSheets.appendRow: append failed (${d.status})`);const f=await d.json();if(!y(f))return;const h=f.updates;if(!y(h))return;const l=h.updatedRange;return"string"==typeof l?function(e){const t=/![A-Z]+(\d+)/.exec(e);if(!t)return;const n=Number.parseInt(t[1],10);return Number.isFinite(n)?n:void 0}(l):void 0}var k=async e=>{!function(e){if(!e||"string"!=typeof e.id||0===e.id.length)throw new Error("storeSheetsInit: settings.id (spreadsheet ID) is required (non-empty string)")}(e.config.settings);const t=e.config.settings,{logger:n}=e,r=e.id,o=s($(t.credentials)),a=u(t),i=function(e){return(e.key??"A").toUpperCase()}(t),f=function(e){return(e.value??"B").toUpperCase()}(t),p=function(e){return e.headerRows??1}(t);await async function(e,t,n,r){const o=w.get(e);if(void 0!==o)return void await o;const s=(async()=>{let o;try{const t=await n(),r=`${c}/${encodeURIComponent(e)}?fields=spreadsheetId`;o=await fetch(r,{headers:{Authorization:`Bearer ${t}`}})}catch(t){return r.debug("ensureSpreadsheetExists check failed (non-fatal)",{spreadsheetId:e,error:t instanceof Error?t.message:String(t)}),!0}if(404===o.status)throw new Error(`Spreadsheet not found: ${e}. Run "walkeros setup store.${t}" to ensure the sheet exists and is shared with the service account.`);return o.ok||r.debug("ensureSpreadsheetExists check failed (non-fatal)",{spreadsheetId:e,status:o.status}),!0})();w.set(e,s);try{await s}catch(t){throw w.delete(e),t}}(t.id,r,o,n);const h=await v(t.id,a,i,p,o);return{type:"sheets",config:{settings:t,env:e.config.env,id:e.config.id,logger:e.config.logger},setup:l,async get(e){const r=h.get(e);if(void 0===r)return;const s=await async function(e,t,n,r,o){const s=await o(),a=d(t,n,r),i=`${c}/${encodeURIComponent(e)}/values/${encodeURIComponent(a)}`,u=await fetch(i,{headers:{Authorization:`Bearer ${s}`}});if(!u.ok)return;const f=await u.json();if(!y(f))return;const p=f.values;if(!Array.isArray(p)||0===p.length)return;const h=p[0];if(!Array.isArray(h)||0===h.length)return;const l=h[0];return"string"==typeof l?l:void 0}(t.id,a,f,r,o);if(void 0!==s&&""!==s)try{return JSON.parse(s)}catch(t){return void n.debug("storeSheets.get: JSON parse failed",{key:e,error:t instanceof Error?t.message:String(t)})}},async set(e,n){const r=JSON.stringify(n),s=h.get(e);if(void 0===s){const n=await S(t.id,a,i,f,[e,r],o);void 0!==n&&h.set(e,n)}else await m(t.id,a,f,s,r,o)},async delete(e){const n=h.get(e);void 0!==n&&await m(t.id,a,f,n,"",o)}}};export{h as DEFAULT_SETUP,k as default,l as setup,k as storeSheetsInit};//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth.ts","../src/setup.ts","../src/setup-helpers.ts","../src/store.ts"],"sourcesContent":["import { createSign } from 'node:crypto';\nimport type { ServiceAccountCredentials } from './types';\n\nexport type TokenProvider = () => Promise<string>;\n\nconst METADATA_URL =\n 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token';\nconst OAUTH_URL = 'https://oauth2.googleapis.com/token';\nconst SCOPE = 'https://www.googleapis.com/auth/spreadsheets';\nconst REFRESH_MARGIN_MS = 60_000;\n\ninterface CachedToken {\n token: string;\n expiresAt: number;\n}\n\nexport function createTokenProvider(\n credentials?: ServiceAccountCredentials,\n): TokenProvider {\n let cached: CachedToken | undefined;\n\n return async function getToken(): Promise<string> {\n if (cached && Date.now() < cached.expiresAt) return cached.token;\n\n cached = credentials\n ? await fetchServiceAccountToken(credentials)\n : await fetchMetadataToken();\n\n return cached.token;\n };\n}\n\nasync function fetchMetadataToken(): Promise<CachedToken> {\n const res = await fetch(METADATA_URL, {\n headers: { 'Metadata-Flavor': 'Google' },\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`Metadata server error: ${res.status}`);\n\n const data = (await res.json()) as {\n access_token: string;\n expires_in: number;\n };\n return {\n token: data.access_token,\n expiresAt: Date.now() + data.expires_in * 1000 - REFRESH_MARGIN_MS,\n };\n}\n\nasync function fetchServiceAccountToken(\n creds: ServiceAccountCredentials,\n): Promise<CachedToken> {\n const now = Math.floor(Date.now() / 1000);\n const jwt = signJwt(\n {\n iss: creds.client_email,\n scope: SCOPE,\n aud: OAUTH_URL,\n iat: now,\n exp: now + 3600,\n },\n creds.private_key,\n );\n\n const res = await fetch(OAUTH_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,\n });\n if (!res.ok) throw new Error(`Token exchange error: ${res.status}`);\n\n const data = (await res.json()) as {\n access_token: string;\n expires_in: number;\n };\n return {\n token: data.access_token,\n expiresAt: Date.now() + data.expires_in * 1000 - REFRESH_MARGIN_MS,\n };\n}\n\nfunction base64url(input: string | Buffer): string {\n const buf = typeof input === 'string' ? Buffer.from(input) : input;\n return buf.toString('base64url');\n}\n\nfunction signJwt(payload: object, privateKey: string): string {\n const header = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const body = base64url(JSON.stringify(payload));\n const sign = createSign('RSA-SHA256');\n sign.update(`${header}.${body}`);\n return `${header}.${body}.${sign.sign(privateKey, 'base64url')}`;\n}\n","import type { LifecycleContext, SetupFn, Store } from '@walkeros/core';\nimport { resolveSetup } from '@walkeros/core';\nimport type {\n ServiceAccountCredentials,\n Setup,\n SheetsStoreSettings,\n Types,\n} from './types';\nimport { createTokenProvider } from './auth';\nimport { SHEETS_BASE, buildHeaderRange, resolveSheet } from './setup-helpers';\n\n/**\n * Default setup options. `headers` is intentionally omitted so callers must\n * opt in to writing a header row.\n */\nexport const DEFAULT_SETUP: Setup = {};\n\nexport interface SetupResult {\n headersWritten: boolean;\n}\n\n/**\n * Public alias kept for callers that imported the prior shape.\n * Equivalent to the framework's `Store.Config<Types>`.\n */\nexport type SheetsStoreConfig = Store.Config<Types>;\n\n/**\n * Provision a Google Sheets store described in the flow config.\n * Verifies the spreadsheet exists and (idempotently) writes the configured\n * `setup.headers` row. Never alters existing data: re-running with the same\n * headers is a no-op overwrite.\n */\nexport const setup: SetupFn<SheetsStoreConfig, Store.BaseEnv> = async (\n context: LifecycleContext<SheetsStoreConfig, Store.BaseEnv>,\n) => {\n const { config, logger, id } = context;\n const options = resolveSetup(config.setup, DEFAULT_SETUP);\n if (!options) {\n logger.debug('setup: skipped (config.setup is false or unset)');\n return;\n }\n\n assertSheetsSettings(config.settings);\n const settings = config.settings;\n\n const creds = parseCredentials(settings.credentials);\n const getToken = createTokenProvider(creds);\n const token = await getToken();\n\n const probeUrl = `${SHEETS_BASE}/${encodeURIComponent(settings.id)}?fields=spreadsheetId`;\n const probeRes = await fetch(probeUrl, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (probeRes.status === 404) {\n throw new Error(\n `Spreadsheet not found: ${settings.id}. Run \"walkeros setup store.${id}\" to ensure the sheet exists and is shared with the service account.`,\n );\n }\n if (!probeRes.ok) {\n const text = await safeText(probeRes);\n throw new Error(\n `setup: spreadsheet probe failed (${probeRes.status}): ${text}`,\n );\n }\n\n let headersWritten = false;\n if (options.headers && options.headers.length > 0) {\n const sheet = resolveSheet(settings);\n const range = buildHeaderRange(sheet, options.headers.length);\n const url = `${SHEETS_BASE}/${encodeURIComponent(settings.id)}/values/${encodeURIComponent(range)}?valueInputOption=RAW`;\n const body = { values: [options.headers], range };\n const writeRes = await fetch(url, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n if (!writeRes.ok) {\n const text = await safeText(writeRes);\n throw new Error(\n `setup: header write failed (${writeRes.status}): ${text}`,\n );\n }\n headersWritten = true;\n logger.info('setup: headers written', {\n spreadsheetId: settings.id,\n headers: options.headers.length,\n });\n } else {\n logger.debug('setup: spreadsheet verified, no headers configured', {\n spreadsheetId: settings.id,\n });\n }\n\n return { headersWritten };\n};\n\nfunction assertSheetsSettings(\n settings: Partial<SheetsStoreSettings> | undefined,\n): asserts settings is SheetsStoreSettings {\n if (\n !settings ||\n typeof settings.id !== 'string' ||\n settings.id.length === 0\n ) {\n throw new Error('setup: settings.id (spreadsheet ID) is required');\n }\n}\n\nfunction parseCredentials(\n credentials?: string | ServiceAccountCredentials,\n): ServiceAccountCredentials | undefined {\n if (!credentials) return undefined;\n if (typeof credentials === 'string') {\n let parsed: unknown;\n try {\n parsed = JSON.parse(credentials);\n } catch {\n return undefined;\n }\n if (isServiceAccountShape(parsed)) return parsed;\n return undefined;\n }\n return credentials;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isServiceAccountShape(\n value: unknown,\n): value is ServiceAccountCredentials {\n if (!isRecord(value)) return false;\n return (\n typeof value.client_email === 'string' &&\n typeof value.private_key === 'string'\n );\n}\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return await res.text();\n } catch {\n return '';\n }\n}\n","import type { SheetsStoreSettings } from './types';\n\nexport const SHEETS_BASE = 'https://sheets.googleapis.com/v4/spreadsheets';\n\nexport function resolveSheet(settings: SheetsStoreSettings): string {\n return settings.sheet ?? 'Sheet1';\n}\n\nexport function resolveKeyColumn(settings: SheetsStoreSettings): string {\n return (settings.key ?? 'A').toUpperCase();\n}\n\nexport function resolveValueColumn(settings: SheetsStoreSettings): string {\n return (settings.value ?? 'B').toUpperCase();\n}\n\nexport function resolveHeaderRows(settings: SheetsStoreSettings): number {\n return settings.headerRows ?? 1;\n}\n\n/** Build an A1 range like 'Sheet1!A2:A' for reading a whole column from row N. */\nexport function buildColumnRange(\n sheet: string,\n column: string,\n fromRow: number,\n): string {\n return `${encodeSheet(sheet)}!${column}${fromRow}:${column}`;\n}\n\n/** Build an A1 range like 'Sheet1!B5' for a single cell. */\nexport function buildCellRange(\n sheet: string,\n column: string,\n row: number,\n): string {\n return `${encodeSheet(sheet)}!${column}${row}`;\n}\n\n/** Build an A1 range for writing the header row. */\nexport function buildHeaderRange(sheet: string, columnsCount: number): string {\n const last = columnLetter(columnsCount);\n return `${encodeSheet(sheet)}!A1:${last}1`;\n}\n\n/** Build an A1 range for appending rows across the key+value columns. */\nexport function buildAppendRange(\n sheet: string,\n keyColumn: string,\n valueColumn: string,\n): string {\n return `${encodeSheet(sheet)}!${keyColumn}:${valueColumn}`;\n}\n\n/** Convert a 1-based column index into a column letter (1 -> A, 27 -> AA). */\nexport function columnLetter(index: number): string {\n if (index < 1) {\n throw new Error(`columnLetter: index must be >=1, got ${index}`);\n }\n let n = index;\n let out = '';\n while (n > 0) {\n const rem = (n - 1) % 26;\n out = String.fromCharCode(65 + rem) + out;\n n = Math.floor((n - 1) / 26);\n }\n return out;\n}\n\n/** Sheet names with spaces or special chars must be wrapped in single quotes. */\nexport function encodeSheet(name: string): string {\n if (/^[A-Za-z0-9_]+$/.test(name)) return name;\n return `'${name.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Parse a row index out of a Sheets API `updates.updatedRange` value such as\n * `Sheet1!A5:B5` or `'My Sheet'!A5:B5`. Returns the first row number found.\n */\nexport function parseRowFromRange(range: string): number | undefined {\n const match = /![A-Z]+(\\d+)/.exec(range);\n if (!match) return undefined;\n const row = Number.parseInt(match[1], 10);\n return Number.isFinite(row) ? row : undefined;\n}\n","import type { Logger, Store } from '@walkeros/core';\nimport type {\n ServiceAccountCredentials,\n SheetsStoreSettings,\n Types,\n} from './types';\nimport { createTokenProvider, type TokenProvider } from './auth';\nimport { setup as sheetsSetup } from './setup';\nimport {\n SHEETS_BASE,\n buildAppendRange,\n buildCellRange,\n buildColumnRange,\n parseRowFromRange,\n resolveHeaderRows,\n resolveKeyColumn,\n resolveSheet,\n resolveValueColumn,\n} from './setup-helpers';\n\n/**\n * Module-level cache for the spreadsheet existence pre-check, keyed by\n * spreadsheet ID. Shared across `storeSheetsInit` invocations so the same\n * spreadsheet is probed at most once per process.\n */\nconst spreadsheetExistsCache: Map<string, Promise<boolean>> = new Map();\n\n/** @internal Test-only: clear the existence cache. */\nexport function __resetSpreadsheetExistenceCache(): void {\n spreadsheetExistsCache.clear();\n}\n\n/** @internal Test-only: seed the existence cache for a spreadsheet. */\nexport function __seedSpreadsheetExists(spreadsheetId: string): void {\n spreadsheetExistsCache.set(spreadsheetId, Promise.resolve(true));\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isServiceAccountShape(\n value: unknown,\n): value is ServiceAccountCredentials {\n if (!isRecord(value)) return false;\n return (\n typeof value.client_email === 'string' &&\n typeof value.private_key === 'string'\n );\n}\n\nfunction parseCredentials(\n credentials?: string | ServiceAccountCredentials,\n): ServiceAccountCredentials | undefined {\n if (!credentials) return undefined;\n if (typeof credentials === 'string') {\n let parsed: unknown;\n try {\n parsed = JSON.parse(credentials);\n } catch {\n return undefined;\n }\n if (isServiceAccountShape(parsed)) return parsed;\n return undefined;\n }\n return credentials;\n}\n\nfunction assertSheetsSettings(\n settings: Partial<SheetsStoreSettings> | undefined,\n): asserts settings is SheetsStoreSettings {\n if (\n !settings ||\n typeof settings.id !== 'string' ||\n settings.id.length === 0\n ) {\n throw new Error(\n 'storeSheetsInit: settings.id (spreadsheet ID) is required (non-empty string)',\n );\n }\n}\n\nasync function ensureSpreadsheetExists(\n spreadsheetId: string,\n storeId: string,\n getToken: TokenProvider,\n logger: Logger.Instance,\n): Promise<void> {\n const existing = spreadsheetExistsCache.get(spreadsheetId);\n if (existing !== undefined) {\n await existing;\n return;\n }\n\n const promise = (async (): Promise<boolean> => {\n let res: Response;\n try {\n const token = await getToken();\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}?fields=spreadsheetId`;\n res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n } catch (err) {\n logger.debug('ensureSpreadsheetExists check failed (non-fatal)', {\n spreadsheetId,\n error: err instanceof Error ? err.message : String(err),\n });\n return true;\n }\n\n if (res.status === 404) {\n throw new Error(\n `Spreadsheet not found: ${spreadsheetId}. Run \"walkeros setup store.${storeId}\" to ensure the sheet exists and is shared with the service account.`,\n );\n }\n if (!res.ok) {\n logger.debug('ensureSpreadsheetExists check failed (non-fatal)', {\n spreadsheetId,\n status: res.status,\n });\n }\n return true;\n })();\n\n spreadsheetExistsCache.set(spreadsheetId, promise);\n try {\n await promise;\n } catch (err) {\n spreadsheetExistsCache.delete(spreadsheetId);\n throw err;\n }\n}\n\ninterface KeyColumnResponse {\n values?: unknown[];\n}\n\nfunction readKeyColumnRows(payload: unknown): string[] {\n if (!isRecord(payload)) return [];\n const values = (payload as KeyColumnResponse).values;\n if (!Array.isArray(values)) return [];\n const out: string[] = [];\n for (const row of values) {\n if (Array.isArray(row) && typeof row[0] === 'string') {\n out.push(row[0]);\n } else {\n out.push('');\n }\n }\n return out;\n}\n\nasync function buildKeyIndex(\n spreadsheetId: string,\n sheet: string,\n keyCol: string,\n headerRows: number,\n getToken: TokenProvider,\n): Promise<Map<string, number>> {\n const token = await getToken();\n const range = buildColumnRange(sheet, keyCol, headerRows + 1);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) {\n throw new Error(\n `storeSheetsInit: failed to read key column (${res.status})`,\n );\n }\n const payload: unknown = await res.json();\n const rows = readKeyColumnRows(payload);\n const index = new Map<string, number>();\n rows.forEach((key, i) => {\n if (key) index.set(key, headerRows + i + 1);\n });\n return index;\n}\n\nasync function readCell(\n spreadsheetId: string,\n sheet: string,\n column: string,\n row: number,\n getToken: TokenProvider,\n): Promise<string | undefined> {\n const token = await getToken();\n const range = buildCellRange(sheet, column, row);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) return undefined;\n const payload: unknown = await res.json();\n if (!isRecord(payload)) return undefined;\n const values = (payload as KeyColumnResponse).values;\n if (!Array.isArray(values) || values.length === 0) return undefined;\n const first = values[0];\n if (!Array.isArray(first) || first.length === 0) return undefined;\n const cell = first[0];\n return typeof cell === 'string' ? cell : undefined;\n}\n\nasync function writeCell(\n spreadsheetId: string,\n sheet: string,\n column: string,\n row: number,\n value: string,\n getToken: TokenProvider,\n): Promise<void> {\n const token = await getToken();\n const range = buildCellRange(sheet, column, row);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}?valueInputOption=RAW`;\n const res = await fetch(url, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ values: [[value]], range }),\n });\n if (!res.ok) {\n throw new Error(`storeSheets.writeCell: write failed (${res.status})`);\n }\n}\n\ninterface AppendResponse {\n updates?: { updatedRange?: unknown };\n}\n\nasync function appendRow(\n spreadsheetId: string,\n sheet: string,\n keyCol: string,\n valueCol: string,\n values: [string, string],\n getToken: TokenProvider,\n): Promise<number | undefined> {\n const token = await getToken();\n const range = buildAppendRange(sheet, keyCol, valueCol);\n const url = `${SHEETS_BASE}/${encodeURIComponent(spreadsheetId)}/values/${encodeURIComponent(range)}:append?valueInputOption=RAW`;\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ values: [values] }),\n });\n if (!res.ok) {\n throw new Error(`storeSheets.appendRow: append failed (${res.status})`);\n }\n const payload: unknown = await res.json();\n if (!isRecord(payload)) return undefined;\n const updates = (payload as AppendResponse).updates;\n if (!isRecord(updates)) return undefined;\n const updatedRange = updates.updatedRange;\n if (typeof updatedRange !== 'string') return undefined;\n return parseRowFromRange(updatedRange);\n}\n\nexport const storeSheetsInit: Store.Init<Types> = async (context) => {\n assertSheetsSettings(context.config.settings);\n const settings: SheetsStoreSettings = context.config.settings;\n const { logger } = context;\n const id = context.id;\n const creds = parseCredentials(settings.credentials);\n const getToken = createTokenProvider(creds);\n const sheet = resolveSheet(settings);\n const keyCol = resolveKeyColumn(settings);\n const valueCol = resolveValueColumn(settings);\n const headerRows = resolveHeaderRows(settings);\n\n await ensureSpreadsheetExists(settings.id, id, getToken, logger);\n\n const keyToRow = await buildKeyIndex(\n settings.id,\n sheet,\n keyCol,\n headerRows,\n getToken,\n );\n\n const config: Store.Config<Types> = {\n settings,\n env: context.config.env,\n id: context.config.id,\n logger: context.config.logger,\n };\n\n return {\n type: 'sheets',\n config,\n setup: sheetsSetup,\n\n async get(key: string): Promise<unknown> {\n const row = keyToRow.get(key);\n if (row === undefined) return undefined;\n const cell = await readCell(settings.id, sheet, valueCol, row, getToken);\n if (cell === undefined || cell === '') return undefined;\n try {\n return JSON.parse(cell);\n } catch (err) {\n logger.debug('storeSheets.get: JSON parse failed', {\n key,\n error: err instanceof Error ? err.message : String(err),\n });\n return undefined;\n }\n },\n\n async set(key: string, value: unknown): Promise<void> {\n const serialized = JSON.stringify(value);\n const existingRow = keyToRow.get(key);\n if (existingRow === undefined) {\n const newRow = await appendRow(\n settings.id,\n sheet,\n keyCol,\n valueCol,\n [key, serialized],\n getToken,\n );\n if (newRow !== undefined) keyToRow.set(key, newRow);\n } else {\n await writeCell(\n settings.id,\n sheet,\n valueCol,\n existingRow,\n serialized,\n getToken,\n );\n }\n },\n\n async delete(key: string): Promise<void> {\n const row = keyToRow.get(key);\n if (row === undefined) return;\n await writeCell(settings.id, sheet, valueCol, row, '', getToken);\n },\n };\n};\n"],"mappings":"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","names":["isRecord","isServiceAccountShape","parseCredentials","assertSheetsSettings"]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$meta": {
|
|
3
|
+
"package": "@walkeros/server-store-sheets",
|
|
4
|
+
"version": "4.0.1-next-1778230564486",
|
|
5
|
+
"type": "store",
|
|
6
|
+
"platform": [
|
|
7
|
+
"server"
|
|
8
|
+
],
|
|
9
|
+
"docs": "https://www.walkeros.io/docs/stores/server/sheets",
|
|
10
|
+
"source": "https://github.com/elbwalker/walkerOS/tree/main/packages/server/stores/sheets/src"
|
|
11
|
+
},
|
|
12
|
+
"schemas": {
|
|
13
|
+
"settings": {
|
|
14
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"id": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"minLength": 1,
|
|
20
|
+
"description": "Spreadsheet ID, the segment between /d/ and /edit in the URL"
|
|
21
|
+
},
|
|
22
|
+
"sheet": {
|
|
23
|
+
"default": "Sheet1",
|
|
24
|
+
"description": "Sheet (tab) name within the spreadsheet",
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"key": {
|
|
28
|
+
"default": "A",
|
|
29
|
+
"description": "Column letter for keys (the lookup column)",
|
|
30
|
+
"type": "string"
|
|
31
|
+
},
|
|
32
|
+
"value": {
|
|
33
|
+
"default": "B",
|
|
34
|
+
"description": "Column letter for values (JSON-serialized blob)",
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"headerRows": {
|
|
38
|
+
"default": 1,
|
|
39
|
+
"description": "Number of header rows to skip when reading the key column",
|
|
40
|
+
"type": "integer",
|
|
41
|
+
"minimum": 0,
|
|
42
|
+
"maximum": 9007199254740991
|
|
43
|
+
},
|
|
44
|
+
"credentials": {
|
|
45
|
+
"description": "Service account JSON (string or object). Omit for ADC on Cloud Run/GKE",
|
|
46
|
+
"anyOf": [
|
|
47
|
+
{
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"client_email": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Service account email"
|
|
56
|
+
},
|
|
57
|
+
"private_key": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "RSA private key in PEM format"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"required": [
|
|
63
|
+
"client_email",
|
|
64
|
+
"private_key"
|
|
65
|
+
],
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": [
|
|
72
|
+
"id",
|
|
73
|
+
"sheet",
|
|
74
|
+
"key",
|
|
75
|
+
"value",
|
|
76
|
+
"headerRows"
|
|
77
|
+
],
|
|
78
|
+
"additionalProperties": false
|
|
79
|
+
},
|
|
80
|
+
"setup": {
|
|
81
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"headers": {
|
|
85
|
+
"description": "Header values to write into row 1 of the configured sheet",
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": {
|
|
88
|
+
"type": "string"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"description": "Provisioning options for \"walkeros setup store.<id>\". Idempotent: re-running with the same headers is a no-op overwrite, never alters existing data rows."
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"examples": {
|
|
97
|
+
"cloudRunAdc": {
|
|
98
|
+
"settings": {
|
|
99
|
+
"id": "1AbCdEfGhIjKlMnOpQrStUvWxYz",
|
|
100
|
+
"sheet": "Customers"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"serviceAccount": {
|
|
104
|
+
"settings": {
|
|
105
|
+
"id": "1AbCdEfGhIjKlMnOpQrStUvWxYz",
|
|
106
|
+
"sheet": "Customers",
|
|
107
|
+
"credentials": "$env.SHEETS_SA_KEY"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"step": {
|
|
111
|
+
"readWithAdc": {
|
|
112
|
+
"title": "Read with ADC",
|
|
113
|
+
"description": "Read a value from the Sheets store using ADC, no credentials field needed on Cloud Run or GKE",
|
|
114
|
+
"in": {
|
|
115
|
+
"operation": "get",
|
|
116
|
+
"key": "alice"
|
|
117
|
+
},
|
|
118
|
+
"out": [
|
|
119
|
+
[
|
|
120
|
+
"get",
|
|
121
|
+
"alice",
|
|
122
|
+
"{ tier: \"gold\" }"
|
|
123
|
+
]
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"writeWithServiceAccount": {
|
|
127
|
+
"title": "Write with service account",
|
|
128
|
+
"description": "Append or update a JSON value in the configured sheet using an explicit service account JSON",
|
|
129
|
+
"in": {
|
|
130
|
+
"operation": "set",
|
|
131
|
+
"key": "bob",
|
|
132
|
+
"value": {
|
|
133
|
+
"tier": "silver"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"out": [
|
|
137
|
+
[
|
|
138
|
+
"set",
|
|
139
|
+
"bob",
|
|
140
|
+
"{ tier: \"silver\" }"
|
|
141
|
+
]
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@walkeros/server-store-sheets",
|
|
3
|
+
"description": "Google Sheets store for walkerOS server flows",
|
|
4
|
+
"version": "4.0.1-next-1778230564486",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./dev": {
|
|
16
|
+
"types": "./dist/dev.d.ts",
|
|
17
|
+
"import": "./dist/dev.mjs",
|
|
18
|
+
"require": "./dist/dev.js"
|
|
19
|
+
},
|
|
20
|
+
"./walkerOS.json": "./dist/walkerOS.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/**"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup --silent",
|
|
27
|
+
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
|
28
|
+
"dev": "jest --watchAll --colors",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"lint": "eslint \"**/*.ts*\"",
|
|
31
|
+
"test": "jest",
|
|
32
|
+
"update": "npx npm-check-updates -u && npm update"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@walkeros/core": "4.0.1-next-1778230564486"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {},
|
|
38
|
+
"repository": {
|
|
39
|
+
"url": "git+https://github.com/elbwalker/walkerOS.git",
|
|
40
|
+
"directory": "packages/server/stores/sheets"
|
|
41
|
+
},
|
|
42
|
+
"author": "elbwalker <hello@elbwalker.com>",
|
|
43
|
+
"homepage": "https://github.com/elbwalker/walkerOS#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/elbwalker/walkerOS/issues"
|
|
46
|
+
},
|
|
47
|
+
"walkerOS": {
|
|
48
|
+
"type": "store",
|
|
49
|
+
"platform": [
|
|
50
|
+
"server"
|
|
51
|
+
],
|
|
52
|
+
"docs": "https://www.walkeros.io/docs/stores/server/sheets"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"walkerOS",
|
|
56
|
+
"store",
|
|
57
|
+
"google-sheets",
|
|
58
|
+
"sheets",
|
|
59
|
+
"server"
|
|
60
|
+
],
|
|
61
|
+
"funding": [
|
|
62
|
+
{
|
|
63
|
+
"type": "GitHub Sponsors",
|
|
64
|
+
"url": "https://github.com/sponsors/elbwalker"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|