@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,367 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Database from "better-sqlite3";
4
+ const DEFAULT_SQLITE_PATH = path.join(process.cwd(), "data", "tomorrowos.db");
5
+ function optionalString(value) {
6
+ return value ?? undefined;
7
+ }
8
+ function optionalNumber(value) {
9
+ return value ?? undefined;
10
+ }
11
+ function parseJson(raw, fallback) {
12
+ if (!raw)
13
+ return fallback;
14
+ return JSON.parse(raw);
15
+ }
16
+ /**
17
+ * Durable single-node store backed by a local SQLite database.
18
+ * For multi-instance production, inject a Postgres/Supabase-backed TomorrowOSStore instead.
19
+ */
20
+ export class SQLiteStore {
21
+ databasePath;
22
+ db;
23
+ constructor(options = {}) {
24
+ const databasePath = typeof options === "string"
25
+ ? options
26
+ : options.databasePath ?? DEFAULT_SQLITE_PATH;
27
+ this.databasePath = databasePath === ":memory:" ? databasePath : path.resolve(databasePath);
28
+ if (this.databasePath !== ":memory:") {
29
+ fs.mkdirSync(path.dirname(this.databasePath), { recursive: true });
30
+ }
31
+ this.db = new Database(this.databasePath);
32
+ this.init();
33
+ }
34
+ init() {
35
+ this.db.pragma("foreign_keys = ON");
36
+ this.db.pragma("journal_mode = WAL");
37
+ this.db.pragma("busy_timeout = 5000");
38
+ this.db.exec(`
39
+ CREATE TABLE IF NOT EXISTS schema_migrations (
40
+ id INTEGER PRIMARY KEY,
41
+ name TEXT NOT NULL UNIQUE,
42
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS pending_codes (
46
+ code TEXT PRIMARY KEY,
47
+ device_id TEXT NOT NULL,
48
+ created_at INTEGER NOT NULL
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS device_registry (
52
+ device_id TEXT PRIMARY KEY,
53
+ permanent_pairing_code TEXT NOT NULL UNIQUE,
54
+ code_created_at INTEGER NOT NULL,
55
+ serial_number TEXT,
56
+ first_seen_at INTEGER,
57
+ last_hello_at INTEGER
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS paired_devices (
61
+ device_id TEXT PRIMARY KEY,
62
+ pairing_token TEXT NOT NULL,
63
+ paired_at TEXT NOT NULL,
64
+ device_name TEXT,
65
+ platform TEXT,
66
+ system TEXT,
67
+ last_boot_at TEXT,
68
+ last_online_at TEXT,
69
+ last_offline_at TEXT,
70
+ last_policy_push_at TEXT
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS playlists (
74
+ id TEXT PRIMARY KEY,
75
+ name TEXT NOT NULL,
76
+ schedule_json TEXT,
77
+ items_json TEXT NOT NULL DEFAULT '[]',
78
+ version INTEGER NOT NULL,
79
+ updated_at TEXT NOT NULL,
80
+ retired INTEGER NOT NULL DEFAULT 0,
81
+ retired_at TEXT
82
+ );
83
+
84
+ CREATE UNIQUE INDEX IF NOT EXISTS playlists_active_name_unique
85
+ ON playlists (lower(trim(name)))
86
+ WHERE retired = 0;
87
+
88
+ CREATE TABLE IF NOT EXISTS device_assignments (
89
+ device_id TEXT NOT NULL,
90
+ playlist_id TEXT NOT NULL,
91
+ sort_order INTEGER NOT NULL,
92
+ published_version INTEGER NOT NULL,
93
+ published_at TEXT NOT NULL,
94
+ snapshot_json TEXT NOT NULL,
95
+ PRIMARY KEY (device_id, playlist_id)
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS device_assignments_device_order_idx
99
+ ON device_assignments (device_id, sort_order);
100
+
101
+ INSERT OR IGNORE INTO schema_migrations (id, name)
102
+ VALUES (1, 'initial_tomorrowos_store');
103
+ `);
104
+ }
105
+ close() {
106
+ this.db.close();
107
+ }
108
+ async setPendingCode(code, record) {
109
+ this.db.prepare(`
110
+ INSERT INTO pending_codes (code, device_id, created_at)
111
+ VALUES (?, ?, ?)
112
+ ON CONFLICT(code) DO UPDATE SET
113
+ device_id = excluded.device_id,
114
+ created_at = excluded.created_at
115
+ `).run(code, record.deviceId, record.createdAt);
116
+ }
117
+ async getPendingCode(code) {
118
+ const row = this.db.prepare(`
119
+ SELECT device_id, created_at
120
+ FROM pending_codes
121
+ WHERE code = ?
122
+ `).get(code);
123
+ if (!row)
124
+ return undefined;
125
+ return { deviceId: row.device_id, createdAt: row.created_at };
126
+ }
127
+ async deletePendingCode(code) {
128
+ this.db.prepare("DELETE FROM pending_codes WHERE code = ?").run(code);
129
+ }
130
+ async listPendingCodes() {
131
+ const rows = this.db.prepare(`
132
+ SELECT code, device_id, created_at
133
+ FROM pending_codes
134
+ ORDER BY created_at ASC
135
+ `).all();
136
+ return rows.map((row) => ({
137
+ code: row.code,
138
+ record: { deviceId: row.device_id, createdAt: row.created_at }
139
+ }));
140
+ }
141
+ async getDeviceRegistry(deviceId) {
142
+ const row = this.db.prepare(`
143
+ SELECT *
144
+ FROM device_registry
145
+ WHERE device_id = ?
146
+ `).get(deviceId);
147
+ return row ? this.mapDeviceRegistryRow(row) : undefined;
148
+ }
149
+ async setDeviceRegistry(deviceId, record) {
150
+ this.db.prepare(`
151
+ INSERT INTO device_registry (
152
+ device_id,
153
+ permanent_pairing_code,
154
+ code_created_at,
155
+ serial_number,
156
+ first_seen_at,
157
+ last_hello_at
158
+ )
159
+ VALUES (?, ?, ?, ?, ?, ?)
160
+ ON CONFLICT(device_id) DO UPDATE SET
161
+ permanent_pairing_code = excluded.permanent_pairing_code,
162
+ code_created_at = excluded.code_created_at,
163
+ serial_number = excluded.serial_number,
164
+ first_seen_at = excluded.first_seen_at,
165
+ last_hello_at = excluded.last_hello_at
166
+ `).run(deviceId, record.permanentPairingCode, record.codeCreatedAt, record.serialNumber ?? null, record.firstSeenAt ?? null, record.lastHelloAt ?? null);
167
+ }
168
+ async getDeviceRegistryByCode(code) {
169
+ const row = this.db.prepare(`
170
+ SELECT *
171
+ FROM device_registry
172
+ WHERE permanent_pairing_code = ?
173
+ `).get(code);
174
+ if (!row)
175
+ return undefined;
176
+ return {
177
+ deviceId: row.device_id,
178
+ record: this.mapDeviceRegistryRow(row)
179
+ };
180
+ }
181
+ async listDeviceRegistry() {
182
+ const rows = this.db.prepare(`
183
+ SELECT *
184
+ FROM device_registry
185
+ ORDER BY code_created_at ASC
186
+ `).all();
187
+ return rows.map((row) => ({
188
+ deviceId: row.device_id,
189
+ record: this.mapDeviceRegistryRow(row)
190
+ }));
191
+ }
192
+ async setPairedDevice(deviceId, record) {
193
+ this.db.prepare(`
194
+ INSERT INTO paired_devices (
195
+ device_id,
196
+ pairing_token,
197
+ paired_at,
198
+ device_name,
199
+ platform,
200
+ system,
201
+ last_boot_at,
202
+ last_online_at,
203
+ last_offline_at,
204
+ last_policy_push_at
205
+ )
206
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
207
+ ON CONFLICT(device_id) DO UPDATE SET
208
+ pairing_token = excluded.pairing_token,
209
+ paired_at = excluded.paired_at,
210
+ device_name = excluded.device_name,
211
+ platform = excluded.platform,
212
+ system = excluded.system,
213
+ last_boot_at = excluded.last_boot_at,
214
+ last_online_at = excluded.last_online_at,
215
+ last_offline_at = excluded.last_offline_at,
216
+ last_policy_push_at = excluded.last_policy_push_at
217
+ `).run(deviceId, record.pairingToken, record.pairedAt, record.deviceName ?? null, record.platform ?? null, record.system ?? null, record.lastBootAt ?? null, record.lastOnlineAt ?? null, record.lastOfflineAt ?? null, record.lastPolicyPushAt ?? null);
218
+ }
219
+ async getPairedDevice(deviceId) {
220
+ const row = this.db.prepare(`
221
+ SELECT *
222
+ FROM paired_devices
223
+ WHERE device_id = ?
224
+ `).get(deviceId);
225
+ return row ? this.mapPairedDeviceRow(row) : undefined;
226
+ }
227
+ async deletePairedDevice(deviceId) {
228
+ this.db.prepare("DELETE FROM paired_devices WHERE device_id = ?").run(deviceId);
229
+ }
230
+ async listPairedDevices() {
231
+ const rows = this.db.prepare(`
232
+ SELECT *
233
+ FROM paired_devices
234
+ ORDER BY paired_at ASC
235
+ `).all();
236
+ return rows.map((row) => ({
237
+ deviceId: row.device_id,
238
+ record: this.mapPairedDeviceRow(row)
239
+ }));
240
+ }
241
+ async listPlaylists() {
242
+ const rows = this.db.prepare(`
243
+ SELECT *
244
+ FROM playlists
245
+ ORDER BY updated_at DESC
246
+ `).all();
247
+ return rows.map((row) => this.mapPlaylistRow(row));
248
+ }
249
+ async getPlaylist(id) {
250
+ const row = this.db.prepare(`
251
+ SELECT *
252
+ FROM playlists
253
+ WHERE id = ?
254
+ `).get(id);
255
+ return row ? this.mapPlaylistRow(row) : undefined;
256
+ }
257
+ async setPlaylist(record) {
258
+ this.db.prepare(`
259
+ INSERT INTO playlists (
260
+ id,
261
+ name,
262
+ schedule_json,
263
+ items_json,
264
+ version,
265
+ updated_at,
266
+ retired,
267
+ retired_at
268
+ )
269
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
270
+ ON CONFLICT(id) DO UPDATE SET
271
+ name = excluded.name,
272
+ schedule_json = excluded.schedule_json,
273
+ items_json = excluded.items_json,
274
+ version = excluded.version,
275
+ updated_at = excluded.updated_at,
276
+ retired = excluded.retired,
277
+ retired_at = excluded.retired_at
278
+ `).run(record.id, record.name, record.schedule ? JSON.stringify(record.schedule) : null, JSON.stringify(record.items ?? []), record.version, record.updatedAt, record.retired ? 1 : 0, record.retiredAt ?? null);
279
+ }
280
+ async isPlaylistNameTaken(name, excludeId) {
281
+ const target = name.trim().toLowerCase();
282
+ if (!target)
283
+ return false;
284
+ const row = this.db.prepare(`
285
+ SELECT id
286
+ FROM playlists
287
+ WHERE retired = 0
288
+ AND lower(trim(name)) = ?
289
+ AND (? IS NULL OR id != ?)
290
+ LIMIT 1
291
+ `).get(target, excludeId ?? null, excludeId ?? null);
292
+ return !!row;
293
+ }
294
+ async getDeviceAssignments(deviceId) {
295
+ const rows = this.db.prepare(`
296
+ SELECT playlist_id, published_version, published_at, snapshot_json
297
+ FROM device_assignments
298
+ WHERE device_id = ?
299
+ ORDER BY sort_order ASC
300
+ `).all(deviceId);
301
+ return rows.map((row) => ({
302
+ playlistId: row.playlist_id,
303
+ publishedVersion: row.published_version,
304
+ publishedAt: row.published_at,
305
+ snapshot: parseJson(row.snapshot_json, {
306
+ id: row.playlist_id,
307
+ name: row.playlist_id,
308
+ version: row.published_version,
309
+ items: []
310
+ })
311
+ }));
312
+ }
313
+ async setDeviceAssignments(deviceId, assignments) {
314
+ const replaceAssignments = this.db.transaction(() => {
315
+ this.db.prepare("DELETE FROM device_assignments WHERE device_id = ?").run(deviceId);
316
+ const insert = this.db.prepare(`
317
+ INSERT INTO device_assignments (
318
+ device_id,
319
+ playlist_id,
320
+ sort_order,
321
+ published_version,
322
+ published_at,
323
+ snapshot_json
324
+ )
325
+ VALUES (?, ?, ?, ?, ?, ?)
326
+ `);
327
+ assignments.forEach((assignment, index) => {
328
+ insert.run(deviceId, assignment.playlistId, index, assignment.publishedVersion, assignment.publishedAt, JSON.stringify(assignment.snapshot));
329
+ });
330
+ });
331
+ replaceAssignments();
332
+ }
333
+ mapDeviceRegistryRow(row) {
334
+ return {
335
+ permanentPairingCode: row.permanent_pairing_code,
336
+ codeCreatedAt: row.code_created_at,
337
+ serialNumber: optionalString(row.serial_number),
338
+ firstSeenAt: optionalNumber(row.first_seen_at),
339
+ lastHelloAt: optionalNumber(row.last_hello_at)
340
+ };
341
+ }
342
+ mapPairedDeviceRow(row) {
343
+ return {
344
+ pairingToken: row.pairing_token,
345
+ pairedAt: row.paired_at,
346
+ deviceName: optionalString(row.device_name),
347
+ platform: optionalString(row.platform),
348
+ system: optionalString(row.system),
349
+ lastBootAt: optionalString(row.last_boot_at),
350
+ lastOnlineAt: optionalString(row.last_online_at),
351
+ lastOfflineAt: optionalString(row.last_offline_at),
352
+ lastPolicyPushAt: optionalString(row.last_policy_push_at)
353
+ };
354
+ }
355
+ mapPlaylistRow(row) {
356
+ return {
357
+ id: row.id,
358
+ name: row.name,
359
+ schedule: parseJson(row.schedule_json, undefined),
360
+ items: parseJson(row.items_json, []),
361
+ version: row.version,
362
+ updatedAt: row.updated_at,
363
+ retired: row.retired === 1,
364
+ retiredAt: optionalString(row.retired_at)
365
+ };
366
+ }
367
+ }
@@ -29,6 +29,14 @@ export interface PairedDeviceEntry {
29
29
  deviceId: string;
30
30
  record: PairedDeviceRecord;
31
31
  }
