@sysa-ivan/gas-helpers 1.0.0

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 ADDED
@@ -0,0 +1,36 @@
1
+ # gas-helpers
2
+
3
+ Helper utilities for Google Apps Script: UI, triggers, Telegram notifications, Google Sheets (Entity/Repository).
4
+
5
+ > **For AI agents**: See [AGENTS.md](./AGENTS.md) for imports, patterns, and GAS caveats.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install gas-helpers
11
+ ```
12
+
13
+ ## Modules
14
+
15
+ | Module | Description |
16
+ | ---------------------------------------- | ----------------------------------------------------------------------- |
17
+ | [ui](./src/ui/README.md) | Spreadsheet UI dialogs (alert, confirm, prompt) with availability check |
18
+ | [triggers](./src/triggers/README.md) | Managing time-based triggers in Google Apps Script |
19
+ | [telegram](./src/telegram/README.md) | Sending notifications to Telegram via Bot API |
20
+ | [sheet](./src/sheet/README.md) | Entity and Repository for working with Google Sheets data |
21
+ | [properties](./src/properties/README.md) | ScriptProperty for typed key-value storage in script properties |
22
+
23
+ ## Requirements
24
+
25
+ - Google Apps Script
26
+ - `@types/google-apps-script` (peer dependency)
27
+
28
+ ## Common Pitfalls
29
+
30
+ - **Ui unavailable**: When script runs from a trigger, `onEdit`, or time-driven execution, `SpreadsheetApp.getUi()` throws. Always use `Ui.isAvailable()` before `alert`, `confirm`, `prompt`.
31
+ - **Secrets**: Store `botToken` in `PropertiesService.getScriptProperties()`, not in source code.
32
+ - **Entity columns**: Column indices are 0-based (same as array indices).
33
+
34
+ ## License
35
+
36
+ ISC
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Type-safe wrapper for PropertiesService.getScriptProperties().
3
+ * Stores values as JSON. Use for script state, not configuration.
4
+ *
5
+ * @template T - Stored value type (typically an object)
6
+ */
7
+ export declare class ScriptProperty<T> {
8
+ private readonly key;
9
+ private readonly defaults?;
10
+ constructor(key: string, defaults?: T | undefined);
11
+ /**
12
+ * Reads value from script properties.
13
+ * @returns Parsed value or null if not set (or defaults if provided in constructor)
14
+ */
15
+ get: () => T | null;
16
+ /**
17
+ * Writes value to script properties.
18
+ * @param value - Value to store (serialized as JSON)
19
+ */
20
+ set: (value: T) => void;
21
+ /**
22
+ * Removes the property.
23
+ */
24
+ delete: () => void;
25
+ /**
26
+ * Checks if the property exists (non-empty value).
27
+ */
28
+ exists: () => boolean;
29
+ /**
30
+ * Returns stored value or fallback. Uses constructor defaults when def is omitted.
31
+ * @param def - Override when no defaults in constructor; required if no defaults
32
+ * @throws Error when value is missing and neither def nor constructor defaults provided
33
+ */
34
+ getOrDefault: (def?: T) => T;
35
+ }
@@ -0,0 +1,38 @@
1
+ class o {
2
+ constructor(r, i) {
3
+ this.key = r, this.defaults = i, this.get = () => {
4
+ const t = PropertiesService.getScriptProperties().getProperty(
5
+ this.key
6
+ );
7
+ if (t == null) return this.defaults ?? null;
8
+ try {
9
+ return JSON.parse(t);
10
+ } catch {
11
+ return this.defaults ?? null;
12
+ }
13
+ }, this.set = (t) => {
14
+ PropertiesService.getScriptProperties().setProperty(
15
+ this.key,
16
+ JSON.stringify(t)
17
+ );
18
+ }, this.delete = () => {
19
+ PropertiesService.getScriptProperties().deleteProperty(this.key);
20
+ }, this.exists = () => {
21
+ const t = PropertiesService.getScriptProperties().getProperty(
22
+ this.key
23
+ );
24
+ return t != null;
25
+ }, this.getOrDefault = (t) => {
26
+ const e = this.get();
27
+ if (e !== null) return e;
28
+ if (t !== void 0) return t;
29
+ if (this.defaults !== void 0) return this.defaults;
30
+ throw new Error(
31
+ `ScriptProperty "${this.key}" has no value and no defaults`
32
+ );
33
+ };
34
+ }
35
+ }
36
+ export {
37
+ o as ScriptProperty
38
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Value transformation when reading from sheet (from) and writing (to).
3
+ * @template T - Value type
4
+ */
5
+ export type Transformer<T> = {
6
+ /** When reading row from sheet */
7
+ from?: (value: unknown) => T;
8
+ /** When writing to sheet */
9
+ to?: (value: T) => unknown;
10
+ };
11
+ /** Entity configuration: columns, transformers, optional, defaults, primaryKey */
12
+ export type EntityConfig<T extends Record<string, any>> = {
13
+ columns: Record<keyof T, number>;
14
+ transformers?: {
15
+ [K in keyof T]?: Transformer<T[K]>;
16
+ };
17
+ optional?: ReadonlyArray<keyof T>;
18
+ defaults?: {
19
+ [K in keyof T]?: () => T[K];
20
+ };
21
+ primaryKey?: string;
22
+ };
23
+ /**
24
+ * Base class for a Google Sheets table row.
25
+ * Maps array row[] to object and back via config.
26
+ * @template T - Entity data type
27
+ */
28
+ export declare abstract class Entity<T extends Record<string, any>> {
29
+ protected static config: EntityConfig<any>;
30
+ private _original?;
31
+ constructor(data?: Partial<T>);
32
+ /**
33
+ * Creates entity from sheet row (array of values).
34
+ * @param row - Array of cell values (0-based indices from config.columns)
35
+ * @returns Entity of type T
36
+ * @example const user = UserEntity.fromRow([1, 'Alice', 'a@x.com']);
37
+ */
38
+ static fromRow<T>(this: new (data: T) => any, row: any[]): T;
39
+ /**
40
+ * Converts entity to row for writing to sheet.
41
+ * validate() is called before conversion.
42
+ * @returns Array of values for cells
43
+ */
44
+ toRow(): any[];
45
+ /** Validates entity before toRow(). Throws Error on invalid data. */
46
+ abstract validate(): void;
47
+ /**
48
+ * Checks if data changed after fromRow or insert.
49
+ * @returns true if data was modified
50
+ */
51
+ isDirty(): boolean;
52
+ /**
53
+ * Returns primary key (primaryKey or _rowIndex).
54
+ * @throws Error if primaryKey not set and _rowIndex is missing
55
+ */
56
+ getPrimaryKey(): any;
57
+ }
@@ -0,0 +1,82 @@
1
+ import { Entity, EntityConfig } from './Entity';
2
+ /**
3
+ * Repository for working with a Google Sheets tab.
4
+ * Cache, Unit of Work, CRUD. First row is headers, data from row 2.
5
+ * @template T - Entity data type
6
+ * @template E - Entity class
7
+ */
8
+ export declare abstract class Repository<T extends Record<string, any>, E extends Entity<T>> {
9
+ protected cache: (E & {
10
+ _rowIndex: number;
11
+ })[];
12
+ protected dirty: Set<E>;
13
+ protected toDelete: Set<E>;
14
+ protected abstract entity: {
15
+ new (data?: Partial<T>): E;
16
+ fromRow(row: any[]): E;
17
+ config: EntityConfig<T>;
18
+ };
19
+ protected abstract sheetName: string;
20
+ private _sheet?;
21
+ get sheet(): GoogleAppsScript.Spreadsheet.Sheet;
22
+ /**
23
+ * Loads data from sheet into cache. First row = headers, data from row 2.
24
+ * @param options - fromRow, toRow, columns (projection)
25
+ * @example repo.load(); repo.load({ fromRow: 2, toRow: 100 });
26
+ */
27
+ load(options?: {
28
+ fromRow?: number;
29
+ toRow?: number;
30
+ columns?: (keyof T)[];
31
+ }): void;
32
+ /** All entities from cache. */
33
+ findAll(): (E & {
34
+ _rowIndex: number;
35
+ })[];
36
+ /** Entity by row number or null. */
37
+ findByRowIndex(rowIndex: number): (E & {
38
+ _rowIndex: number;
39
+ }) | null;
40
+ /** Array of entities with given field value. */
41
+ findBy<K extends keyof T>(field: K, value: T[K]): (E & {
42
+ _rowIndex: number;
43
+ })[];
44
+ /** First entity with given value or null. */
45
+ findOne<K extends keyof T>(field: K, value: T[K]): (E & {
46
+ _rowIndex: number;
47
+ }) | null;
48
+ /** Checks if at least one record with given value exists. */
49
+ exists<K extends keyof T>(field: K, value: T[K]): boolean;
50
+ /** Number of records in cache. */
51
+ count(): number;
52
+ /** Number of records with given field value. */
53
+ countBy<K extends keyof T>(field: K, value: T[K]): number;
54
+ /** Adds entity to dirty set if isDirty(). */
55
+ save(entity: E & {
56
+ _rowIndex: number;
57
+ }): void;
58
+ /** Updates entities via callback and adds to dirty. */
59
+ update(entities: (E & {
60
+ _rowIndex: number;
61
+ })[], updater: (entity: E) => void): void;
62
+ /** Marks entity for deletion on commit(). */
63
+ delete(entity: E & {
64
+ _rowIndex: number;
65
+ }): void;
66
+ /**
67
+ * Applies all changes (dirty, toDelete) to sheet and reloads cache.
68
+ * @example repo.save(entity); repo.delete(entity); repo.commit();
69
+ */
70
+ commit(): void;
71
+ /** Inserts one row, updates _rowIndex and cache. */
72
+ insert(entity: E): void;
73
+ /** Inserts multiple rows in a single API call. */
74
+ insertBatch(entities: E[]): void;
75
+ /** save() if _rowIndex exists, otherwise insert(). */
76
+ upsert(entity: E & {
77
+ _rowIndex?: number;
78
+ }): void;
79
+ /** Removes all data rows (except header), clears cache. */
80
+ clear(): void;
81
+ private _snapshotColumns;
82
+ }
@@ -0,0 +1,2 @@
1
+ export { Entity, type EntityConfig, type Transformer } from './Entity';
2
+ export { Repository } from './Repository';
package/dist/sheet.js ADDED
@@ -0,0 +1,227 @@
1
+ class x {
2
+ constructor(t) {
3
+ const s = this.constructor.config;
4
+ if (s?.defaults)
5
+ for (const e in s.defaults)
6
+ this[e] = s.defaults[e]();
7
+ t && Object.assign(this, t);
8
+ }
9
+ /**
10
+ * Creates entity from sheet row (array of values).
11
+ * @param row - Array of cell values (0-based indices from config.columns)
12
+ * @returns Entity of type T
13
+ * @example const user = UserEntity.fromRow([1, 'Alice', 'a@x.com']);
14
+ */
15
+ static fromRow(t) {
16
+ const s = new this({}), o = this.config, r = o.columns, n = o.transformers ?? {};
17
+ for (const i in r) {
18
+ const a = r[i];
19
+ let l = t[a];
20
+ n[i]?.from && (l = n[i].from(l)), s[i] = l;
21
+ }
22
+ const c = {};
23
+ for (const i in r)
24
+ c[i] = s[i];
25
+ return s._original = c, s;
26
+ }
27
+ /**
28
+ * Converts entity to row for writing to sheet.
29
+ * validate() is called before conversion.
30
+ * @returns Array of values for cells
31
+ */
32
+ toRow() {
33
+ this.validate();
34
+ const s = this.constructor.config, e = s.columns, o = s.transformers ?? {}, r = new Set(s.optional ?? []), n = [];
35
+ for (const c in e) {
36
+ let i = this[c];
37
+ if (i === void 0 && r.has(c)) {
38
+ n[e[c]] = "";
39
+ continue;
40
+ }
41
+ o[c]?.to && (i = o[c].to(i)), n[e[c]] = i;
42
+ }
43
+ return n;
44
+ }
45
+ /**
46
+ * Checks if data changed after fromRow or insert.
47
+ * @returns true if data was modified
48
+ */
49
+ isDirty() {
50
+ if (!this._original) return !0;
51
+ const s = this.constructor.config.columns;
52
+ for (const e in s) {
53
+ const o = this[e], r = this._original[e];
54
+ if (o instanceof Date && r instanceof Date) {
55
+ if (o.getTime() !== r.getTime()) return !0;
56
+ } else if (Array.isArray(o) && Array.isArray(r)) {
57
+ if (JSON.stringify(o) !== JSON.stringify(r))
58
+ return !0;
59
+ } else if (o !== r)
60
+ return !0;
61
+ }
62
+ return !1;
63
+ }
64
+ /**
65
+ * Returns primary key (primaryKey or _rowIndex).
66
+ * @throws Error if primaryKey not set and _rowIndex is missing
67
+ */
68
+ getPrimaryKey() {
69
+ const t = this.constructor;
70
+ if (t.config.primaryKey)
71
+ return this[t.config.primaryKey];
72
+ const s = this._rowIndex;
73
+ if (s == null)
74
+ throw new Error("Primary key not defined and rowIndex not set");
75
+ return s;
76
+ }
77
+ }
78
+ class p {
79
+ constructor() {
80
+ this.cache = [], this.dirty = /* @__PURE__ */ new Set(), this.toDelete = /* @__PURE__ */ new Set();
81
+ }
82
+ get sheet() {
83
+ if (!this._sheet) {
84
+ const t = SpreadsheetApp.getActive();
85
+ if (this._sheet = t.getSheetByName(this.sheetName), !this._sheet)
86
+ throw new Error(`Sheet ${this.sheetName} not found`);
87
+ }
88
+ return this._sheet;
89
+ }
90
+ /**
91
+ * Loads data from sheet into cache. First row = headers, data from row 2.
92
+ * @param options - fromRow, toRow, columns (projection)
93
+ * @example repo.load(); repo.load({ fromRow: 2, toRow: 100 });
94
+ */
95
+ load(t) {
96
+ const s = t?.fromRow ?? 2, e = t?.toRow ?? this.sheet.getLastRow();
97
+ if (e < s) return;
98
+ const o = this.entity.config.columns, n = (t?.columns ?? Object.keys(o)).map((h) => o[h]), c = Math.min(...n), a = Math.max(...n) - c + 1, l = e - s + 1, f = this.sheet.getRange(s, c + 1, l, a).getValues();
99
+ this.cache = [];
100
+ for (let h = 0; h < f.length; h++) {
101
+ const u = [];
102
+ for (const y of Object.keys(o)) {
103
+ const g = o[y], w = n.indexOf(g);
104
+ u[g] = w >= 0 ? f[h][w] : void 0;
105
+ }
106
+ const d = this.entity.fromRow(u);
107
+ d._rowIndex = s + h, this.cache.push(d);
108
+ }
109
+ }
110
+ /** All entities from cache. */
111
+ findAll() {
112
+ return this.cache;
113
+ }
114
+ /** Entity by row number or null. */
115
+ findByRowIndex(t) {
116
+ return this.cache.find((s) => s._rowIndex === t) ?? null;
117
+ }
118
+ /** Array of entities with given field value. */
119
+ findBy(t, s) {
120
+ return this.cache.filter((e) => e[t] === s);
121
+ }
122
+ /** First entity with given value or null. */
123
+ findOne(t, s) {
124
+ return this.cache.find((e) => e[t] === s) ?? null;
125
+ }
126
+ /** Checks if at least one record with given value exists. */
127
+ exists(t, s) {
128
+ return this.cache.some((e) => e[t] === s);
129
+ }
130
+ /** Number of records in cache. */
131
+ count() {
132
+ return this.cache.length;
133
+ }
134
+ /** Number of records with given field value. */
135
+ countBy(t, s) {
136
+ return this.cache.filter((e) => e[t] === s).length;
137
+ }
138
+ /** Adds entity to dirty set if isDirty(). */
139
+ save(t) {
140
+ t.isDirty() && this.dirty.add(t);
141
+ }
142
+ /** Updates entities via callback and adds to dirty. */
143
+ update(t, s) {
144
+ t.forEach((e) => {
145
+ s(e), this.save(e);
146
+ });
147
+ }
148
+ /** Marks entity for deletion on commit(). */
149
+ delete(t) {
150
+ this.toDelete.add(t);
151
+ }
152
+ /**
153
+ * Applies all changes (dirty, toDelete) to sheet and reloads cache.
154
+ * @example repo.save(entity); repo.delete(entity); repo.commit();
155
+ */
156
+ commit() {
157
+ const t = Array.from(this.dirty).map((e) => ({
158
+ rowIndex: e._rowIndex,
159
+ row: e.toRow().map((o) => o ?? "")
160
+ })).sort((e, o) => e.rowIndex - o.rowIndex);
161
+ if (t.length > 0) {
162
+ const e = Math.max(...t.map((n) => n.row.length)), o = [];
163
+ let r = null;
164
+ for (const { rowIndex: n, row: c } of t) {
165
+ const i = [
166
+ ...c,
167
+ ...new Array(e - c.length).fill("")
168
+ ];
169
+ r && n === r.startRow + r.rows.length ? r.rows.push(i) : (r = { startRow: n, rows: [i] }, o.push(r));
170
+ }
171
+ for (const n of o)
172
+ this.sheet.getRange(n.startRow, 1, n.rows.length, e).setValues(n.rows);
173
+ }
174
+ this.dirty.clear();
175
+ const s = Array.from(this.toDelete).map((e) => e._rowIndex).filter(Boolean).sort((e, o) => o - e);
176
+ for (const e of s)
177
+ this.sheet.getRange(
178
+ e,
179
+ 1,
180
+ 1,
181
+ Math.max(...Object.values(this.entity.config.columns)) + 1
182
+ ).clearContent();
183
+ this.toDelete.clear(), this.load();
184
+ }
185
+ /** Inserts one row, updates _rowIndex and cache. */
186
+ insert(t) {
187
+ const s = t.toRow().map((o) => o ?? ""), e = this.sheet.getLastRow() + 1;
188
+ this.sheet.getRange(e, 1, 1, s.length).setValues([s]), t._rowIndex = e, t._original = this._snapshotColumns(t), this.cache.push(t);
189
+ }
190
+ /** Inserts multiple rows in a single API call. */
191
+ insertBatch(t) {
192
+ if (t.length === 0) return;
193
+ const s = this.sheet.getLastRow() + 1, e = t.map((n) => n.toRow()), o = Math.max(...e.map((n) => n.length)), r = e.map((n) => {
194
+ const c = new Array(o).fill("");
195
+ return n.forEach((i, a) => {
196
+ c[a] = i ?? "";
197
+ }), c;
198
+ });
199
+ this.sheet.getRange(s, 1, t.length, o).setValues(r), t.forEach((n, c) => {
200
+ n._rowIndex = s + c, n._original = this._snapshotColumns(n), this.cache.push(n);
201
+ });
202
+ }
203
+ /** save() if _rowIndex exists, otherwise insert(). */
204
+ upsert(t) {
205
+ t._rowIndex !== null && t._rowIndex !== void 0 ? this.save(t) : this.insert(t);
206
+ }
207
+ /** Removes all data rows (except header), clears cache. */
208
+ clear() {
209
+ const t = this.sheet.getLastRow();
210
+ t <= 1 || (this.sheet.getRange(
211
+ 2,
212
+ 1,
213
+ t - 1,
214
+ Math.max(...Object.values(this.entity.config.columns)) + 1
215
+ ).clearContent(), this.cache = [], this.dirty.clear(), this.toDelete.clear());
216
+ }
217
+ _snapshotColumns(t) {
218
+ const s = this.entity.config.columns, e = {};
219
+ for (const o in s)
220
+ e[o] = t[o];
221
+ return e;
222
+ }
223
+ }
224
+ export {
225
+ x as Entity,
226
+ p as Repository
227
+ };
@@ -0,0 +1,30 @@
1
+ /** Parameters for TelegramNotifier constructor */
2
+ export type TelegramNotifierParams = {
3
+ /** Bot token from @BotFather */
4
+ botToken: string;
5
+ /** Chat ID for sending messages */
6
+ chatId: string;
7
+ /** Spreadsheet name — prepended to messages (optional) */
8
+ spreadsheetName?: string;
9
+ };
10
+ /**
11
+ * Sends notifications to Telegram via Bot API.
12
+ * Uses UrlFetchApp for HTTP requests.
13
+ */
14
+ export declare class TelegramNotifier {
15
+ private readonly apiUrl;
16
+ private readonly chatId;
17
+ private readonly spreadsheetName;
18
+ /**
19
+ * @param params - botToken, chatId, spreadsheetName (optional)
20
+ */
21
+ constructor(params: TelegramNotifierParams);
22
+ /**
23
+ * Sends a message to Telegram. Logs errors; does not throw.
24
+ * @param message - Message text
25
+ * @param disableNotification - Mute notification sound (default true)
26
+ * @param spreadsheetName - Spreadsheet name prefix (default from constructor)
27
+ * @example notifier.sendNotification('Error in script');
28
+ */
29
+ sendNotification(message: string, disableNotification?: boolean, spreadsheetName?: string): void;
30
+ }
@@ -0,0 +1,46 @@
1
+ class c {
2
+ /**
3
+ * @param params - botToken, chatId, spreadsheetName (optional)
4
+ */
5
+ constructor(t) {
6
+ const { botToken: s, chatId: o, spreadsheetName: e = "" } = t;
7
+ this.apiUrl = `https://api.telegram.org/bot${s}/sendMessage`, this.chatId = o, this.spreadsheetName = e;
8
+ }
9
+ /**
10
+ * Sends a message to Telegram. Logs errors; does not throw.
11
+ * @param message - Message text
12
+ * @param disableNotification - Mute notification sound (default true)
13
+ * @param spreadsheetName - Spreadsheet name prefix (default from constructor)
14
+ * @example notifier.sendNotification('Error in script');
15
+ */
16
+ sendNotification(t, s = !0, o = this.spreadsheetName) {
17
+ try {
18
+ const e = o ? `${o}
19
+ ${t}` : t, r = {
20
+ chat_id: this.chatId,
21
+ text: e,
22
+ disable_notification: s
23
+ }, n = UrlFetchApp.fetch(this.apiUrl, {
24
+ method: "post",
25
+ headers: {
26
+ "Content-Type": "application/json"
27
+ },
28
+ payload: JSON.stringify(r),
29
+ muteHttpExceptions: !0
30
+ }), a = n.getResponseCode();
31
+ if (a !== 200) {
32
+ const i = n.getContentText();
33
+ Logger.log(
34
+ `Error sending message to Telegram: ${a} - ${i}`
35
+ );
36
+ } else
37
+ Logger.log(`Message sent to Telegram: ${t}`);
38
+ } catch (e) {
39
+ const r = e instanceof Error ? e.message : String(e);
40
+ Logger.log(`Error sending message to Telegram: ${r}`);
41
+ }
42
+ }
43
+ }
44
+ export {
45
+ c as TelegramNotifier
46
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Manages time-based triggers in Google Apps Script.
3
+ * Create, check and delete triggers by handler function name.
4
+ */
5
+ export declare class Trigger {
6
+ private readonly functionName;
7
+ /**
8
+ * @param functionName - Name of the trigger handler function
9
+ */
10
+ constructor(functionName: string);
11
+ /**
12
+ * Checks if a clock trigger exists for this function.
13
+ * @returns true if trigger exists
14
+ */
15
+ has: () => boolean;
16
+ /** Deletes one trigger for this function. */
17
+ delete: () => void;
18
+ /** Deletes all triggers for this function. */
19
+ deleteAll: () => void;
20
+ /**
21
+ * Creates a one-time trigger to run after specified minutes.
22
+ * @param afterMinutes - Minutes until execution
23
+ * @param removeOld - Remove existing triggers before creating (default true)
24
+ * @example trigger.set(5); // run myHandler in 5 minutes
25
+ */
26
+ set: (afterMinutes: number, removeOld?: boolean) => void;
27
+ /**
28
+ * Creates a recurring trigger. Skips if one already exists.
29
+ * @param intervalMinutes - Interval in minutes (skips if trigger already exists)
30
+ * @example trigger.setRecurring(60); // every hour
31
+ */
32
+ setRecurring: (intervalMinutes: number) => void;
33
+ }
@@ -0,0 +1,28 @@
1
+ class g {
2
+ /**
3
+ * @param functionName - Name of the trigger handler function
4
+ */
5
+ constructor(r) {
6
+ this.functionName = r, this.has = () => ScriptApp.getProjectTriggers().some(
7
+ (e) => e.getHandlerFunction() === this.functionName && e.getTriggerSource() === ScriptApp.TriggerSource.CLOCK
8
+ ), this.delete = () => {
9
+ const e = ScriptApp.getProjectTriggers().find(
10
+ (t) => t.getHandlerFunction() === this.functionName
11
+ );
12
+ e && ScriptApp.deleteTrigger(e);
13
+ }, this.deleteAll = () => {
14
+ ScriptApp.getProjectTriggers().forEach((e) => {
15
+ e.getHandlerFunction() === this.functionName && ScriptApp.deleteTrigger(e);
16
+ });
17
+ }, this.set = (e, t = !0) => {
18
+ t && this.deleteAll(), ScriptApp.newTrigger(this.functionName).timeBased().after(e * 60 * 1e3).create();
19
+ }, this.setRecurring = (e) => {
20
+ this.has() || (ScriptApp.newTrigger(this.functionName).timeBased().everyMinutes(e).create(), Logger.log(
21
+ `[setRecurringTrigger] Created ${this.functionName} (every ${e} min)`
22
+ ));
23
+ };
24
+ }
25
+ }
26
+ export {
27
+ g as Trigger
28
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Utilities for Google Sheets UI (alert, confirm, prompt dialogs).
3
+ * Methods safely check UI availability (e.g. when running from a trigger).
4
+ */
5
+ export declare class Ui {
6
+ /**
7
+ * Checks if Spreadsheet UI is available (e.g. when document is open by user).
8
+ * UI is NOT available when running from triggers, onEdit, or time-driven scripts.
9
+ * @returns true if UI is available
10
+ * @example
11
+ * if (Ui.isAvailable()) Ui.alert('Hello');
12
+ */
13
+ static isAvailable: () => boolean;
14
+ /**
15
+ * Shows an alert dialog. No-op if UI unavailable.
16
+ * @param message - Message text
17
+ * @example Ui.alert('Done');
18
+ */
19
+ static alert: (message: string) => void;
20
+ /**
21
+ * Shows a confirm dialog (OK/Cancel).
22
+ * @param title - Dialog title
23
+ * @param message - Message text
24
+ * @returns true if user clicked OK
25
+ */
26
+ static confirm: (title: string, message: string) => boolean;
27
+ /**
28
+ * Shows a prompt dialog for text input.
29
+ * @param title - Dialog title
30
+ * @param message - Prompt hint
31
+ * @returns Entered text or null if cancelled
32
+ */
33
+ static prompt: (title: string, message: string) => string | null;
34
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,23 @@
1
+ const t = class t {
2
+ };
3
+ t.isAvailable = () => {
4
+ try {
5
+ return SpreadsheetApp.getUi(), !0;
6
+ } catch {
7
+ return !1;
8
+ }
9
+ }, t.alert = (r) => {
10
+ t.isAvailable() && SpreadsheetApp.getUi().alert(r);
11
+ }, t.confirm = (r, s) => {
12
+ if (!t.isAvailable()) return !1;
13
+ const e = SpreadsheetApp.getUi();
14
+ return e.alert(r, s, e.ButtonSet.OK_CANCEL) === e.Button.OK;
15
+ }, t.prompt = (r, s) => {
16
+ if (!t.isAvailable()) return null;
17
+ const e = SpreadsheetApp.getUi(), a = e.prompt(r, s, e.ButtonSet.OK_CANCEL);
18
+ return a.getSelectedButton() === e.Button.OK ? a.getResponseText() : null;
19
+ };
20
+ let i = t;
21
+ export {
22
+ i as Ui
23
+ };
@@ -0,0 +1,30 @@
1
+ import gts from 'gts';
2
+ import tseslint from 'typescript-eslint';
3
+
4
+ export default [
5
+ {
6
+ ignores: [
7
+ '**/node_modules/*',
8
+ 'build/*',
9
+ 'dist/*',
10
+ 'testing',
11
+ 'template/**/*',
12
+ 'template-ui/**/*',
13
+ ],
14
+ },
15
+ ...gts,
16
+ {
17
+ plugins: { '@typescript-eslint': tseslint.plugin },
18
+ rules: {
19
+ '@typescript-eslint/no-unused-vars': [
20
+ 'error',
21
+ {
22
+ argsIgnorePattern: '^_',
23
+ varsIgnorePattern: 'onOpen',
24
+ caughtErrorsIgnorePattern: '^_',
25
+ },
26
+ ],
27
+ '@typescript-eslint/no-explicit-any': 'off',
28
+ },
29
+ },
30
+ ];
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@sysa-ivan/gas-helpers",
3
+ "version": "1.0.0",
4
+ "description": "Helper utilities for Google Apps Script: UI dialogs, triggers, Telegram notifications, Entity/Repository for Sheets",
5
+ "keywords": [
6
+ "google-apps-script",
7
+ "gas",
8
+ "spreadsheet",
9
+ "google-sheets",
10
+ "triggers",
11
+ "entity",
12
+ "repository",
13
+ "unit-of-work",
14
+ "properties",
15
+ "script-properties"
16
+ ],
17
+ "private": false,
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "exports": {
21
+ "./triggers": {
22
+ "types": "./dist/triggers/index.d.ts",
23
+ "default": "./dist/triggers.js"
24
+ },
25
+ "./telegram": {
26
+ "types": "./dist/telegram/index.d.ts",
27
+ "default": "./dist/telegram.js"
28
+ },
29
+ "./sheet": {
30
+ "types": "./dist/sheet/index.d.ts",
31
+ "default": "./dist/sheet.js"
32
+ },
33
+ "./ui": {
34
+ "types": "./dist/ui/index.d.ts",
35
+ "default": "./dist/ui.js"
36
+ },
37
+ "./properties": {
38
+ "types": "./dist/properties/index.d.ts",
39
+ "default": "./dist/properties.js"
40
+ },
41
+ "./eslint": "./eslint.config.mjs",
42
+ "./prettier": "./prettier.config.js"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "*.mjs",
47
+ "*.js"
48
+ ],
49
+ "scripts": {
50
+ "build": "rimraf dist && vite build",
51
+ "lint": "eslint .",
52
+ "format": "prettier --check .",
53
+ "test": "vitest",
54
+ "test:run": "vitest run",
55
+ "pr-ready": "npm run format -- --write && npm run lint && npm run test:run && npm run build"
56
+ },
57
+ "peerDependencies": {
58
+ "@types/google-apps-script": "^2.0.8",
59
+ "eslint": ">=9",
60
+ "prettier": ">=3",
61
+ "typescript": ">=5"
62
+ },
63
+ "dependencies": {
64
+ "eslint-config-prettier": "^10.1.8",
65
+ "eslint-plugin-prettier": "^5.5.5",
66
+ "gts": "^7.0.0",
67
+ "typescript-eslint": "^8.46.0",
68
+ "prettier-plugin-organize-imports": "^4.3.0"
69
+ },
70
+ "devDependencies": {
71
+ "@types/google-apps-script": "^2.0.8",
72
+ "rimraf": "^6.0.0",
73
+ "@types/node": "^25.3.3",
74
+ "vite": "^7.3.1",
75
+ "vite-plugin-dts": "^4.5.4",
76
+ "vitest": "^4.0.18"
77
+ }
78
+ }
@@ -0,0 +1,13 @@
1
+ /** @type {import('prettier').Config} */
2
+ export default {
3
+ plugins: ['prettier-plugin-organize-imports'],
4
+ arrowParens: 'always',
5
+ bracketSpacing: true,
6
+ bracketSameLine: false,
7
+ printWidth: 80,
8
+ semi: true,
9
+ singleQuote: true,
10
+ trailingComma: 'es5',
11
+ tabWidth: 4,
12
+ useTabs: true,
13
+ };