@tomorrowos/sdk 0.4.7 → 0.5.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.
@@ -0,0 +1,435 @@
1
+ import { Pool } from "pg";
2
+ function optionalString(value) {
3
+ return value ?? undefined;
4
+ }
5
+ function optionalNumber(value) {
6
+ if (value == null)
7
+ return undefined;
8
+ const parsed = Number(value);
9
+ return Number.isFinite(parsed) ? parsed : undefined;
10
+ }
11
+ function parseJson(raw, fallback) {
12
+ if (!raw)
13
+ return fallback;
14
+ return JSON.parse(raw);
15
+ }
16
+ /**
17
+ * Durable store backed by Postgres. Supabase works through its Postgres
18
+ * connection string, usually provided as DATABASE_URL.
19
+ */
20
+ export class PostgresStore {
21
+ pool;
22
+ ready;
23
+ constructor(options) {
24
+ const connectionString = typeof options === "string" ? options : options.connectionString;
25
+ const ssl = typeof options === "string" ? undefined : options.ssl;
26
+ if (!connectionString.trim()) {
27
+ throw new Error("PostgresStore requires a connection string.");
28
+ }
29
+ this.pool = new Pool({ connectionString, ssl });
30
+ }
31
+ async init() {
32
+ await this.pool.query(`
33
+ CREATE TABLE IF NOT EXISTS schema_migrations (
34
+ id INTEGER PRIMARY KEY,
35
+ name TEXT NOT NULL UNIQUE,
36
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS pending_codes (
40
+ code TEXT PRIMARY KEY,
41
+ device_id TEXT NOT NULL,
42
+ created_at BIGINT NOT NULL
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS device_registry (
46
+ device_id TEXT PRIMARY KEY,
47
+ permanent_pairing_code TEXT NOT NULL UNIQUE,
48
+ code_created_at BIGINT NOT NULL,
49
+ serial_number TEXT,
50
+ first_seen_at BIGINT,
51
+ last_hello_at BIGINT
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS paired_devices (
55
+ device_id TEXT PRIMARY KEY,
56
+ pairing_token TEXT NOT NULL,
57
+ paired_at TEXT NOT NULL,
58
+ device_name TEXT,
59
+ platform TEXT,
60
+ system TEXT,
61
+ last_boot_at TEXT,
62
+ last_online_at TEXT,
63
+ last_offline_at TEXT,
64
+ last_policy_push_at TEXT
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS playlists (
68
+ id TEXT PRIMARY KEY,
69
+ name TEXT NOT NULL,
70
+ schedule_json TEXT,
71
+ items_json TEXT NOT NULL DEFAULT '[]',
72
+ version INTEGER NOT NULL,
73
+ updated_at TEXT NOT NULL,
74
+ retired BOOLEAN NOT NULL DEFAULT false,
75
+ retired_at TEXT
76
+ );
77
+
78
+ CREATE UNIQUE INDEX IF NOT EXISTS playlists_active_name_unique
79
+ ON playlists (lower(trim(name)))
80
+ WHERE retired = false;
81
+
82
+ CREATE TABLE IF NOT EXISTS device_assignments (
83
+ device_id TEXT NOT NULL,
84
+ playlist_id TEXT NOT NULL,
85
+ sort_order INTEGER NOT NULL,
86
+ published_version INTEGER NOT NULL,
87
+ published_at TEXT NOT NULL,
88
+ snapshot_json TEXT NOT NULL,
89
+ PRIMARY KEY (device_id, playlist_id)
90
+ );
91
+
92
+ CREATE INDEX IF NOT EXISTS device_assignments_device_order_idx
93
+ ON device_assignments (device_id, sort_order);
94
+
95
+ INSERT INTO schema_migrations (id, name)
96
+ VALUES (1, 'initial_tomorrowos_store')
97
+ ON CONFLICT (id) DO NOTHING;
98
+ `);
99
+ }
100
+ async close() {
101
+ await this.pool.end();
102
+ }
103
+ async ensureReady() {
104
+ this.ready ??= this.init();
105
+ await this.ready;
106
+ }
107
+ async setPendingCode(code, record) {
108
+ await this.ensureReady();
109
+ await this.pool.query(`
110
+ INSERT INTO pending_codes (code, device_id, created_at)
111
+ VALUES ($1, $2, $3)
112
+ ON CONFLICT (code) DO UPDATE SET
113
+ device_id = EXCLUDED.device_id,
114
+ created_at = EXCLUDED.created_at
115
+ `, [code, record.deviceId, record.createdAt]);
116
+ }
117
+ async getPendingCode(code) {
118
+ await this.ensureReady();
119
+ const result = await this.pool.query(`
120
+ SELECT device_id, created_at
121
+ FROM pending_codes
122
+ WHERE code = $1
123
+ `, [code]);
124
+ const row = result.rows[0];
125
+ if (!row)
126
+ return undefined;
127
+ return { deviceId: row.device_id, createdAt: Number(row.created_at) };
128
+ }
129
+ async deletePendingCode(code) {
130
+ await this.ensureReady();
131
+ await this.pool.query("DELETE FROM pending_codes WHERE code = $1", [code]);
132
+ }
133
+ async listPendingCodes() {
134
+ await this.ensureReady();
135
+ const result = await this.pool.query(`
136
+ SELECT code, device_id, created_at
137
+ FROM pending_codes
138
+ ORDER BY created_at ASC
139
+ `);
140
+ return result.rows.map((row) => ({
141
+ code: row.code,
142
+ record: { deviceId: row.device_id, createdAt: Number(row.created_at) }
143
+ }));
144
+ }
145
+ async getDeviceRegistry(deviceId) {
146
+ await this.ensureReady();
147
+ const result = await this.pool.query(`
148
+ SELECT *
149
+ FROM device_registry
150
+ WHERE device_id = $1
151
+ `, [deviceId]);
152
+ const row = result.rows[0];
153
+ return row ? this.mapDeviceRegistryRow(row) : undefined;
154
+ }
155
+ async setDeviceRegistry(deviceId, record) {
156
+ await this.ensureReady();
157
+ await this.pool.query(`
158
+ INSERT INTO device_registry (
159
+ device_id,
160
+ permanent_pairing_code,
161
+ code_created_at,
162
+ serial_number,
163
+ first_seen_at,
164
+ last_hello_at
165
+ )
166
+ VALUES ($1, $2, $3, $4, $5, $6)
167
+ ON CONFLICT (device_id) DO UPDATE SET
168
+ permanent_pairing_code = EXCLUDED.permanent_pairing_code,
169
+ code_created_at = EXCLUDED.code_created_at,
170
+ serial_number = EXCLUDED.serial_number,
171
+ first_seen_at = EXCLUDED.first_seen_at,
172
+ last_hello_at = EXCLUDED.last_hello_at
173
+ `, [
174
+ deviceId,
175
+ record.permanentPairingCode,
176
+ record.codeCreatedAt,
177
+ record.serialNumber ?? null,
178
+ record.firstSeenAt ?? null,
179
+ record.lastHelloAt ?? null
180
+ ]);
181
+ }
182
+ async getDeviceRegistryByCode(code) {
183
+ await this.ensureReady();
184
+ const result = await this.pool.query(`
185
+ SELECT *
186
+ FROM device_registry
187
+ WHERE permanent_pairing_code = $1
188
+ `, [code]);
189
+ const row = result.rows[0];
190
+ if (!row)
191
+ return undefined;
192
+ return {
193
+ deviceId: row.device_id,
194
+ record: this.mapDeviceRegistryRow(row)
195
+ };
196
+ }
197
+ async listDeviceRegistry() {
198
+ await this.ensureReady();
199
+ const result = await this.pool.query(`
200
+ SELECT *
201
+ FROM device_registry
202
+ ORDER BY code_created_at ASC
203
+ `);
204
+ return result.rows.map((row) => ({
205
+ deviceId: row.device_id,
206
+ record: this.mapDeviceRegistryRow(row)
207
+ }));
208
+ }
209
+ async setPairedDevice(deviceId, record) {
210
+ await this.ensureReady();
211
+ await this.pool.query(`
212
+ INSERT INTO paired_devices (
213
+ device_id,
214
+ pairing_token,
215
+ paired_at,
216
+ device_name,
217
+ platform,
218
+ system,
219
+ last_boot_at,
220
+ last_online_at,
221
+ last_offline_at,
222
+ last_policy_push_at
223
+ )
224
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
225
+ ON CONFLICT (device_id) DO UPDATE SET
226
+ pairing_token = EXCLUDED.pairing_token,
227
+ paired_at = EXCLUDED.paired_at,
228
+ device_name = EXCLUDED.device_name,
229
+ platform = EXCLUDED.platform,
230
+ system = EXCLUDED.system,
231
+ last_boot_at = EXCLUDED.last_boot_at,
232
+ last_online_at = EXCLUDED.last_online_at,
233
+ last_offline_at = EXCLUDED.last_offline_at,
234
+ last_policy_push_at = EXCLUDED.last_policy_push_at
235
+ `, [
236
+ deviceId,
237
+ record.pairingToken,
238
+ record.pairedAt,
239
+ record.deviceName ?? null,
240
+ record.platform ?? null,
241
+ record.system ?? null,
242
+ record.lastBootAt ?? null,
243
+ record.lastOnlineAt ?? null,
244
+ record.lastOfflineAt ?? null,
245
+ record.lastPolicyPushAt ?? null
246
+ ]);
247
+ }
248
+ async getPairedDevice(deviceId) {
249
+ await this.ensureReady();
250
+ const result = await this.pool.query(`
251
+ SELECT *
252
+ FROM paired_devices
253
+ WHERE device_id = $1
254
+ `, [deviceId]);
255
+ const row = result.rows[0];
256
+ return row ? this.mapPairedDeviceRow(row) : undefined;
257
+ }
258
+ async deletePairedDevice(deviceId) {
259
+ await this.ensureReady();
260
+ await this.pool.query("DELETE FROM paired_devices WHERE device_id = $1", [
261
+ deviceId
262
+ ]);
263
+ }
264
+ async listPairedDevices() {
265
+ await this.ensureReady();
266
+ const result = await this.pool.query(`
267
+ SELECT *
268
+ FROM paired_devices
269
+ ORDER BY paired_at ASC
270
+ `);
271
+ return result.rows.map((row) => ({
272
+ deviceId: row.device_id,
273
+ record: this.mapPairedDeviceRow(row)
274
+ }));
275
+ }
276
+ async listPlaylists() {
277
+ await this.ensureReady();
278
+ const result = await this.pool.query(`
279
+ SELECT *
280
+ FROM playlists
281
+ ORDER BY updated_at DESC
282
+ `);
283
+ return result.rows.map((row) => this.mapPlaylistRow(row));
284
+ }
285
+ async getPlaylist(id) {
286
+ await this.ensureReady();
287
+ const result = await this.pool.query(`
288
+ SELECT *
289
+ FROM playlists
290
+ WHERE id = $1
291
+ `, [id]);
292
+ const row = result.rows[0];
293
+ return row ? this.mapPlaylistRow(row) : undefined;
294
+ }
295
+ async setPlaylist(record) {
296
+ await this.ensureReady();
297
+ await this.pool.query(`
298
+ INSERT INTO playlists (
299
+ id,
300
+ name,
301
+ schedule_json,
302
+ items_json,
303
+ version,
304
+ updated_at,
305
+ retired,
306
+ retired_at
307
+ )
308
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
309
+ ON CONFLICT (id) DO UPDATE SET
310
+ name = EXCLUDED.name,
311
+ schedule_json = EXCLUDED.schedule_json,
312
+ items_json = EXCLUDED.items_json,
313
+ version = EXCLUDED.version,
314
+ updated_at = EXCLUDED.updated_at,
315
+ retired = EXCLUDED.retired,
316
+ retired_at = EXCLUDED.retired_at
317
+ `, [
318
+ record.id,
319
+ record.name,
320
+ record.schedule ? JSON.stringify(record.schedule) : null,
321
+ JSON.stringify(record.items ?? []),
322
+ record.version,
323
+ record.updatedAt,
324
+ record.retired === true,
325
+ record.retiredAt ?? null
326
+ ]);
327
+ }
328
+ async isPlaylistNameTaken(name, excludeId) {
329
+ await this.ensureReady();
330
+ const target = name.trim().toLowerCase();
331
+ if (!target)
332
+ return false;
333
+ const result = await this.pool.query(`
334
+ SELECT id
335
+ FROM playlists
336
+ WHERE retired = false
337
+ AND lower(trim(name)) = $1
338
+ AND ($2::text IS NULL OR id != $2)
339
+ LIMIT 1
340
+ `, [target, excludeId ?? null]);
341
+ return result.rows.length > 0;
342
+ }
343
+ async getDeviceAssignments(deviceId) {
344
+ await this.ensureReady();
345
+ const result = await this.pool.query(`
346
+ SELECT playlist_id, published_version, published_at, snapshot_json
347
+ FROM device_assignments
348
+ WHERE device_id = $1
349
+ ORDER BY sort_order ASC
350
+ `, [deviceId]);
351
+ return result.rows.map((row) => ({
352
+ playlistId: row.playlist_id,
353
+ publishedVersion: row.published_version,
354
+ publishedAt: row.published_at,
355
+ snapshot: parseJson(row.snapshot_json, {
356
+ id: row.playlist_id,
357
+ name: row.playlist_id,
358
+ version: row.published_version,
359
+ items: []
360
+ })
361
+ }));
362
+ }
363
+ async setDeviceAssignments(deviceId, assignments) {
364
+ await this.ensureReady();
365
+ const client = await this.pool.connect();
366
+ try {
367
+ await client.query("BEGIN");
368
+ await client.query("DELETE FROM device_assignments WHERE device_id = $1", [
369
+ deviceId
370
+ ]);
371
+ for (const [index, assignment] of assignments.entries()) {
372
+ await client.query(`
373
+ INSERT INTO device_assignments (
374
+ device_id,
375
+ playlist_id,
376
+ sort_order,
377
+ published_version,
378
+ published_at,
379
+ snapshot_json
380
+ )
381
+ VALUES ($1, $2, $3, $4, $5, $6)
382
+ `, [
383
+ deviceId,
384
+ assignment.playlistId,
385
+ index,
386
+ assignment.publishedVersion,
387
+ assignment.publishedAt,
388
+ JSON.stringify(assignment.snapshot)
389
+ ]);
390
+ }
391
+ await client.query("COMMIT");
392
+ }
393
+ catch (err) {
394
+ await client.query("ROLLBACK");
395
+ throw err;
396
+ }
397
+ finally {
398
+ client.release();
399
+ }
400
+ }
401
+ mapDeviceRegistryRow(row) {
402
+ return {
403
+ permanentPairingCode: row.permanent_pairing_code,
404
+ codeCreatedAt: Number(row.code_created_at),
405
+ serialNumber: optionalString(row.serial_number),
406
+ firstSeenAt: optionalNumber(row.first_seen_at),
407
+ lastHelloAt: optionalNumber(row.last_hello_at)
408
+ };
409
+ }
410
+ mapPairedDeviceRow(row) {
411
+ return {
412
+ pairingToken: row.pairing_token,
413
+ pairedAt: row.paired_at,
414
+ deviceName: optionalString(row.device_name),
415
+ platform: optionalString(row.platform),
416
+ system: optionalString(row.system),
417
+ lastBootAt: optionalString(row.last_boot_at),
418
+ lastOnlineAt: optionalString(row.last_online_at),
419
+ lastOfflineAt: optionalString(row.last_offline_at),
420
+ lastPolicyPushAt: optionalString(row.last_policy_push_at)
421
+ };
422
+ }
423
+ mapPlaylistRow(row) {
424
+ return {
425
+ id: row.id,
426
+ name: row.name,
427
+ schedule: parseJson(row.schedule_json, undefined),
428
+ items: parseJson(row.items_json, []),
429
+ version: row.version,
430
+ updatedAt: row.updated_at,
431
+ retired: row.retired,
432
+ retiredAt: optionalString(row.retired_at)
433
+ };
434
+ }
435
+ }
@@ -0,0 +1,41 @@
1
+ import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore } from "./types.js";
2
+ interface SQLiteStoreOptions {
3
+ databasePath?: string;
4
+ }
5
+ /**
6
+ * Durable single-node store backed by a local SQLite database.
7
+ * For multi-instance production, inject a Postgres/Supabase-backed TomorrowOSStore instead.
8
+ */
9
+ export declare class SQLiteStore implements TomorrowOSMigratableStore {
10
+ readonly databasePath: string;
11
+ private readonly db;
12
+ constructor(options?: SQLiteStoreOptions | string);
13
+ init(): void;
14
+ close(): void;
15
+ setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
16
+ getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
17
+ deletePendingCode(code: string): Promise<void>;
18
+ listPendingCodes(): Promise<PendingCodeEntry[]>;
19
+ getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined>;
20
+ setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void>;
21
+ getDeviceRegistryByCode(code: string): Promise<{
22
+ deviceId: string;
23
+ record: DeviceRegistryRecord;
24
+ } | undefined>;
25
+ listDeviceRegistry(): Promise<DeviceRegistryEntry[]>;
26
+ setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
27
+ getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
28
+ deletePairedDevice(deviceId: string): Promise<void>;
29
+ listPairedDevices(): Promise<PairedDeviceEntry[]>;
30
+ listPlaylists(): Promise<StoredPlaylist[]>;
31
+ getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
32
+ setPlaylist(record: StoredPlaylist): Promise<void>;
33
+ isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
34
+ getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
35
+ setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
36
+ private mapDeviceRegistryRow;
37
+ private mapPairedDeviceRow;
38
+ private mapPlaylistRow;
39
+ }
40
+ export {};
41
+ //# sourceMappingURL=sqlite-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+DD;;;GAGG;AACH,qBAAa,WAAY,YAAW,yBAAyB;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;gBAE3B,OAAO,GAAE,kBAAkB,GAAG,MAAW;IAerD,IAAI,IAAI,IAAI;IAwEZ,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAUpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAY/C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAStC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA2BV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAapE,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAYpD,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAuCV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IASpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAYjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAS1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAS5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcvE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAoBhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IA8BhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,cAAc;CAYvB"}