32
+ export interface PendingCodeEntry {
33
+ code: string;
34
+ record: PendingCodeRecord;
35
+ }
36
+ export interface DeviceRegistryEntry {
37
+ deviceId: string;
38
+ record: DeviceRegistryRecord;
39
+ }
32
40
  export interface PlaylistItemRecord {
33
41
  url: string;
34
42
  type?: string;
@@ -93,4 +101,18 @@ export interface TomorrowOSStore {
93
101
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
94
102
  setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
95
103
  }
104
+ export interface TomorrowOSDataSnapshot {
105
+ pendingCodes: PendingCodeEntry[];
106
+ deviceRegistry: DeviceRegistryEntry[];
107
+ pairedDevices: PairedDeviceEntry[];
108
+ playlists: StoredPlaylist[];
109
+ deviceAssignments: Array<{
110
+ deviceId: string;
111
+ assignments: DevicePlaylistAssignment[];
112
+ }>;
113
+ }
114
+ export interface TomorrowOSMigratableStore extends TomorrowOSStore {
115
+ listPendingCodes(): Promise<PendingCodeEntry[]>;
116
+ listDeviceRegistry(): Promise<DeviceRegistryEntry[]>;
117
+ }
96
118
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,13 +32,16 @@
32
32
  "node": ">=18"
33
33
  },
34
34
  "dependencies": {
35
+ "better-sqlite3": "^12.11.1",
36
+ "pg": "^8.22.0",
35
37
  "ws": "^8.18.0"
36
38
  },
37
39
  "devDependencies": {
40
+ "@types/better-sqlite3": "^7.6.13",
38
41
  "@types/node": "^20.19.41",
42
+ "@types/pg": "^8.20.0",
39
43
  "@types/ws": "^8.5.10",
40
44
  "typescript": "^5.5.0"
41
45
  },
42
46
  "license": "Apache-2.0"
43
47
  }
44
-
@@ -0,0 +1,10 @@
1
+ # Default local database.
2
+ TOMORROWOS_STORE=sqlite
3
+ TOMORROWOS_DB_PATH=./data/tomorrowos.db
4
+
5
+ # Supabase/Postgres option.
6
+ # TOMORROWOS_STORE=supabase
7
+ # DATABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
8
+ # DATABASE_SSL=true
9
+
10
+ PORT=3000
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
5
5
  "private": true,
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  "build-player": "tomorrowos build --platform tizen"
11
11
  },
12
12
  "dependencies": {
13
- "@tomorrowos/sdk": "^0.4.7"
13
+ "@tomorrowos/sdk": "^0.5.0",
14
+ "dotenv": "^17.2.3"
14
15
  },
15
16
  "devDependencies": {
16
17
  "@types/node": "^20.0.0",
@@ -1,22 +1,26 @@
1
1
  /**
2
2
  * TomorrowOS CMS server — minimal starter.
3
- * Add routes, database, or serve a React build via listen({ staticRoot }).
3
+ * Uses SQLite by default so pairings/playlists survive server restarts.
4
4
  */
5
5
 
6
+ import "dotenv/config";
6
7
  import { readFileSync } from "fs";
7
8
  import { fileURLToPath } from "url";
8
9
  import { dirname, join } from "path";
9
- import { TomorrowOS } from "@tomorrowos/sdk";
10
+ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
14
+ const store = createTomorrowOSStore({
15
+ sqlitePath: join(__dirname, "data", "tomorrowos.db")
16
+ });
13
17
 
14
- const tomorrowos = new TomorrowOS({ brand });
18
+ const tomorrowos = new TomorrowOS({ brand, store });
15
19
 
16
20
  tomorrowos.listen({
17
21
  port: Number(process.env.PORT) || 3000,
18
22
  host: "0.0.0.0",
19
- staticRoot: join(__dirname,"public"),
23
+ staticRoot: join(__dirname, "public"),
20
24
  });
21
25
 
22
26
  tomorrowos.on("device.paired", (event) => {
@@ -0,0 +1,14 @@
1
+ # Database Interface Examples
2
+
3
+ The starter server already switches databases from `.env`:
4
+
5
+ - `TOMORROWOS_STORE=sqlite` uses `data/tomorrowos.db`.
6
+ - `TOMORROWOS_STORE=supabase` plus `DATABASE_URL=...` uses Supabase/Postgres.
7
+ - `TOMORROWOS_STORE=memory` is only for throwaway tests.
8
+
9
+ These files are reference examples for customers who want to understand or build
10
+ their own database interface. You do not need to import them for the starter to
11
+ work.
12
+
13
+ If you build a custom database backend, implement `TomorrowOSStore` and pass it
14
+ to `new TomorrowOS({ brand, store })` in `server.ts`.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reference only. The starter already uses this behavior by default.
3
+ */
4
+ import { SQLiteStore } from "@tomorrowos/sdk";
5
+
6
+ export function createSQLiteInterface() {
7
+ return new SQLiteStore({
8
+ databasePath: process.env.TOMORROWOS_DB_PATH || "./data/tomorrowos.db"
9
+ });
10
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Reference only. Supabase exposes a Postgres connection string, so TomorrowOS
3
+ * uses PostgresStore for Supabase-backed persistence.
4
+ */
5
+ import { PostgresStore } from "@tomorrowos/sdk";
6
+
7
+ export function createSupabaseInterface() {
8
+ const connectionString = process.env.DATABASE_URL;
9
+ if (!connectionString) {
10
+ throw new Error("DATABASE_URL is required for Supabase/Postgres.");
11
+ }
12
+
13
+ return new PostgresStore({
14
+ connectionString,
15
+ ssl: { rejectUnauthorized: false }
16
+ });
17
+ }